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:
tw-yshuang
2026-05-12 22:49:57 +08:00
225 changed files with 4552 additions and 2103 deletions
@@ -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)
+15 -14
View File
@@ -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)
+30 -205
View File
@@ -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"])
})
})
})
+8 -52
View File
@@ -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)
})
})
+3 -8
View File
@@ -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)
+9 -1
View File
@@ -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")
})
})
+13 -18
View File
@@ -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")
})
})
+1 -1
View File
@@ -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()
+21 -10
View File
@@ -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
+1
View File
@@ -188,6 +188,7 @@ export async function executeSyncTask(
args,
promptText: delegatedPromptText,
systemContent,
directory: createSessionResult.parentDirectory,
toastManager,
taskId,
sisyphusAgentConfig: executorCtx.sisyphusAgentConfig,
+3 -2
View File
@@ -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",
+2 -1
View File
@@ -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)
+3 -2
View File
@@ -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 () => {
+7 -6
View File
@@ -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)
})
+36 -35
View File
@@ -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" },
+3 -2
View File
@@ -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"
)
+26 -2
View File
@@ -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)
}
+6 -5
View File
@@ -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(