merge(dev): resolve latest manager and runtime-fallback conflicts
Sync the PR branch with the newest dev branch and resolve the new import-level conflicts in background-agent manager and runtime-fallback tests. Preserve both the delegated bootstrap coverage from this branch and the newer upstream test utilities and runtime wiring changes, then re-verify the affected delegated fallback suites and typecheck. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -6,6 +6,7 @@ import { describe, expect, mock, test } from "bun:test"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import { clearPendingStore, consumeToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { createBackgroundTask } from "./create-background-task"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode"
|
||||
|
||||
@@ -18,7 +19,7 @@ describe("createBackgroundTask metadata", () => {
|
||||
// #given
|
||||
clearPendingStore()
|
||||
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
launch: mock(() => Promise.resolve({
|
||||
id: "task-1",
|
||||
sessionID: null,
|
||||
@@ -27,12 +28,12 @@ describe("createBackgroundTask metadata", () => {
|
||||
status: "pending",
|
||||
})),
|
||||
getTask: mock(() => undefined),
|
||||
} as unknown as BackgroundManager
|
||||
const client = {
|
||||
})
|
||||
const client = unsafeTestValue<PluginInput["client"]>({
|
||||
session: {
|
||||
messages: mock(() => Promise.resolve({ data: [] })),
|
||||
},
|
||||
} as unknown as PluginInput["client"]
|
||||
})
|
||||
|
||||
let capturedMetadata: { title?: string; metadata?: Record<string, unknown> } | undefined
|
||||
const tool = createBackgroundTask(manager, client)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, test, expect, mock } from "bun:test"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { createBackgroundTask } from "./create-background-task"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("createBackgroundTask", () => {
|
||||
const launchMock = mock(async (): Promise<{
|
||||
@@ -21,16 +22,16 @@ describe("createBackgroundTask", () => {
|
||||
}))
|
||||
const getTaskMock = mock()
|
||||
|
||||
const mockManager = {
|
||||
const mockManager = unsafeTestValue<BackgroundManager>({
|
||||
launch: launchMock,
|
||||
getTask: getTaskMock,
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
|
||||
const mockClient = {
|
||||
const mockClient = unsafeTestValue<PluginInput["client"]>({
|
||||
session: {
|
||||
messages: mock(() => Promise.resolve({ data: [] })),
|
||||
},
|
||||
} as unknown as PluginInput["client"]
|
||||
})
|
||||
|
||||
const tool = createBackgroundTask(mockManager, mockClient)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BackgroundManager, BackgroundTask } from "../../features/backgroun
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import type { BackgroundCancelClient, BackgroundOutputManager, BackgroundOutputClient } from "./tools"
|
||||
import { consumeToolMetadata, clearPendingStore } from "../../features/tool-metadata-store"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode"
|
||||
|
||||
@@ -66,10 +67,10 @@ describe("background_output full_session", () => {
|
||||
const manager = createMockManager(task)
|
||||
const client = createMockClient({})
|
||||
const tool = createBackgroundOutput(manager, client)
|
||||
const ctxWithCallId = {
|
||||
const ctxWithCallId = unsafeTestValue<ToolContext>({
|
||||
...mockContext,
|
||||
callID: "call-1",
|
||||
} as unknown as ToolContext
|
||||
})
|
||||
|
||||
// #when
|
||||
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
||||
@@ -93,10 +94,10 @@ describe("background_output full_session", () => {
|
||||
const manager = createMockManager(task)
|
||||
const client = createMockClient({})
|
||||
const tool = createBackgroundOutput(manager, client)
|
||||
const ctxWithCallId = {
|
||||
const ctxWithCallId = unsafeTestValue<ToolContext>({
|
||||
...mockContext,
|
||||
callID: "call-1",
|
||||
} as unknown as ToolContext
|
||||
})
|
||||
|
||||
// #when
|
||||
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
||||
@@ -387,7 +388,7 @@ describe("background_cancel", () => {
|
||||
// #given
|
||||
const task = createTask({ status: "running" })
|
||||
const cancelled: string[] = []
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
getTask: (id: string) => (id === task.id ? task : undefined),
|
||||
getAllDescendantTasks: () => [task],
|
||||
cancelTask: async (taskId: string) => {
|
||||
@@ -395,7 +396,7 @@ describe("background_cancel", () => {
|
||||
task.status = "cancelled"
|
||||
return true
|
||||
},
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||
const tool = createBackgroundCancel(manager, client)
|
||||
|
||||
@@ -412,7 +413,7 @@ describe("background_cancel", () => {
|
||||
const taskA = createTask({ id: "task-a", status: "running" })
|
||||
const taskB = createTask({ id: "task-b", status: "pending" })
|
||||
const cancelled: string[] = []
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
getTask: () => undefined,
|
||||
getAllDescendantTasks: () => [taskA, taskB],
|
||||
cancelTask: async (taskId: string) => {
|
||||
@@ -421,7 +422,7 @@ describe("background_cancel", () => {
|
||||
task.status = "cancelled"
|
||||
return true
|
||||
},
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||
const tool = createBackgroundCancel(manager, client)
|
||||
|
||||
@@ -437,7 +438,7 @@ describe("background_cancel", () => {
|
||||
// #given
|
||||
const taskA = createTask({ id: "task-a", status: "running", sessionId: "ses-a", description: "running task" })
|
||||
const taskB = createTask({ id: "task-b", status: "pending", sessionId: undefined, description: "pending task" })
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
getTask: () => undefined,
|
||||
getAllDescendantTasks: () => [taskA, taskB],
|
||||
cancelTask: async (taskId: string) => {
|
||||
@@ -445,7 +446,7 @@ describe("background_cancel", () => {
|
||||
task.status = "cancelled"
|
||||
return true
|
||||
},
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||
const tool = createBackgroundCancel(manager, client)
|
||||
|
||||
@@ -461,7 +462,7 @@ describe("background_cancel", () => {
|
||||
// #given
|
||||
const task = createTask({ id: "task-1", status: "running" })
|
||||
const cancelOptions: Array<{ taskId: string; options: unknown }> = []
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
getTask: (id: string) => (id === task.id ? task : undefined),
|
||||
getAllDescendantTasks: () => [task],
|
||||
cancelTask: async (taskId: string, options?: unknown) => {
|
||||
@@ -469,7 +470,7 @@ describe("background_cancel", () => {
|
||||
task.status = "cancelled"
|
||||
return true
|
||||
},
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||
const tool = createBackgroundCancel(manager, client)
|
||||
|
||||
@@ -487,7 +488,7 @@ describe("background_cancel", () => {
|
||||
// #given
|
||||
const task = createTask({ id: "task-1", status: "running" })
|
||||
const cancelOptions: Array<{ taskId: string; options: unknown }> = []
|
||||
const manager = {
|
||||
const manager = unsafeTestValue<BackgroundManager>({
|
||||
getTask: (id: string) => (id === task.id ? task : undefined),
|
||||
getAllDescendantTasks: () => [task],
|
||||
cancelTask: async (taskId: string, options?: unknown) => {
|
||||
@@ -495,7 +496,7 @@ describe("background_cancel", () => {
|
||||
task.status = "cancelled"
|
||||
return true
|
||||
},
|
||||
} as unknown as BackgroundManager
|
||||
})
|
||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||
const tool = createBackgroundCancel(manager, client)
|
||||
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
/**
|
||||
* Requirement-based tests for resolveCallableAgents().
|
||||
*
|
||||
* These tests are derived from behavioral requirements in the PR description
|
||||
* and feature spec, NOT from reading the implementation:
|
||||
*
|
||||
* R1: ALLOWED_AGENTS always present as baseline
|
||||
* R2: Dynamic agents from client.app.agents() merged into the result
|
||||
* R3: Primary-mode agents excluded from callable list
|
||||
* R4: Falls back to ALLOWED_AGENTS alone when client.app.agents() fails
|
||||
* R5: All output names are lowercase
|
||||
* R6: No duplicate agent names in output
|
||||
* R7: Malformed agent entries (null, missing name, non-string name, whitespace-only) are skipped gracefully
|
||||
*/
|
||||
const { describe, test, expect, mock, beforeEach } = require("bun:test")
|
||||
const { resolveCallableAgents, clearCallableAgentsCache } = require("./agent-resolver")
|
||||
const { ALLOWED_AGENTS } = require("./constants")
|
||||
|
||||
function createMockClient(agents: Array<Record<string, unknown>>) {
|
||||
function createMockClient(agents: Array<Record<string, string>> = []) {
|
||||
return {
|
||||
app: {
|
||||
agents: mock(() => Promise.resolve({ data: agents })),
|
||||
@@ -24,215 +10,54 @@ function createMockClient(agents: Array<Record<string, unknown>>) {
|
||||
}
|
||||
}
|
||||
|
||||
function createFailingClient(error: Error = new Error("API unavailable")) {
|
||||
return {
|
||||
app: {
|
||||
agents: mock(() => Promise.reject(error)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("resolveCallableAgents", () => {
|
||||
beforeEach(() => {
|
||||
clearCallableAgentsCache()
|
||||
})
|
||||
|
||||
describe("#given the SDK returns agents successfully", () => {
|
||||
describe("#when only built-in agents exist", () => {
|
||||
test("#then every ALLOWED_AGENT appears in the result", async () => {
|
||||
const builtinAgents = ALLOWED_AGENTS.map((name: string) => ({
|
||||
name,
|
||||
mode: "subagent",
|
||||
}))
|
||||
const client = createMockClient(builtinAgents)
|
||||
describe("#given call_omo_agent is restricted to lookup agents", () => {
|
||||
test("#then only ALLOWED_AGENTS are returned", async () => {
|
||||
const client = createMockClient()
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
for (const agent of ALLOWED_AGENTS) {
|
||||
expect(result).toContain(agent)
|
||||
}
|
||||
})
|
||||
expect(result).toEqual([...ALLOWED_AGENTS])
|
||||
})
|
||||
|
||||
describe("#when dynamic custom agents are present alongside built-ins", () => {
|
||||
test("#then custom agents are included in the result", async () => {
|
||||
const agents = [
|
||||
...ALLOWED_AGENTS.map((name: string) => ({ name, mode: "subagent" })),
|
||||
{ name: "bug-fixer", mode: "subagent" },
|
||||
{ name: "code-reviewer", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
test("#then runtime custom agents are ignored and not queried", async () => {
|
||||
const client = createMockClient([
|
||||
{ name: "general", mode: "subagent" },
|
||||
{ name: "bug-fixer", mode: "subagent" },
|
||||
])
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).toContain("bug-fixer")
|
||||
expect(result).toContain("code-reviewer")
|
||||
})
|
||||
|
||||
test("#then ALLOWED_AGENTS are still present", async () => {
|
||||
const agents = [{ name: "custom-agent", mode: "subagent" }]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
for (const agent of ALLOWED_AGENTS) {
|
||||
expect(result).toContain(agent)
|
||||
}
|
||||
})
|
||||
expect(result).toEqual(["explore", "librarian"])
|
||||
expect(client.app.agents).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe("#when an agent has mode=primary", () => {
|
||||
test("#then it is excluded from the callable list", async () => {
|
||||
const agents = [
|
||||
{ name: "sisyphus", mode: "primary" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
test("#then non-lookup built-ins are not included", async () => {
|
||||
const client = createMockClient([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "hephaestus", mode: "subagent" },
|
||||
{ name: "metis", mode: "subagent" },
|
||||
])
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).not.toContain("sisyphus")
|
||||
expect(result).toContain("explore")
|
||||
})
|
||||
expect(result).not.toContain("oracle")
|
||||
expect(result).not.toContain("hephaestus")
|
||||
expect(result).not.toContain("metis")
|
||||
})
|
||||
|
||||
describe("#when agent names have mixed case", () => {
|
||||
test("#then all output names are lowercase", async () => {
|
||||
const agents = [
|
||||
{ name: "Bug-Fixer", mode: "subagent" },
|
||||
{ name: "CODE-REVIEWER", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
test("#then each call returns a defensive copy", async () => {
|
||||
const client = createMockClient()
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
const first = await resolveCallableAgents(client)
|
||||
first.push("general")
|
||||
const second = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).toContain("bug-fixer")
|
||||
expect(result).toContain("code-reviewer")
|
||||
for (const name of result) {
|
||||
expect(name).toBe(name.toLowerCase())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when duplicate agent names exist across sources", () => {
|
||||
test("#then no duplicates appear in the result", async () => {
|
||||
const agents = [
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "Explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
const exploreCount = result.filter((n: string) => n === "explore").length
|
||||
expect(exploreCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when agent entries are malformed", () => {
|
||||
test("#then entries with null name are skipped", async () => {
|
||||
const agents = [
|
||||
{ name: null, mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).toContain("explore")
|
||||
expect(result.length).toBeGreaterThanOrEqual(ALLOWED_AGENTS.length)
|
||||
})
|
||||
|
||||
test("#then entries with numeric name are skipped", async () => {
|
||||
const agents = [
|
||||
{ name: 42, mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).not.toContain("42")
|
||||
expect(result).toContain("explore")
|
||||
})
|
||||
|
||||
test("#then entries with whitespace-only name are skipped", async () => {
|
||||
const agents = [
|
||||
{ name: " ", mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).not.toContain("")
|
||||
expect(result).not.toContain(" ")
|
||||
expect(result).toContain("explore")
|
||||
})
|
||||
|
||||
test("#then entries with missing name property are skipped", async () => {
|
||||
const agents = [
|
||||
{ mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).toContain("explore")
|
||||
expect(result.length).toBeGreaterThanOrEqual(ALLOWED_AGENTS.length)
|
||||
})
|
||||
|
||||
test("#then entries that are undefined/null themselves are skipped", async () => {
|
||||
const agents = [
|
||||
null,
|
||||
undefined,
|
||||
{ name: "explore", mode: "subagent" },
|
||||
] as unknown as Array<Record<string, unknown>>
|
||||
const client = createMockClient(agents)
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).toContain("explore")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when SDK returns an empty list", () => {
|
||||
test("#then ALLOWED_AGENTS still appear as the baseline", async () => {
|
||||
const client = createMockClient([])
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
for (const agent of ALLOWED_AGENTS) {
|
||||
expect(result).toContain(agent)
|
||||
}
|
||||
expect(result.length).toBe(ALLOWED_AGENTS.length)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given the SDK call fails", () => {
|
||||
describe("#when client.app.agents() throws an error", () => {
|
||||
test("#then it falls back to ALLOWED_AGENTS", async () => {
|
||||
const client = createFailingClient(new Error("Network error"))
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result.length).toBe(ALLOWED_AGENTS.length)
|
||||
for (const agent of ALLOWED_AGENTS) {
|
||||
expect(result).toContain(agent)
|
||||
}
|
||||
})
|
||||
|
||||
test("#then custom agents are NOT available in fallback mode", async () => {
|
||||
const client = createFailingClient()
|
||||
|
||||
const result = await resolveCallableAgents(client)
|
||||
|
||||
expect(result).not.toContain("bug-fixer")
|
||||
expect(result).not.toContain("custom-agent")
|
||||
})
|
||||
expect(second).toEqual(["explore", "librarian"])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,64 +1,20 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
import { ALLOWED_AGENTS } from "./constants";
|
||||
import { normalizeSDKResponse } from "../../shared";
|
||||
import { log } from "../../shared/logger";
|
||||
|
||||
type AgentInfo = {
|
||||
name: string;
|
||||
mode?: "subagent" | "primary" | "all";
|
||||
};
|
||||
|
||||
const callableAgentsCache = new Map<string, { agents: string[]; timestamp: number }>();
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
|
||||
export function clearCallableAgentsCache(): void {
|
||||
callableAgentsCache.clear();
|
||||
// Kept for existing test setup and external callers; the resolver is now static.
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the set of callable agent names at execute-time by merging the
|
||||
* hardcoded `ALLOWED_AGENTS` with any additional agents discovered dynamically
|
||||
* via `client.app.agents()`. Custom agents loaded from registered agent
|
||||
* directories appear here alongside built-ins.
|
||||
* Resolves the set of callable agent names for call_omo_agent.
|
||||
*
|
||||
* Results are cached per session for 30s to avoid redundant SDK IPC calls.
|
||||
*
|
||||
* Falls back to `ALLOWED_AGENTS` alone if the dynamic lookup fails.
|
||||
*
|
||||
* @param client - The plugin client with access to the agent registry
|
||||
* @param sessionId - Optional session ID for cache scoping
|
||||
* @returns Array of lowercase callable agent names (excludes primary-mode agents)
|
||||
* This tool is deliberately narrower than delegate-task: it may only launch
|
||||
* the research lookup agents used by worker-style agents while they continue
|
||||
* local work. Dynamic agents and other built-ins must go through task().
|
||||
*/
|
||||
export async function resolveCallableAgents(
|
||||
client: PluginInput["client"],
|
||||
sessionId?: string,
|
||||
_client?: PluginInput["client"],
|
||||
_sessionId?: string,
|
||||
): Promise<string[]> {
|
||||
const cacheKey = sessionId ?? "__default__";
|
||||
const cached = callableAgentsCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
return cached.agents;
|
||||
}
|
||||
|
||||
try {
|
||||
const agentsResult = await client.app.agents();
|
||||
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
||||
preferResponseOnMissingData: true,
|
||||
});
|
||||
|
||||
const dynamicAgents = agents
|
||||
.filter((a) => a && typeof a.name === "string" && a.name.trim().length > 0 && a.mode !== "primary")
|
||||
.map((a) => a.name.trim().toLowerCase());
|
||||
|
||||
const merged = new Set([...ALLOWED_AGENTS, ...dynamicAgents]);
|
||||
const result = [...merged];
|
||||
callableAgentsCache.set(cacheKey, { agents: result, timestamp: Date.now() });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(
|
||||
"[call_omo_agent] Failed to resolve dynamic agents, falling back to built-in list",
|
||||
{ error: message },
|
||||
);
|
||||
return [...ALLOWED_AGENTS];
|
||||
}
|
||||
return [...ALLOWED_AGENTS];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, mock, test } from "bun:test"
|
||||
import { createCallOmoAgent } from "./tools"
|
||||
import { clearCallableAgentsCache } from "./agent-resolver"
|
||||
|
||||
type AgentEntry = {
|
||||
name: string
|
||||
mode: "subagent" | "primary" | "all"
|
||||
}
|
||||
|
||||
function createPluginInput(agents: AgentEntry[]) {
|
||||
return {
|
||||
client: {
|
||||
app: {
|
||||
agents: mock(() => Promise.resolve({ data: agents })),
|
||||
},
|
||||
},
|
||||
directory: "/test",
|
||||
}
|
||||
}
|
||||
|
||||
function createBackgroundManager() {
|
||||
const launch = mock(() => Promise.resolve({
|
||||
id: "task-id",
|
||||
sessionId: "session-id",
|
||||
description: "Test task",
|
||||
agent: "explore",
|
||||
status: "pending",
|
||||
}))
|
||||
|
||||
return {
|
||||
manager: {
|
||||
launch,
|
||||
getTask: mock(() => undefined),
|
||||
reserveSubagentSpawn: mock(() => Promise.resolve({
|
||||
spawnContext: { rootSessionID: "root", parentDepth: 0, childDepth: 1 },
|
||||
descendantCount: 1,
|
||||
commit: mock(() => undefined),
|
||||
rollback: mock(() => undefined),
|
||||
})),
|
||||
},
|
||||
launch,
|
||||
}
|
||||
}
|
||||
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "message-id",
|
||||
agent: "sisyphus-junior",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
describe("call_omo_agent restricted agent set", () => {
|
||||
test("#when runtime exposes general as a subagent #then call_omo_agent rejects it before launch", async () => {
|
||||
//#given
|
||||
clearCallableAgentsCache()
|
||||
const pluginInput = createPluginInput([
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
{ name: "general", mode: "subagent" },
|
||||
])
|
||||
const { manager, launch } = createBackgroundManager()
|
||||
const toolDefinition = createCallOmoAgent(pluginInput, manager)
|
||||
|
||||
//#when
|
||||
const result = await toolDefinition.execute(
|
||||
{ description: "Test", prompt: "Do work", subagent_type: "general", run_in_background: true },
|
||||
toolContext,
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).toContain("Only explore, librarian are allowed")
|
||||
expect(launch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("#when caller requests oracle #then call_omo_agent rejects it because only research lookup agents are callable", async () => {
|
||||
//#given
|
||||
clearCallableAgentsCache()
|
||||
const pluginInput = createPluginInput([
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
])
|
||||
const { manager, launch } = createBackgroundManager()
|
||||
const toolDefinition = createCallOmoAgent(pluginInput, manager)
|
||||
|
||||
//#when
|
||||
const result = await toolDefinition.execute(
|
||||
{ description: "Test", prompt: "Review this", subagent_type: "oracle", run_in_background: true },
|
||||
toolContext,
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).toContain("Only explore, librarian are allowed")
|
||||
expect(launch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("#when caller requests explore or librarian #then call_omo_agent still launches them", async () => {
|
||||
//#given
|
||||
clearCallableAgentsCache()
|
||||
const pluginInput = createPluginInput([
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
])
|
||||
const { manager, launch } = createBackgroundManager()
|
||||
const toolDefinition = createCallOmoAgent(pluginInput, manager)
|
||||
|
||||
//#when
|
||||
await toolDefinition.execute(
|
||||
{ description: "Explore", prompt: "Read code", subagent_type: "explore", run_in_background: true },
|
||||
toolContext,
|
||||
)
|
||||
await toolDefinition.execute(
|
||||
{ description: "Research", prompt: "Find docs", subagent_type: "librarian", run_in_background: true },
|
||||
toolContext,
|
||||
)
|
||||
|
||||
//#then
|
||||
expect(launch).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,13 @@
|
||||
export const ALLOWED_AGENTS = [
|
||||
"explore",
|
||||
"librarian",
|
||||
"oracle",
|
||||
"hephaestus",
|
||||
"metis",
|
||||
"momus",
|
||||
"multimodal-looker",
|
||||
] as const
|
||||
|
||||
export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent or custom agents. run_in_background REQUIRED (true=async with task_id, false=sync).
|
||||
export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent. run_in_background REQUIRED (true=async with task_id, false=sync).
|
||||
|
||||
Built-in agents:
|
||||
Allowed agents:
|
||||
{agents}
|
||||
|
||||
Custom agents registered via user or project agent directories are also supported.
|
||||
Other built-in agents, custom agents, and task categories are intentionally not supported by this tool.
|
||||
|
||||
Pass \`session_id=<id>\` to continue previous agent with full context. Nested subagent depth is tracked automatically and blocked past the configured limit. Prompts MUST be in English. Use \`background_output\` for async results.`
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { createOrGetSession } from "./session-creator"
|
||||
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("call-omo-agent createOrGetSession", () => {
|
||||
test("creates child session without overriding permission and tracks it as subagent session", async () => {
|
||||
@@ -37,12 +38,12 @@ describe("call-omo-agent createOrGetSession", () => {
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await createOrGetSession(args as any, toolContext as any, ctx as any)
|
||||
const result = await createOrGetSession(unsafeTestValue(args), unsafeTestValue(toolContext), unsafeTestValue(ctx))
|
||||
|
||||
// then
|
||||
expect(result).toEqual({ sessionID: "ses_child", isNew: true })
|
||||
expect(createCalls).toHaveLength(1)
|
||||
const createBody = (createCalls[0] as any)?.body
|
||||
const createBody = (unsafeTestValue(createCalls[0]))?.body
|
||||
expect(createBody?.parentID).toBe("ses_parent")
|
||||
expect(createBody?.permission).toBeUndefined()
|
||||
expect(subagentSessions.has("ses_child")).toBe(true)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { resolveOrCreateSessionId } from "./subagent-session-creator"
|
||||
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("call-omo-agent resolveOrCreateSessionId", () => {
|
||||
const originalPlatform = process.platform
|
||||
@@ -19,7 +20,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => {
|
||||
const { parentDirectory, contextDirectory } = options
|
||||
const parentSessionData = parentDirectory ? { data: { directory: parentDirectory } } : { data: {} }
|
||||
|
||||
const ctx = {
|
||||
const ctx = unsafeTestValue<Parameters<typeof resolveOrCreateSessionId>[0]>({
|
||||
directory: contextDirectory,
|
||||
client: {
|
||||
session: {
|
||||
@@ -31,7 +32,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof resolveOrCreateSessionId>[0]
|
||||
})
|
||||
|
||||
const args = {
|
||||
description: "sync test",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
const { describe, test, expect, mock } = require("bun:test")
|
||||
|
||||
type ExecuteSync = typeof import("./sync-executor").executeSync
|
||||
@@ -389,7 +390,7 @@ describe("executeSync", () => {
|
||||
}
|
||||
|
||||
//#when
|
||||
await executeSync(args, toolContext, ctx as any, deps, undefined, spawnReservation)
|
||||
await executeSync(args, toolContext, unsafeTestValue(ctx), deps, undefined, spawnReservation)
|
||||
|
||||
//#then
|
||||
expect(spawnReservation.commit).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -14,6 +14,10 @@ type SessionWithPromptAsync = {
|
||||
promptAsync: (opts: { path: { id: string }; body: Record<string, unknown> }) => Promise<unknown>
|
||||
}
|
||||
|
||||
function hasPromptAsync(session: PluginInput["client"]["session"]): session is PluginInput["client"]["session"] & SessionWithPromptAsync {
|
||||
return "promptAsync" in session && typeof session.promptAsync === "function"
|
||||
}
|
||||
|
||||
type ExecuteSyncDeps = {
|
||||
createOrGetSession: typeof createOrGetSession
|
||||
waitForCompletion: typeof waitForCompletion
|
||||
@@ -102,7 +106,11 @@ export async function executeSync(
|
||||
const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type)
|
||||
|
||||
try {
|
||||
await (ctx.client.session as unknown as SessionWithPromptAsync).promptAsync({
|
||||
if (!hasPromptAsync(ctx.client.session)) {
|
||||
return `Error: Failed to send prompt: promptAsync is not available on this OpenCode client.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||
}
|
||||
|
||||
await ctx.client.session.promptAsync({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: normalizedSubagentType,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Requirement-based integration tests for createCallOmoAgent edge cases
|
||||
* introduced by the dev rebase and dynamic agent resolution feature.
|
||||
* around restricted agent validation and execution cleanup.
|
||||
*
|
||||
* R1: Spawn reservation is rolled back when execution fails after reservation
|
||||
* R2: Agent names with leading/trailing whitespace are trimmed before matching
|
||||
* R3: An agent present in both ALLOWED_AGENTS and dynamic list is callable (no conflict)
|
||||
* R2: Dynamic runtime agents do not expand the call_omo_agent allowlist
|
||||
* R3: An agent present in both ALLOWED_AGENTS and runtime results is callable
|
||||
* R4: session_id continuation rejects in background mode when session already exists
|
||||
*/
|
||||
const { describe, test, expect, mock, beforeEach } = require("bun:test")
|
||||
@@ -27,11 +27,6 @@ function createMockCtx(agents: Array<{ name: string; mode?: string }> = []): Plu
|
||||
const DEFAULT_AGENTS = [
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "hephaestus", mode: "subagent" },
|
||||
{ name: "metis", mode: "subagent" },
|
||||
{ name: "momus", mode: "subagent" },
|
||||
{ name: "multimodal-looker", mode: "subagent" },
|
||||
]
|
||||
|
||||
const reserveCommitMock = mock(() => 1)
|
||||
@@ -91,8 +86,8 @@ describe("createCallOmoAgent edge cases", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given agent names with extra whitespace from SDK", () => {
|
||||
test("#then whitespace-padded names are trimmed and matched correctly", async () => {
|
||||
describe("#given a non-allowed agent appears in runtime agent results", () => {
|
||||
test("#then the runtime agent is still rejected", async () => {
|
||||
const agents = [
|
||||
...DEFAULT_AGENTS,
|
||||
{ name: " bug-fixer ", mode: "subagent" },
|
||||
@@ -123,11 +118,12 @@ describe("createCallOmoAgent edge cases", () => {
|
||||
toolCtx,
|
||||
)
|
||||
|
||||
expect(result).not.toContain("Invalid agent type")
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).toContain("Only explore, librarian are allowed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given an agent exists in both ALLOWED_AGENTS and dynamic results", () => {
|
||||
describe("#given an agent exists in both ALLOWED_AGENTS and runtime results", () => {
|
||||
test("#then the agent is callable without conflict", async () => {
|
||||
const agents = [
|
||||
...DEFAULT_AGENTS,
|
||||
@@ -163,8 +159,8 @@ describe("createCallOmoAgent edge cases", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given a disabled custom agent from dynamic resolution", () => {
|
||||
test("#then disabled_agents check takes precedence over dynamic availability", async () => {
|
||||
describe("#given a disabled custom agent appears in runtime results", () => {
|
||||
test("#then restricted agent validation takes precedence over dynamic availability", async () => {
|
||||
const agents = [
|
||||
...DEFAULT_AGENTS,
|
||||
{ name: "bug-fixer", mode: "subagent" },
|
||||
@@ -189,7 +185,8 @@ describe("createCallOmoAgent edge cases", () => {
|
||||
toolCtx,
|
||||
)
|
||||
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -35,11 +35,6 @@ function createFailingMockCtx(error: Error = new Error("API unavailable")): Plug
|
||||
const DEFAULT_AGENTS = [
|
||||
{ name: "explore", mode: "subagent" },
|
||||
{ name: "librarian", mode: "subagent" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "hephaestus", mode: "subagent" },
|
||||
{ name: "metis", mode: "subagent" },
|
||||
{ name: "momus", mode: "subagent" },
|
||||
{ name: "multimodal-looker", mode: "subagent" },
|
||||
]
|
||||
|
||||
const assertCanSpawnMock = mock(() => Promise.resolve(undefined))
|
||||
@@ -135,7 +130,7 @@ describe("createCallOmoAgent", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("dynamic custom agent resolution", () => {
|
||||
describe("restricted agent validation", () => {
|
||||
test("should reject missing subagent_type without throwing", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
@@ -149,22 +144,22 @@ describe("createCallOmoAgent", () => {
|
||||
expect(result).toContain("subagent_type is required")
|
||||
})
|
||||
|
||||
test("should accept a custom agent returned by client.app.agents()", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "bug-fixer", mode: "subagent" }]
|
||||
test("should reject general even when returned by client.app.agents()", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "general", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "bug-fixer", run_in_background: true },
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "general", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).not.toContain("Invalid agent type")
|
||||
expect(result).not.toContain("not found")
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).toContain("Only explore, librarian are allowed")
|
||||
})
|
||||
|
||||
test("should reject a custom agent NOT returned by client.app.agents()", async () => {
|
||||
test("should reject unknown non-allowed agents", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
@@ -177,14 +172,13 @@ describe("createCallOmoAgent", () => {
|
||||
expect(result).toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should perform case-insensitive matching for custom agents", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "Bug-Fixer", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
test("should perform case-insensitive matching for allowed agents", async () => {
|
||||
const mockCtx = createMockCtx(DEFAULT_AGENTS)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
|
||||
const executeFunc = toolDef.execute as Function
|
||||
|
||||
const result = await executeFunc(
|
||||
{ description: "Test", prompt: "Fix bug", subagent_type: "bug-fixer", run_in_background: true },
|
||||
{ description: "Test", prompt: "Explore", subagent_type: "EXPLORE", run_in_background: true },
|
||||
toolCtx
|
||||
)
|
||||
|
||||
@@ -234,7 +228,7 @@ describe("createCallOmoAgent", () => {
|
||||
expect(result).toContain("Invalid agent type")
|
||||
})
|
||||
|
||||
test("should still apply disabled_agents check to dynamically resolved custom agents", async () => {
|
||||
test("should reject non-allowed agents before disabled_agents can make them appear callable", async () => {
|
||||
const agents = [...DEFAULT_AGENTS, { name: "bug-fixer", mode: "subagent" }]
|
||||
const mockCtx = createMockCtx(agents)
|
||||
const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, ["bug-fixer"])
|
||||
@@ -245,7 +239,8 @@ describe("createCallOmoAgent", () => {
|
||||
toolCtx
|
||||
)
|
||||
|
||||
expect(result).toContain("disabled via disabled_agents")
|
||||
expect(result).toContain("Invalid agent type")
|
||||
expect(result).not.toContain("disabled via disabled_agents")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export function createCallOmoAgent(
|
||||
subagent_type: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"The agent to invoke. Supports built-in agents and any custom agents registered at runtime.",
|
||||
"The agent to invoke. Only explore and librarian are allowed.",
|
||||
),
|
||||
run_in_background: tool.schema
|
||||
.boolean()
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import type { OpencodeClient } from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { isRecord } from "../../shared/record-type-guard"
|
||||
import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache"
|
||||
|
||||
type ModelListClient = OpencodeClient & {
|
||||
model: { list: () => Promise<unknown> }
|
||||
}
|
||||
|
||||
function hasModelList(client: OpencodeClient): client is ModelListClient {
|
||||
return "model" in client && isRecord(client.model) && typeof client.model.list === "function"
|
||||
}
|
||||
|
||||
function isModelRow(value: unknown): value is { provider: string; id: string } {
|
||||
return isRecord(value) && typeof value.provider === "string" && typeof value.id === "string"
|
||||
}
|
||||
|
||||
function extractModelRows(result: unknown): Array<{ provider: string; id: string }> {
|
||||
const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.data) ? result.data : []
|
||||
return rows.filter(isModelRow)
|
||||
}
|
||||
|
||||
function addFromProviderModels(
|
||||
out: Set<string>,
|
||||
providerID: string,
|
||||
@@ -35,24 +53,17 @@ export async function getAvailableModelsForDelegateTask(client: OpencodeClient):
|
||||
return new Set()
|
||||
}
|
||||
|
||||
const modelList = (client as unknown as { model?: { list?: () => Promise<unknown> } })
|
||||
?.model
|
||||
?.list
|
||||
|
||||
if (!modelList) {
|
||||
if (!hasModelList(client)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await modelList()
|
||||
const rows = Array.isArray(result)
|
||||
? result
|
||||
: ((result as { data?: unknown }).data as Array<{ provider?: string; id?: string }> | undefined) ?? []
|
||||
const result = await client.model.list()
|
||||
const rows = extractModelRows(result)
|
||||
|
||||
const connected = new Set(connectedProviders)
|
||||
const out = new Set<string>()
|
||||
for (const row of rows) {
|
||||
if (!row?.provider || !row?.id) continue
|
||||
if (!connected.has(row.provider)) continue
|
||||
out.add(`${row.provider}/${row.id}`)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("
|
||||
import { resolveCategoryExecution } from "./category-resolver"
|
||||
import type { ExecutorContext } from "./executor-types"
|
||||
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("resolveCategoryExecution", () => {
|
||||
let connectedProvidersSpy: ReturnType<typeof spyOn> | undefined
|
||||
@@ -26,8 +27,8 @@ describe("resolveCategoryExecution", () => {
|
||||
})
|
||||
|
||||
const createMockExecutorContext = (): ExecutorContext => ({
|
||||
client: {} as any,
|
||||
manager: {} as any,
|
||||
client: unsafeTestValue({}),
|
||||
manager: unsafeTestValue({}),
|
||||
directory: "/tmp/test",
|
||||
userCategories: {},
|
||||
sisyphusJuniorModel: undefined,
|
||||
|
||||
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
|
||||
|
||||
import { executeBackgroundTask } from "./executor"
|
||||
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("task tool metadata awaiting", () => {
|
||||
test("executeBackgroundTask awaits ctx.metadata before returning", async () => {
|
||||
@@ -28,7 +29,7 @@ describe("task tool metadata awaiting", () => {
|
||||
subagent_type: "explore",
|
||||
}
|
||||
|
||||
const executorCtx = {
|
||||
const executorCtx = unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "task_1",
|
||||
@@ -40,7 +41,7 @@ describe("task tool metadata awaiting", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any
|
||||
})
|
||||
|
||||
const parentContext = {
|
||||
sessionID: "ses_parent",
|
||||
|
||||
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
|
||||
|
||||
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
import type { ParentContext } from "./executor-types"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" }
|
||||
const MODEL_WITH_VARIANT = { providerID: "google", modelID: "gemini-3.1-pro", variant: "high" }
|
||||
@@ -63,7 +64,7 @@ describe("metadata model unification", () => {
|
||||
load_skills: [], run_in_background: true, subagent_type: "explore",
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
await executeBackgroundTask(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_1", description: "test", agent: "explore",
|
||||
@@ -71,7 +72,7 @@ describe("metadata model unification", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "explore", MODEL, undefined)
|
||||
}), parentContext, "explore", MODEL, undefined)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -92,7 +93,7 @@ describe("metadata model unification", () => {
|
||||
}
|
||||
await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
@@ -109,7 +110,7 @@ describe("metadata model unification", () => {
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
}),
|
||||
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
@@ -126,14 +127,14 @@ describe("metadata model unification", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_2", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed", model: MODEL,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -153,7 +154,7 @@ describe("metadata model unification", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -162,7 +163,7 @@ describe("metadata model unification", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -206,7 +207,7 @@ describe("metadata model unification", () => {
|
||||
load_skills: [], run_in_background: true, subagent_type: "explore",
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
await executeBackgroundTask(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_1", description: "test", agent: "explore",
|
||||
@@ -214,7 +215,7 @@ describe("metadata model unification", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "explore", undefined, undefined)
|
||||
}), parentContext, "explore", undefined, undefined)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -236,7 +237,7 @@ describe("metadata model unification", () => {
|
||||
|
||||
await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
@@ -253,7 +254,7 @@ describe("metadata model unification", () => {
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
}),
|
||||
parentContext, "explore", undefined, undefined, "anthropic/claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
@@ -270,14 +271,14 @@ describe("metadata model unification", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_2", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed",
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -297,14 +298,14 @@ describe("metadata model unification", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [] }),
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -381,7 +382,7 @@ describe("metadata model unification", () => {
|
||||
category: "visual-engineering", load_skills: [], run_in_background: true, subagent_type: "explore",
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
await executeBackgroundTask(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_variant", description: "test", agent: "explore",
|
||||
@@ -389,7 +390,7 @@ describe("metadata model unification", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "explore", MODEL_WITH_VARIANT, undefined)
|
||||
}), parentContext, "explore", MODEL_WITH_VARIANT, undefined)
|
||||
|
||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -411,7 +412,7 @@ describe("metadata model unification", () => {
|
||||
|
||||
await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
@@ -428,7 +429,7 @@ describe("metadata model unification", () => {
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
}),
|
||||
parentContext, "explore", MODEL_WITH_VARIANT, undefined, "google/gemini-3.1-pro high",
|
||||
)
|
||||
|
||||
@@ -445,14 +446,14 @@ describe("metadata model unification", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resumed_variant",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_resume_variant", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed_variant", model: MODEL_WITH_VARIANT,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -472,7 +473,7 @@ describe("metadata model unification", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -481,7 +482,7 @@ describe("metadata model unification", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
|
||||
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
|
||||
|
||||
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
import type { ParentContext } from "./executor-types"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" }
|
||||
|
||||
@@ -64,7 +65,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
load_skills: [], run_in_background: true, subagent_type: "explore",
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
await executeBackgroundTask(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_abc123", description: "test", agent: "explore",
|
||||
@@ -72,7 +73,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "explore", MODEL, undefined)
|
||||
}), parentContext, "explore", MODEL, undefined)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -98,7 +99,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
|
||||
await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
@@ -115,7 +116,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
}),
|
||||
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
@@ -136,14 +137,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_resumed_y", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -160,14 +161,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_resumed_y", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed_x", model: MODEL, category: "deep",
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -187,14 +188,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
task_id: "ses_resumed_x",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_resumed_y", description: "continue", agent: "explore",
|
||||
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -216,7 +217,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -225,7 +226,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -246,7 +247,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -255,7 +256,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -275,7 +276,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -284,7 +285,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -309,7 +310,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -318,7 +319,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, deps)
|
||||
}), parentContext, deps)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -368,7 +369,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
run_in_background: true,
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
await executeBackgroundTask(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_abc123", description: "test", agent: "Sisyphus-Junior",
|
||||
@@ -376,7 +377,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "Sisyphus-Junior", MODEL, undefined)
|
||||
}), parentContext, "Sisyphus-Junior", MODEL, undefined)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -402,7 +403,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
|
||||
await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
unsafeTestValue({
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
@@ -419,7 +420,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
}),
|
||||
parentContext, "Sisyphus-Junior", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
@@ -438,14 +439,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
load_skills: [], run_in_background: true, task_id: "ses_resume_title",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
await executeBackgroundContinuation(args, ctx, unsafeTestValue({
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_resume_title", description: "continue work", agent: "explore",
|
||||
status: "running", sessionId: "ses_resume_title", model: MODEL,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
}), parentContext)
|
||||
|
||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
@@ -460,7 +461,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
load_skills: [], run_in_background: false, task_id: "ses_sync_title",
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
@@ -469,7 +470,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, parentContext, {
|
||||
}), parentContext, {
|
||||
pollSyncSession: async () => null,
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
})
|
||||
@@ -500,8 +501,8 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const bgOutput = createBackgroundOutput(manager as any, client as any)
|
||||
await bgOutput.execute({ task_id: "bg_output_xyz" } as any, ctx as any)
|
||||
const bgOutput = createBackgroundOutput(unsafeTestValue(manager), unsafeTestValue(client))
|
||||
await bgOutput.execute(unsafeTestValue({ task_id: "bg_output_xyz" }), unsafeTestValue(ctx))
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.backgroundTaskId)
|
||||
expect(meta).toBeDefined()
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, mock, test } from "bun:test"
|
||||
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import type { OpencodeClient } from "./types"
|
||||
import { sendSyncPrompt } from "./sync-prompt-sender"
|
||||
import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
|
||||
type PromptRetryClient = Parameters<typeof promptWithModelSuggestionRetry>[0]
|
||||
type PromptRetryArgs = Parameters<typeof promptWithModelSuggestionRetry>[1]
|
||||
type PromptSyncRetryClient = Parameters<typeof promptSyncWithModelSuggestionRetry>[0]
|
||||
type PromptSyncRetryArgs = Parameters<typeof promptSyncWithModelSuggestionRetry>[1]
|
||||
|
||||
describe("sendSyncPrompt session routing", () => {
|
||||
test("#given a sync child session directory #when sending the prompt #then promptAsync uses that OpenCode directory route", async () => {
|
||||
// given
|
||||
const promptCalls: PromptRetryArgs[] = []
|
||||
const promptWithRetry = mock(async (_client: PromptRetryClient, input: PromptRetryArgs) => {
|
||||
promptCalls.push(input)
|
||||
})
|
||||
|
||||
// when
|
||||
await sendSyncPrompt(
|
||||
unsafeTestValue<OpencodeClient>({ session: {} }),
|
||||
{
|
||||
sessionID: "ses_child",
|
||||
agentToUse: "sisyphus-junior",
|
||||
args: {
|
||||
description: "test task",
|
||||
prompt: "test prompt",
|
||||
run_in_background: false,
|
||||
load_skills: [],
|
||||
},
|
||||
systemContent: undefined,
|
||||
categoryModel: undefined,
|
||||
directory: "/parent/project",
|
||||
toastManager: null,
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
promptSyncWithModelSuggestionRetry: mock(async () => {}),
|
||||
},
|
||||
)
|
||||
|
||||
// then
|
||||
expect(promptCalls).toHaveLength(1)
|
||||
expect(promptCalls[0]?.query).toEqual({ directory: "/parent/project" })
|
||||
})
|
||||
|
||||
test("#given oracle falls back to promptSync #when async prompt returns unexpected EOF #then the sync retry keeps the same directory route", async () => {
|
||||
// given
|
||||
const promptSyncCalls: PromptSyncRetryArgs[] = []
|
||||
const promptWithRetry = mock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
const promptSyncWithRetry = mock(async (_client: PromptSyncRetryClient, input: PromptSyncRetryArgs) => {
|
||||
promptSyncCalls.push(input)
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await sendSyncPrompt(
|
||||
unsafeTestValue<OpencodeClient>({ session: {} }),
|
||||
{
|
||||
sessionID: "ses_child",
|
||||
agentToUse: "oracle",
|
||||
args: {
|
||||
description: "test task",
|
||||
prompt: "test prompt",
|
||||
run_in_background: false,
|
||||
load_skills: [],
|
||||
},
|
||||
systemContent: undefined,
|
||||
categoryModel: undefined,
|
||||
directory: "/parent/project",
|
||||
toastManager: null,
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
promptSyncWithModelSuggestionRetry: promptSyncWithRetry,
|
||||
},
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBeNull()
|
||||
expect(promptSyncCalls).toHaveLength(1)
|
||||
expect(promptSyncCalls[0]?.query).toEqual({ directory: "/parent/project" })
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
import { routePromptRetry, routePromptSyncRetry } from "../../shared/session-route"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names"
|
||||
@@ -60,6 +61,7 @@ export async function sendSyncPrompt(
|
||||
promptText?: string
|
||||
systemContent: string | undefined
|
||||
categoryModel: DelegatedModelConfig | undefined
|
||||
directory: string
|
||||
toastManager: { removeTask: (id: string) => void } | null | undefined
|
||||
taskId: string | undefined
|
||||
sisyphusAgentConfig?: SisyphusAgentConfig
|
||||
@@ -100,11 +102,12 @@ export async function sendSyncPrompt(
|
||||
}
|
||||
|
||||
try {
|
||||
await deps.promptWithModelSuggestionRetry(client, promptArgs)
|
||||
const routedPromptArgs = routePromptRetry(promptArgs, input.directory)
|
||||
await deps.promptWithModelSuggestionRetry(client, routedPromptArgs)
|
||||
} catch (promptError) {
|
||||
if (isOracleAgent(input.agentToUse) && isUnexpectedEofError(promptError)) {
|
||||
try {
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, promptArgs)
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, routePromptSyncRetry(promptArgs, input.directory))
|
||||
return null
|
||||
} catch (oracleRetryError) {
|
||||
promptError = oracleRetryError
|
||||
|
||||
@@ -188,6 +188,7 @@ export async function executeSyncTask(
|
||||
args,
|
||||
promptText: delegatedPromptText,
|
||||
systemContent,
|
||||
directory: createSessionResult.parentDirectory,
|
||||
toastManager,
|
||||
taskId,
|
||||
sisyphusAgentConfig: executorCtx.sisyphusAgentConfig,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
const { describe, expect, test } = require("bun:test")
|
||||
|
||||
function requireFresh<T>(modulePath: string): T {
|
||||
@@ -18,14 +19,14 @@ function createDelegateTask(...args: Parameters<typeof import("./tools").createD
|
||||
const toolDefinition = createDelegateTask({ manager: {} as never, client: {} as never, directory: "/tmp/test" })
|
||||
|
||||
//#when
|
||||
const categorySchema = toolDefinition.args.category as unknown as {
|
||||
const categorySchema = unsafeTestValue<{
|
||||
def: {
|
||||
type: string
|
||||
innerType: {
|
||||
def: { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}>(toolDefinition.args.category)
|
||||
|
||||
//#then
|
||||
expect(categorySchema.def.type).toBe("optional")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { executeUnstableAgentTask } from "./unstable-agent-task"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("executeUnstableAgentTask session permission", () => {
|
||||
test("passes question-deny session permission into background launch", async () => {
|
||||
@@ -33,7 +34,7 @@ describe("executeUnstableAgentTask session permission", () => {
|
||||
metadata: () => {},
|
||||
abort: new AbortController().signal,
|
||||
} satisfies Parameters<typeof executeUnstableAgentTask>[1]
|
||||
const executorContext = {
|
||||
const executorContext = unsafeTestValue<Parameters<typeof executeUnstableAgentTask>[2]>({
|
||||
manager: mockManager,
|
||||
client: {
|
||||
session: {
|
||||
@@ -41,7 +42,7 @@ describe("executeUnstableAgentTask session permission", () => {
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof executeUnstableAgentTask>[2]
|
||||
})
|
||||
const parentContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "msg_parent",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { HASHLINE_DICT } from "./constants"
|
||||
import { createHashlineChunkFormatter } from "./hashline-chunk-formatter"
|
||||
import { bunHashXxh32 } from "../../shared/bun-hash-shim"
|
||||
|
||||
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u
|
||||
|
||||
function computeNormalizedLineHash(lineNumber: number, normalizedContent: string): string {
|
||||
const stripped = normalizedContent
|
||||
const seed = RE_SIGNIFICANT.test(stripped) ? 0 : lineNumber
|
||||
const hash = Bun.hash.xxHash32(stripped, seed)
|
||||
const hash = bunHashXxh32(stripped, seed)
|
||||
const index = hash % 256
|
||||
return HASHLINE_DICT[index]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { publishToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { bunFile, bunWrite } from "../../shared/bun-file-shim"
|
||||
import { applyHashlineEditsWithReport } from "./edit-operations"
|
||||
import { countLineDiffs, generateUnifiedDiff } from "./diff-utils"
|
||||
import { canonicalizeFileText, restoreFileText } from "./file-text-canonicalization"
|
||||
@@ -94,7 +95,7 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T
|
||||
|
||||
const edits = deleteMode ? [] : normalizeHashlineEdits(args.edits)
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
const file = bunFile(filePath)
|
||||
const exists = await file.exists()
|
||||
if (!exists && !deleteMode && !canCreateFromMissingFile(edits)) {
|
||||
return `Error: File not found: ${filePath}`
|
||||
@@ -102,7 +103,7 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T
|
||||
|
||||
if (deleteMode) {
|
||||
if (!exists) return `Error: File not found: ${filePath}`
|
||||
await Bun.file(filePath).delete()
|
||||
await bunFile(filePath).delete()
|
||||
return `Successfully deleted ${filePath}`
|
||||
}
|
||||
|
||||
@@ -122,11 +123,11 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T
|
||||
|
||||
const writeContent = restoreFileText(canonicalNewContent, oldEnvelope)
|
||||
|
||||
await Bun.write(filePath, writeContent)
|
||||
await bunWrite(filePath, writeContent)
|
||||
|
||||
if (pluginCtx?.client) {
|
||||
await runFormattersForFile(pluginCtx.client as FormatterClient, context.directory, filePath)
|
||||
const formattedContent = Buffer.from(await Bun.file(filePath).arrayBuffer()).toString("utf8")
|
||||
const formattedContent = Buffer.from(await bunFile(filePath).arrayBuffer()).toString("utf8")
|
||||
if (formattedContent !== writeContent) {
|
||||
const formattedEnvelope = canonicalizeFileText(formattedContent)
|
||||
const formattedMeta = buildSuccessMeta(
|
||||
@@ -138,8 +139,8 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T
|
||||
)
|
||||
await publishToolMetadata(metadataContext, formattedMeta)
|
||||
if (rename && rename !== filePath) {
|
||||
await Bun.write(rename, formattedContent)
|
||||
await Bun.file(filePath).delete()
|
||||
await bunWrite(rename, formattedContent)
|
||||
await bunFile(filePath).delete()
|
||||
return `Moved ${filePath} to ${rename}`
|
||||
}
|
||||
return `Updated ${filePath}`
|
||||
@@ -147,8 +148,8 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T
|
||||
}
|
||||
|
||||
if (rename && rename !== filePath) {
|
||||
await Bun.write(rename, writeContent)
|
||||
await Bun.file(filePath).delete()
|
||||
await bunWrite(rename, writeContent)
|
||||
await bunFile(filePath).delete()
|
||||
}
|
||||
|
||||
const effectivePath = rename && rename !== filePath ? rename : filePath
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("normalizeHashlineEdits", () => {
|
||||
it("maps replace with pos to replace", () => {
|
||||
@@ -51,9 +52,9 @@ describe("normalizeHashlineEdits", () => {
|
||||
|
||||
it("rejects legacy payload without op", () => {
|
||||
//#given
|
||||
const input = [{ type: "set_line", line: "2#VK", text: "updated" }] as unknown as Parameters<
|
||||
const input = unsafeTestValue<Parameters<
|
||||
typeof normalizeHashlineEdits
|
||||
>[0]
|
||||
>[0]>([{ type: "set_line", line: "2#VK", text: "updated" }])
|
||||
|
||||
//#when / #then
|
||||
expect(() => normalizeHashlineEdits(input)).toThrow(/legacy format was removed/i)
|
||||
|
||||
@@ -6,16 +6,17 @@ import { canonicalizeFileText } from "./file-text-canonicalization"
|
||||
import * as fs from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
function createMockContext(): ToolContext {
|
||||
return {
|
||||
return unsafeTestValue<ToolContext>({
|
||||
sessionID: "test",
|
||||
messageID: "test",
|
||||
agent: "test",
|
||||
abort: new AbortController().signal,
|
||||
metadata: mock(() => {}),
|
||||
ask: async () => {},
|
||||
} as unknown as ToolContext
|
||||
})
|
||||
}
|
||||
|
||||
describe("createHashlineEditTool", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadat
|
||||
import { setVisionCapableModelsCache, clearVisionCapableModelsCache } from "../../shared/vision-capable-models-cache"
|
||||
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
|
||||
import * as modelAvailability from "../../shared/model-availability"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
function createPluginInput(agentData: Array<Record<string, unknown>>): PluginInput {
|
||||
const client = {} as PluginInput["client"]
|
||||
@@ -32,8 +33,8 @@ describe("resolveMultimodalLookerAgentMetadata", () => {
|
||||
|
||||
afterEach(() => {
|
||||
clearVisionCapableModelsCache()
|
||||
;(modelAvailability.fetchAvailableModels as unknown as { mockRestore?: () => void }).mockRestore?.()
|
||||
;(connectedProvidersCache.readConnectedProvidersCache as unknown as { mockRestore?: () => void }).mockRestore?.()
|
||||
;(unsafeTestValue<{ mockRestore?: () => void }>(modelAvailability.fetchAvailableModels)).mockRestore?.()
|
||||
;(unsafeTestValue<{ mockRestore?: () => void }>(connectedProvidersCache.readConnectedProvidersCache)).mockRestore?.()
|
||||
})
|
||||
|
||||
test("returns configured multimodal-looker model when it already matches a vision-capable override", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test, mock } from "bun:test"
|
||||
import { pollSessionUntilIdle } from "./session-poller"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
type SessionStatusResult = {
|
||||
data?: Record<string, { type: string; attempt?: number; message?: string; next?: number }>
|
||||
@@ -30,7 +31,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
{ data: { ses_test: { type: "idle" } } },
|
||||
])
|
||||
|
||||
await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
|
||||
expect(client.session.status).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
@@ -43,7 +44,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
{ data: {} },
|
||||
])
|
||||
|
||||
await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
|
||||
expect(client.session.status).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -57,7 +58,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
])
|
||||
|
||||
await expect(
|
||||
pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 50 })
|
||||
pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 50 })
|
||||
).rejects.toThrow("timed out")
|
||||
})
|
||||
|
||||
@@ -69,7 +70,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
{ error: new Error("API error") },
|
||||
])
|
||||
|
||||
await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
|
||||
expect(client.session.status).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -85,7 +86,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
{ data: {} },
|
||||
])
|
||||
|
||||
await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
|
||||
|
||||
expect(client.session.status).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
@@ -98,7 +99,7 @@ describe("pollSessionUntilIdle", () => {
|
||||
{ data: {} },
|
||||
])
|
||||
|
||||
await pollSessionUntilIdle(client as any, "ses_test")
|
||||
await pollSessionUntilIdle(unsafeTestValue(client), "ses_test")
|
||||
|
||||
expect(client.session.status).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test, mock } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { clearVisionCapableModelsCache, setVisionCapableModelsCache } from "../../shared/vision-capable-models-cache"
|
||||
import { normalizeArgs, validateArgs, createLookAt } from "./tools"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("look-at tool", () => {
|
||||
afterEach(() => {
|
||||
@@ -14,7 +15,7 @@ describe("look-at tool", () => {
|
||||
// then should normalize to file_path
|
||||
test("normalizes path to file_path for LLM compatibility", () => {
|
||||
const args = { path: "/some/file.png", goal: "analyze" }
|
||||
const normalized = normalizeArgs(args as any)
|
||||
const normalized = normalizeArgs(unsafeTestValue(args))
|
||||
expect(normalized.file_path).toBe("/some/file.png")
|
||||
expect(normalized.goal).toBe("analyze")
|
||||
})
|
||||
@@ -33,7 +34,7 @@ describe("look-at tool", () => {
|
||||
// then prefer file_path
|
||||
test("prefers file_path over path when both provided", () => {
|
||||
const args = { file_path: "/preferred.png", path: "/fallback.png", goal: "test" }
|
||||
const normalized = normalizeArgs(args as any)
|
||||
const normalized = normalizeArgs(unsafeTestValue(args))
|
||||
expect(normalized.file_path).toBe("/preferred.png")
|
||||
})
|
||||
|
||||
@@ -42,7 +43,7 @@ describe("look-at tool", () => {
|
||||
// then preserve image_data in normalized args
|
||||
test("preserves image_data when provided", () => {
|
||||
const args = { image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "analyze" }
|
||||
const normalized = normalizeArgs(args as any)
|
||||
const normalized = normalizeArgs(unsafeTestValue(args))
|
||||
expect(normalized.image_data).toBe("data:image/png;base64,iVBORw0KGgo=")
|
||||
expect(normalized.file_path).toBeUndefined()
|
||||
})
|
||||
@@ -69,7 +70,7 @@ describe("look-at tool", () => {
|
||||
// when validated
|
||||
// then clear error message
|
||||
test("returns error when neither file_path nor image_data provided", () => {
|
||||
const args = { goal: "analyze" } as any
|
||||
const args = unsafeTestValue({ goal: "analyze" })
|
||||
const error = validateArgs(args)
|
||||
expect(error).toContain("file_path")
|
||||
expect(error).toContain("image_data")
|
||||
@@ -88,7 +89,7 @@ describe("look-at tool", () => {
|
||||
// when validated
|
||||
// then clear error message
|
||||
test("returns error when goal is missing", () => {
|
||||
const args = { file_path: "/some/path.png" } as any
|
||||
const args = unsafeTestValue({ file_path: "/some/path.png" })
|
||||
const error = validateArgs(args)
|
||||
expect(error).toContain("goal")
|
||||
expect(error).toContain("required")
|
||||
@@ -156,10 +157,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -193,10 +194,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -230,10 +231,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -291,10 +292,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -346,10 +347,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -395,10 +396,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -437,10 +438,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -486,10 +487,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
@@ -515,10 +516,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
@@ -539,10 +540,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
@@ -579,10 +580,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -632,10 +633,10 @@ describe("look-at tool", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionID: "parent-session",
|
||||
@@ -701,10 +702,10 @@ describe("look-at tool", () => {
|
||||
test("instructs agent to analyze attached file when Read is disabled (file_path mode)", async () => {
|
||||
const { mockClient, captured } = captureLastPromptBody()
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "describe contents" },
|
||||
@@ -726,10 +727,10 @@ describe("look-at tool", () => {
|
||||
test("instructs agent to analyze attached image when image_data is provided", async () => {
|
||||
const { mockClient, captured } = captureLastPromptBody()
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
await tool.execute(
|
||||
{ image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "describe image" },
|
||||
@@ -751,10 +752,10 @@ describe("look-at tool", () => {
|
||||
test("explicitly warns the agent not to attempt Read when Read is disabled", async () => {
|
||||
const { mockClient, captured } = captureLastPromptBody()
|
||||
|
||||
const tool = createLookAt({
|
||||
const tool = createLookAt(unsafeTestValue({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
}))
|
||||
|
||||
await tool.execute(
|
||||
{ file_path: "/test/file.pdf", goal: "extract text" },
|
||||
|
||||
@@ -16,6 +16,7 @@ afterAll(() => { mock.restore() })
|
||||
|
||||
import { LSPClient, lspManager, validateCwd } from "./client"
|
||||
import type { ResolvedServer } from "./types"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
describe("LSPClient", () => {
|
||||
beforeEach(async () => {
|
||||
@@ -36,7 +37,7 @@ describe("LSPClient", () => {
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
|
||||
fn()
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>
|
||||
return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
|
||||
}) as typeof setTimeout
|
||||
|
||||
const server: ResolvedServer = {
|
||||
@@ -50,7 +51,7 @@ describe("LSPClient", () => {
|
||||
|
||||
// Stub protocol output: we only want to assert notifications.
|
||||
const sendNotificationSpy = spyOn(
|
||||
client as unknown as { sendNotification: (m: string, p?: unknown) => void },
|
||||
unsafeTestValue<{ sendNotification: (m: string, p?: unknown) => void }>(client),
|
||||
"sendNotification"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn as bunSpawn } from "../../shared/bun-spawn-shim"
|
||||
import { spawn as bunSpawn, type SpawnedProcess } from "../../shared/bun-spawn-shim"
|
||||
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
||||
import { existsSync, statSync } from "fs"
|
||||
import { log } from "../../shared/logger"
|
||||
@@ -127,6 +127,30 @@ function wrapNodeProcess(proc: ChildProcess): UnifiedProcess {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function wrapBunProcess(proc: SpawnedProcess): UnifiedProcess {
|
||||
return {
|
||||
stdin: {
|
||||
write(chunk: Uint8Array | string) {
|
||||
proc.stdin.write(chunk)
|
||||
},
|
||||
},
|
||||
stdout: {
|
||||
getReader: () => proc.stdout.getReader(),
|
||||
},
|
||||
stderr: {
|
||||
getReader: () => proc.stderr.getReader(),
|
||||
},
|
||||
get exitCode() {
|
||||
return proc.exitCode
|
||||
},
|
||||
exited: proc.exited,
|
||||
kill(signal?: string) {
|
||||
proc.kill(signal === "SIGKILL" ? "SIGKILL" : undefined)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnProcess(
|
||||
command: string[],
|
||||
options: { cwd: string; env: Record<string, string | undefined> }
|
||||
@@ -154,5 +178,5 @@ export function spawnProcess(
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
})
|
||||
return proc as unknown as UnifiedProcess
|
||||
return wrapBunProcess(proc)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node:
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
|
||||
const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`)
|
||||
const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message")
|
||||
@@ -448,7 +449,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
|
||||
|
||||
// Re-import to get fresh module with mocked isSqliteBackend
|
||||
const { setStorageClient, getMainSessions } = await import("./storage")
|
||||
setStorageClient(mockClient as unknown as Parameters<typeof setStorageClient>[0])
|
||||
setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
|
||||
|
||||
// when
|
||||
const sessions = await getMainSessions({ directory: "/test" })
|
||||
@@ -473,7 +474,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
|
||||
}))
|
||||
|
||||
const { setStorageClient, getAllSessions } = await import("./storage")
|
||||
setStorageClient(mockClient as unknown as Parameters<typeof setStorageClient>[0])
|
||||
setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
|
||||
|
||||
// when
|
||||
const sessionIDs = await getAllSessions()
|
||||
@@ -503,7 +504,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
|
||||
}))
|
||||
|
||||
const { setStorageClient, readSessionMessages } = await import("./storage")
|
||||
setStorageClient(mockClient as unknown as Parameters<typeof setStorageClient>[0])
|
||||
setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
|
||||
|
||||
// when
|
||||
const messages = await readSessionMessages("ses_test")
|
||||
@@ -531,7 +532,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
|
||||
}))
|
||||
|
||||
const { setStorageClient, readSessionTodos } = await import("./storage")
|
||||
setStorageClient(mockClient as unknown as Parameters<typeof setStorageClient>[0])
|
||||
setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
|
||||
|
||||
// when
|
||||
const todos = await readSessionTodos("ses_test")
|
||||
@@ -555,7 +556,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
|
||||
}))
|
||||
|
||||
const { setStorageClient, readSessionMessages } = await import("./storage")
|
||||
setStorageClient(mockClient as unknown as Parameters<typeof setStorageClient>[0])
|
||||
setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
|
||||
|
||||
await expect(readSessionMessages("ses_test")).rejects.toThrow("API error")
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import { clearSkillCache } from "../../../features/opencode-skill-loader/skill-c
|
||||
import type { LoadedSkill } from "../../../features/opencode-skill-loader/types"
|
||||
import type { CommandInfo } from "../../slashcommand/types"
|
||||
import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { unsafeTestValue } from "../../../../test-support/unsafe-test-value"
|
||||
|
||||
const originalReadFileSync = fs.readFileSync.bind(fs)
|
||||
|
||||
@@ -205,7 +206,7 @@ describe("skill tool - agent restriction", () => {
|
||||
// given
|
||||
const loadedSkills = [createMockSkill("sisyphus-only-skill", { agent: "sisyphus" })]
|
||||
const tool = createSkillTool({ skills: loadedSkills })
|
||||
const contextWithoutAgent = { ...mockContext, agent: undefined as unknown as string }
|
||||
const contextWithoutAgent = { ...mockContext, agent: unsafeTestValue<string>(undefined) }
|
||||
|
||||
// when / #then
|
||||
return expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow(
|
||||
|
||||
Reference in New Issue
Block a user