diff --git a/packages/cli/src/acp/agent.ts b/packages/cli/src/acp/agent.ts index cba350b12d..954b36101c 100644 --- a/packages/cli/src/acp/agent.ts +++ b/packages/cli/src/acp/agent.ts @@ -97,6 +97,7 @@ function isACPError(error: unknown): error is ACPError.Error { error instanceof ACPError.InvalidModelError || error instanceof ACPError.InvalidEffortError || error instanceof ACPError.InvalidModeError || + error instanceof ACPError.AuthRequiredError || error instanceof ACPError.UnknownAuthMethodError || error instanceof ACPError.ServiceFailureError ) diff --git a/packages/cli/src/acp/config-option.ts b/packages/cli/src/acp/config-option.ts index 57346f536b..d1083259c3 100644 --- a/packages/cli/src/acp/config-option.ts +++ b/packages/cli/src/acp/config-option.ts @@ -33,7 +33,8 @@ export function buildConfigOptions(input: { currentModeId?: string }): SessionConfigOption[] { const variants = variantsForModel(input.providers, input.currentModel) - const effort = variants.length > 0 ? buildEffortSelectOption({ variants, currentVariant: input.currentVariant }) : undefined + const effort = + variants.length > 0 ? buildEffortSelectOption({ variants, currentVariant: input.currentVariant }) : undefined return [ buildModelSelectOption({ providers: input.providers, currentModel: input.currentModel }), ...(effort ? [effort] : []), @@ -122,8 +123,10 @@ export function formatVariantName(variant: string) { } function variantsForModel(providers: readonly ConfigOptionProvider[], model: ModelSelection["model"]) { - return providers.find((provider) => provider.id === model.providerID)?.models.find((item) => item.id === model.modelID) - ?.variants ?? [] + return ( + providers.find((provider) => provider.id === model.providerID)?.models.find((item) => item.id === model.modelID) + ?.variants ?? [] + ) } function selectVariant(variant: string | undefined, variants: readonly string[]) { diff --git a/packages/cli/src/acp/content.ts b/packages/cli/src/acp/content.ts index b3098c7011..aa6e3b431a 100644 --- a/packages/cli/src/acp/content.ts +++ b/packages/cli/src/acp/content.ts @@ -148,7 +148,12 @@ export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk function resourceLinkToPart(link: ResourceLink): PromptPart { try { if (link.uri.startsWith("file://")) { - return { type: "file", url: link.uri, filename: link.name || filenameFromUri(link.uri) || "file", mime: link.mimeType ?? "text/plain" } + return { + type: "file", + url: link.uri, + filename: link.name || filenameFromUri(link.uri) || "file", + mime: link.mimeType ?? "text/plain", + } } if (link.uri.startsWith("zed://")) { const pathname = new URL(link.uri).searchParams.get("path") @@ -164,9 +169,9 @@ function resourceLinkToPart(link: ResourceLink): PromptPart { return { type: "text", text: link.uri } } -function decodeDataUrl(url: string) { +function decodeDataUrl(url: string): { readonly mime: string; readonly base64: string } | undefined { const match = /^data:([^;]+);base64,(.*)$/.exec(url) - if (!match?.[1] || match[2] === undefined) return + if (!match?.[1] || match[2] === undefined) return undefined return { mime: match[1], base64: match[2] } } @@ -176,8 +181,8 @@ function audienceFlags(audience: readonly Role[] | null | undefined) { return {} } -function filenameFromUri(uri: string | undefined) { - if (!uri || uri.startsWith("data:")) return +function filenameFromUri(uri: string | undefined): string | undefined { + if (!uri || uri.startsWith("data:")) return undefined try { return path.basename(new URL(uri).pathname) || undefined } catch { diff --git a/packages/cli/src/acp/error.ts b/packages/cli/src/acp/error.ts index 0204a000ca..50dec3f4f6 100644 --- a/packages/cli/src/acp/error.ts +++ b/packages/cli/src/acp/error.ts @@ -23,6 +23,8 @@ export class InvalidModeError extends Schema.TaggedErrorClass( mode: Schema.String, }) {} +export class AuthRequiredError extends Schema.TaggedErrorClass()("ACPAuthRequiredError", {}) {} + export class UnknownAuthMethodError extends Schema.TaggedErrorClass()( "ACPUnknownAuthMethodError", { methodId: Schema.String }, @@ -40,10 +42,11 @@ export type Error = | InvalidModelError | InvalidEffortError | InvalidModeError + | AuthRequiredError | UnknownAuthMethodError | ServiceFailureError -export function toRequestError(error: Error) { +export function toRequestError(error: Error): RequestError { switch (error._tag) { case "ACPSessionNotFoundError": return RequestError.invalidParams({ sessionId: error.sessionId }, `session not found: ${error.sessionId}`) @@ -58,6 +61,8 @@ export function toRequestError(error: Error) { return RequestError.invalidParams({ effort: error.effort }, `effort not found: ${error.effort}`) case "ACPInvalidModeError": return RequestError.invalidParams({ mode: error.mode }, `mode not found: ${error.mode}`) + case "ACPAuthRequiredError": + return RequestError.authRequired({}, "provider authentication required") case "ACPUnknownAuthMethodError": return RequestError.invalidParams({ methodId: error.methodId }, `unknown auth method: ${error.methodId}`) case "ACPServiceFailureError": @@ -69,6 +74,8 @@ export function toRequestError(error: Error) { error.safeMessage, ) } + const exhaustive: never = error + return exhaustive } export function fromUnknown(error: unknown, service?: string) { diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 22b546c8b1..30f8573873 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -11,7 +11,15 @@ import type { SessionMessageInfo, } from "@opencode-ai/client/promise" import { partsToContentChunks, type ReplayPart } from "./content" -import { completedToolUpdate, errorToolUpdate, pendingToolCall, runningToolUpdate, type ToolContent, type ToolInput } from "./tool" +import { ACPError } from "./error" +import { + completedToolUpdate, + errorToolUpdate, + pendingToolCall, + runningToolUpdate, + type ToolContent, + type ToolInput, +} from "./tool" type Connection = Pick @@ -44,6 +52,7 @@ export async function streamTurn(input: { readonly sessionID: string readonly cwd: string readonly start: Start + readonly userMessageID?: string | null readonly submit: (signal: AbortSignal) => Promise readonly activate: (control: TurnControl) => void readonly deactivate: () => void @@ -58,6 +67,7 @@ export async function streamTurn(input: { let started = false let assistantMessageID: string | undefined let finish: SessionMessageAssistant["finish"] + let executionError: { readonly type: string; readonly message: string } | undefined const tools = new Map() const update = (value: Parameters[0]["update"]) => @@ -220,7 +230,10 @@ export async function streamTurn(input: { } if (event.type === "session.execution.succeeded") return "succeeded" as const if (event.type === "session.execution.interrupted") return "interrupted" as const - if (event.type === "session.execution.failed") return "failed" as const + if (event.type === "session.execution.failed") { + executionError = event.data.error + return "failed" as const + } } return "interrupted" as const } @@ -233,9 +246,22 @@ export async function streamTurn(input: { }) const terminal = await completed const assistant = assistantMessageID - ? await input.client.session.message({ sessionID: input.sessionID, messageID: assistantMessageID }).catch(() => undefined) + ? await input.client.session + .message({ sessionID: input.sessionID, messageID: assistantMessageID }) + .catch(() => undefined) : undefined - return response(assistant?.type === "assistant" ? assistant : undefined, terminal, control.cancelled, finish, input.start.id) + return response( + assistant?.type === "assistant" ? assistant : undefined, + executionError, + terminal, + control.cancelled, + finish, + input.userMessageID, + ) + } catch (error) { + streamController.abort() + await completed.catch(() => {}) + throw error } finally { input.deactivate() streamController.abort() @@ -261,7 +287,7 @@ export async function replayMessages( }) const files: ReplayPart[] = (message.files ?? []).map((file) => ({ type: "file", - url: `data:${file.mime};base64,${file.data}`, + url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`, filename: file.name, mime: file.mime, })) @@ -320,6 +346,21 @@ export async function replayMessages( }, }) } + if (part.state.status === "running") { + await connection.sessionUpdate({ + sessionId: sessionID, + update: { + sessionUpdate: "tool_call_update", + ...runningToolUpdate({ + toolCallId: part.id, + toolName: part.name, + state: { input: part.state.input }, + content: part.state.content, + cwd, + }), + }, + }) + } if (part.state.status === "error") { await connection.sessionUpdate({ sessionId: sessionID, @@ -343,7 +384,8 @@ export async function replayMessages( function matchesStart(event: EventSubscribeOutput, start: Start) { if (start.type === "input") return event.type === "session.input.promoted" && event.data.inputID === start.id - if (start.type === "compaction") return event.type === "session.compaction.admitted" && event.data.inputID === start.id + if (start.type === "compaction") + return event.type === "session.compaction.admitted" && event.data.inputID === start.id return event.type === "session.skill.activated" && event.id === start.id.replace(/^msg_/, "evt_") } @@ -359,11 +401,21 @@ function toolInput(tool: Extract>() const active = new Map() - const catalog = (cwd: string, refresh = false) => { - if (refresh) catalogs.delete(cwd) + const catalog = (cwd: string) => { const cached = catalogs.get(cwd) if (cached) return cached const loaded = loadCatalog(input.client, cwd).catch((error) => { @@ -174,7 +171,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti }, loadSession: async (params) => { const session = await getSession(input.client, params.sessionId) - const state = await attach(session, params.cwd, params.mcpServers, true) + const state = await attach(session, session.location.directory, params.mcpServers, true) return { configOptions: configOptions(state) } }, listSessions: async (params) => { @@ -185,20 +182,18 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti ...(params.cursor ? { cursor: params.cursor } : {}), }) return { - sessions: page.data.map( - (session): ACPSessionInfo => ({ - sessionId: session.id, - cwd: session.location.directory, - title: session.title, - updatedAt: new Date(session.time.updated).toISOString(), - }), - ), + sessions: page.data.map((session) => ({ + sessionId: session.id, + cwd: session.location.directory, + title: session.title, + updatedAt: new Date(session.time.updated).toISOString(), + })), ...(page.cursor.next ? { nextCursor: page.cursor.next } : {}), } }, resumeSession: async (params) => { const session = await getSession(input.client, params.sessionId) - const state = await attach(session, params.cwd, params.mcpServers ?? [], false) + const state = await attach(session, session.location.directory, params.mcpServers ?? [], false) return { configOptions: configOptions(state) } }, closeSession: async (params) => { @@ -213,15 +208,13 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti return {} }, forkSession: async (params) => { - await requireSession(params.sessionId) const forked = await input.client.session.fork({ sessionID: params.sessionId }) - const state = await attach(forked, params.cwd, params.mcpServers ?? [], true) + const state = await attach(forked, forked.location.directory, params.mcpServers ?? [], true) return { sessionId: state.id, configOptions: configOptions(state) } }, setSessionConfigOption: async (params) => { const state = await requireSession(params.sessionId) - if (typeof params.value !== "string") - throw new ACPError.InvalidConfigOptionError({ configId: params.configId }) + if (typeof params.value !== "string") throw new ACPError.InvalidConfigOptionError({ configId: params.configId }) if (params.configId === "model") { const selected = requireModel(state.catalog, params.value) state.model = selected @@ -257,15 +250,12 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti }, prompt: async (params) => { const state = await requireSession(params.sessionId) - state.catalog = await catalog(state.cwd, true) - const messageID = params.messageId ?? SessionMessage.ID.create() + const messageID = SessionMessage.ID.create() const parts = promptContentToParts(params.prompt) const visible = parts.filter((part) => part.type !== "text" || (!part.synthetic && !part.ignored)) const synthetic = parts.flatMap((part) => (part.type === "text" && part.synthetic ? [part.text] : [])) const text = visible.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n") - const files = visible.flatMap((part) => - part.type === "file" ? [{ uri: part.url, name: part.filename, description: part.mime }] : [], - ) + const files = visible.flatMap((part) => (part.type === "file" ? [{ uri: part.url, name: part.filename }] : [])) const slash = detectSlashCommand(text) const command = slash ? state.catalog.commands.find((item) => item.name === slash.name) : undefined const skill = slash ? state.catalog.skills.find((item) => item.name === slash.name) : undefined @@ -281,6 +271,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti sessionID: state.id, cwd: state.cwd, start, + userMessageID: params.messageId, activate: (control) => active.set(state.id, control), deactivate: () => active.delete(state.id), submit: async (signal) => { @@ -297,7 +288,14 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti if (skill) return input.client.session.skill({ sessionID: state.id, id: messageID, skill: skill.id }) if (command) return input.client.session.command( - { sessionID: state.id, id: messageID, command: command.name, arguments: slash?.args, delivery: "steer" }, + { + sessionID: state.id, + id: messageID, + command: command.name, + arguments: slash?.args, + files, + delivery: "steer", + }, { signal }, ) return input.client.session.prompt( @@ -306,7 +304,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti ) }, }) - await sendUsageUpdate(input.client, input.connection, state) + await sendUsageUpdate(input.client, input.connection, state).catch(() => {}) return response }, cancel: async (params) => { @@ -338,7 +336,11 @@ async function loadCatalog(client: OpenCodeClient, cwd: string): Promise variant.id === "default")?.id ?? defaultModel.variants[0]?.id, + }, modes: agents.map((agent) => ({ id: agent.id, name: agent.name, description: agent.description })), defaultModeID: defaultAgent.id, commands: commandResult.data, @@ -376,8 +378,9 @@ async function selectMode(client: OpenCodeClient, state: Attached, modeID: strin } async function getSession(client: OpenCodeClient, sessionID: string) { - return client.session.get({ sessionID }).catch(() => { - throw new ACPError.SessionNotFoundError({ sessionId: sessionID }) + return client.session.get({ sessionID }).catch((error) => { + if (isSessionNotFoundError(error)) throw new ACPError.SessionNotFoundError({ sessionId: sessionID }) + throw error }) } @@ -385,7 +388,9 @@ async function messages(client: OpenCodeClient, sessionID: string) { const result: SessionMessageInfo[] = [] let cursor: string | undefined do { - const page = await client.message.list({ sessionID, limit: 200, order: "asc", cursor }) + const page = cursor + ? await client.message.list({ sessionID, limit: 200, cursor }) + : await client.message.list({ sessionID, limit: 200, order: "asc" }) result.push(...page.data) cursor = page.cursor.next ?? undefined } while (cursor) @@ -473,11 +478,11 @@ async function sendUsageUpdate(client: OpenCodeClient, connection: Connection, s }) } -function detectSlashCommand(text: string) { +function detectSlashCommand(text: string): { readonly name: string; readonly args: string } | undefined { const value = text.trim() - if (!value.startsWith("/")) return + if (!value.startsWith("/")) return undefined const [name, ...rest] = value.slice(1).split(/\s+/) - if (!name) return + if (!name) return undefined return { name, args: rest.join(" ").trim() } } diff --git a/packages/cli/src/acp/tool.ts b/packages/cli/src/acp/tool.ts index e5cdd1a833..aca367b6fe 100644 --- a/packages/cli/src/acp/tool.ts +++ b/packages/cli/src/acp/tool.ts @@ -130,10 +130,7 @@ export function errorToolUpdate(input: { title: toolTitle(input.toolName, input.input, undefined), locations: toLocations(input.toolName, input.input, input.cwd), rawInput: rawInput(input.toolName, input.input, input.cwd), - content: [ - ...toolContent(input.content), - { type: "content", content: { type: "text", text: input.error } }, - ], + content: [...toolContent(input.content), { type: "content", content: { type: "text", text: input.error } }], rawOutput: { structured: input.structured, error: input.error }, } } @@ -173,7 +170,8 @@ function locationFrom(...values: unknown[]): ToolCallLocation[] { return Array.from( new Set( values.flatMap((value): string[] => { - if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string" && item.length > 0) + if (Array.isArray(value)) + return value.filter((item): item is string => typeof item === "string" && item.length > 0) const path = stringValue(value) return path ? [path] : [] }), diff --git a/packages/cli/test/acp/command.test.ts b/packages/cli/test/acp/command.test.ts index 6d64523ef2..1bf01db2d2 100644 --- a/packages/cli/test/acp/command.test.ts +++ b/packages/cli/test/acp/command.test.ts @@ -23,7 +23,7 @@ describe("acp command", () => { test("initializes over ndjson and exits on stdin eof", async () => { const child = spawn() const stderr = new Response(child.stderr).text() - child.stdin.write( + await child.stdin.write( new TextEncoder().encode( JSON.stringify({ jsonrpc: "2.0", @@ -37,7 +37,7 @@ describe("acp command", () => { }) + "\n", ), ) - child.stdin.flush() + await child.stdin.flush() const response = await readMessage(child.stdout) expect(response.id).toBe(1) expect(response.error).toBeUndefined() @@ -47,7 +47,7 @@ describe("acp command", () => { agentInfo: { name: "OpenCode" }, }) - child.stdin.end() + await child.stdin.end() const exitCode = await child.exited const errorOutput = await stderr if (exitCode !== 0) throw new Error(`ACP exited with ${exitCode}: ${errorOutput}`) @@ -82,10 +82,16 @@ async function readMessage(stream: ReadableStream) { const newline = output.indexOf("\n") if (newline === -1) continue reader.releaseLock() - return JSON.parse(output.slice(0, newline)) as Message + const message: unknown = JSON.parse(output.slice(0, newline)) + if (!isMessage(message)) throw new Error(`invalid ACP response: ${output.slice(0, newline)}`) + return message } } +function isMessage(value: unknown): value is Message { + return typeof value === "object" && value !== null +} + async function cli(args: string[]) { const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { cwd: path.join(import.meta.dir, "../.."), diff --git a/packages/cli/test/acp/config-option.test.ts b/packages/cli/test/acp/config-option.test.ts index 1abf353eca..7436e79269 100644 --- a/packages/cli/test/acp/config-option.test.ts +++ b/packages/cli/test/acp/config-option.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "bun:test" -import { buildConfigOptions, formatVariantName, parseModelSelection, type ConfigOptionProvider } from "../../src/acp/config-option" +import { + buildConfigOptions, + formatVariantName, + parseModelSelection, + type ConfigOptionProvider, +} from "../../src/acp/config-option" const providers: ConfigOptionProvider[] = [ { @@ -19,7 +24,10 @@ describe("acp config options", () => { providers, currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" }, currentVariant: "high", - modes: [{ id: "build", name: "Build" }, { id: "plan", name: "Plan" }], + modes: [ + { id: "build", name: "Build" }, + { id: "plan", name: "Plan" }, + ], currentModeId: "build", }) expect(options.map((option) => option.id)).toEqual(["model", "effort", "mode"]) diff --git a/packages/cli/test/acp/content.test.ts b/packages/cli/test/acp/content.test.ts index 1718529bf6..dc1a80d9cc 100644 --- a/packages/cli/test/acp/content.test.ts +++ b/packages/cli/test/acp/content.test.ts @@ -13,12 +13,12 @@ describe("acp content", () => { }) test("converts file and embedded resources", () => { - expect( - contentBlockToParts({ type: "resource_link", uri: "file:///tmp/notes.txt", name: "notes.txt" }), - ).toEqual([{ type: "file", url: "file:///tmp/notes.txt", filename: "notes.txt", mime: "text/plain" }]) - expect( - contentBlockToParts({ type: "resource", resource: { uri: "mcp://context", text: "hello" } }), - ).toEqual([{ type: "text", text: "[mcp://context]\nhello" }]) + expect(contentBlockToParts({ type: "resource_link", uri: "file:///tmp/notes.txt", name: "notes.txt" })).toEqual([ + { type: "file", url: "file:///tmp/notes.txt", filename: "notes.txt", mime: "text/plain" }, + ]) + expect(contentBlockToParts({ type: "resource", resource: { uri: "mcp://context", text: "hello" } })).toEqual([ + { type: "text", text: "[mcp://context]\nhello" }, + ]) }) test("replays files and data urls", () => { @@ -28,7 +28,9 @@ describe("acp content", () => { { type: "file", url: "data:text/plain;base64,aGVsbG8=", filename: "note.txt", mime: "text/plain" }, ]), ).toEqual([ - { content: { type: "resource_link", uri: "file:///tmp/readme.md", name: "readme.md", mimeType: "text/markdown" } }, + { + content: { type: "resource_link", uri: "file:///tmp/readme.md", name: "readme.md", mimeType: "text/markdown" }, + }, { content: { type: "resource", diff --git a/packages/cli/test/acp/error.test.ts b/packages/cli/test/acp/error.test.ts new file mode 100644 index 0000000000..3dc6b0ef96 --- /dev/null +++ b/packages/cli/test/acp/error.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { RequestError } from "@agentclientprotocol/sdk" +import { ACPError } from "../../src/acp/error" + +describe("acp errors", () => { + test("maps validation failures to invalid params", () => { + const errors: ACPError.Error[] = [ + new ACPError.SessionNotFoundError({ sessionId: "ses_missing" }), + new ACPError.InvalidConfigOptionError({ configId: "temperature" }), + new ACPError.InvalidModelError({ modelId: "missing" }), + new ACPError.InvalidEffortError({ effort: "extreme" }), + new ACPError.InvalidModeError({ mode: "turbo" }), + ] + expect(errors.map((error) => ACPError.toRequestError(error).code)).toEqual([-32602, -32602, -32602, -32602, -32602]) + }) + + test("maps auth and service failures safely", () => { + const auth = ACPError.toRequestError(new ACPError.AuthRequiredError()) + expect(auth).toBeInstanceOf(RequestError) + expect(auth.code).toBe(-32000) + + const internal = ACPError.toRequestError(ACPError.fromUnknown(new Error("secret token"), "session")) + expect(internal.code).toBe(-32603) + expect(JSON.stringify(internal.toErrorResponse())).not.toContain("secret token") + }) +}) diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index 4bd0b0f7df..c4e623363c 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -23,14 +23,19 @@ test("acp prompt resolves after ordered turn updates", async () => { ) } if (url.pathname === "/api/session/ses_test/prompt") { - const body = (await request.json()) as { id: string } + const body: unknown = await request.json() + if (!body || typeof body !== "object") { + return new Response(null, { status: 400 }) + } + const id = Reflect.get(body, "id") + if (typeof id !== "string") return new Response(null, { status: 400 }) queueMicrotask(() => { if (!events) return send(events, { id: "evt_promoted", created: 1, type: "session.input.promoted", - data: { sessionID: "ses_test", inputID: body.id }, + data: { sessionID: "ses_test", inputID: id }, }) send(events, { id: "evt_text", @@ -80,6 +85,7 @@ test("acp prompt resolves after ordered turn updates", async () => { try { const id = "msg_prompt" + const userMessageID = "client-message" const response = await streamTurn({ client, connection: { @@ -91,6 +97,7 @@ test("acp prompt resolves after ordered turn updates", async () => { sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, + userMessageID, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), activate: () => {}, deactivate: () => {}, @@ -106,7 +113,7 @@ test("acp prompt resolves after ordered turn updates", async () => { }, }, ]) - expect(response).toMatchObject({ stopReason: "end_turn", userMessageId: id, usage: { totalTokens: 2 } }) + expect(response).toMatchObject({ stopReason: "end_turn", userMessageId: userMessageID, usage: { totalTokens: 2 } }) } finally { events?.close() await server.stop(true) diff --git a/packages/cli/test/acp/service.test.ts b/packages/cli/test/acp/service.test.ts index d82fc431de..08bd95b310 100644 --- a/packages/cli/test/acp/service.test.ts +++ b/packages/cli/test/acp/service.test.ts @@ -20,7 +20,8 @@ describe("acp service", () => { if (url.pathname === "/api/model") return Response.json({ location, data: [model] }) if (url.pathname === "/api/model/default") return Response.json({ location, data: model }) if (url.pathname === "/api/agent") return Response.json({ location, data: [agent] }) - if (url.pathname === "/api/command") return Response.json({ location, data: [{ name: "review", template: "" }] }) + if (url.pathname === "/api/command") + return Response.json({ location, data: [{ name: "review", template: "" }] }) if (url.pathname === "/api/skill") return Response.json({ location, data: [skill] }) if (url.pathname === "/api/session" && request.method === "POST") return Response.json({ data: session }) if (url.pathname === "/api/mcp/docs" && request.method === "PUT") return new Response(null, { status: 204 }) diff --git a/packages/cli/test/acp/tool.test.ts b/packages/cli/test/acp/tool.test.ts index fbc03ed097..fb66bcedf8 100644 --- a/packages/cli/test/acp/tool.test.ts +++ b/packages/cli/test/acp/tool.test.ts @@ -1,5 +1,12 @@ import { describe, expect, test } from "bun:test" -import { completedToolUpdate, errorToolUpdate, pendingToolCall, runningToolUpdate, toLocations, toToolKind } from "../../src/acp/tool" +import { + completedToolUpdate, + errorToolUpdate, + pendingToolCall, + runningToolUpdate, + toLocations, + toToolKind, +} from "../../src/acp/tool" describe("acp tools", () => { test("maps kinds and locations", () => { @@ -11,7 +18,9 @@ describe("acp tools", () => { }) test("builds tool lifecycle updates", () => { - expect(pendingToolCall({ toolCallId: "call", toolName: "read", state: { input: { filePath: "/tmp/a" } } })).toMatchObject({ + expect( + pendingToolCall({ toolCallId: "call", toolName: "read", state: { input: { filePath: "/tmp/a" } } }), + ).toMatchObject({ toolCallId: "call", status: "pending", kind: "read", @@ -20,12 +29,29 @@ describe("acp tools", () => { toolCallId: "call", status: "in_progress", }) - expect(completedToolUpdate({ toolCallId: "call", toolName: "read", input: {}, content: [{ type: "text", text: "done" }], structured: {} })).toMatchObject({ + expect( + completedToolUpdate({ + toolCallId: "call", + toolName: "read", + input: {}, + content: [{ type: "text", text: "done" }], + structured: {}, + }), + ).toMatchObject({ toolCallId: "call", status: "completed", content: [{ type: "content", content: { type: "text", text: "done" } }], }) - expect(errorToolUpdate({ toolCallId: "call", toolName: "read", input: {}, content: [], structured: {}, error: "failed" })).toMatchObject({ + expect( + errorToolUpdate({ + toolCallId: "call", + toolName: "read", + input: {}, + content: [], + structured: {}, + error: "failed", + }), + ).toMatchObject({ toolCallId: "call", status: "failed", })