import { describe, expect, it, vi } from "vitest"; import { isOpenCodeUnknownSessionError, parseOpenCodeJsonl } from "@paperclipai/adapter-opencode-local/server"; import { parseOpenCodeStdoutLine } from "@paperclipai/adapter-opencode-local/ui"; import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli"; describe("opencode_local parser", () => { it("extracts session, summary, usage, cost, and terminal error message", () => { const stdout = [ JSON.stringify({ type: "step_start", sessionID: "ses_123" }), JSON.stringify({ type: "text", part: { type: "text", text: "hello" } }), JSON.stringify({ type: "step_finish", part: { reason: "tool-calls", cost: 0.001, tokens: { input: 100, output: 40, cache: { read: 20, write: 0 }, }, }, }), JSON.stringify({ type: "step_finish", part: { reason: "stop", cost: 0.002, tokens: { input: 50, output: 25, cache: { read: 10, write: 0 }, }, }, }), JSON.stringify({ type: "error", message: "model access denied" }), ].join("\n"); const parsed = parseOpenCodeJsonl(stdout); expect(parsed.sessionId).toBe("ses_123"); expect(parsed.summary).toBe("hello"); expect(parsed.usage).toEqual({ inputTokens: 150, cachedInputTokens: 30, outputTokens: 65, }); expect(parsed.costUsd).toBeCloseTo(0.003, 6); expect(parsed.errorMessage).toBe("model access denied"); }); }); describe("opencode_local stale session detection", () => { it("treats missing persisted session file as an unknown session error", () => { const stderr = "NotFoundError: Resource not found: /Users/test/.local/share/opencode/storage/session/project/ses_missing.json"; expect(isOpenCodeUnknownSessionError("", stderr)).toBe(true); }); }); describe("opencode_local ui stdout parser", () => { it("parses assistant and tool lifecycle events", () => { const ts = "2026-03-04T00:00:00.000Z"; expect( parseOpenCodeStdoutLine( JSON.stringify({ type: "text", part: { type: "text", text: "I will run a command.", }, }), ts, ), ).toEqual([ { kind: "assistant", ts, text: "I will run a command.", }, ]); expect( parseOpenCodeStdoutLine( JSON.stringify({ type: "tool_use", part: { id: "prt_tool_1", callID: "call_1", tool: "bash", state: { status: "completed", input: { command: "ls -1" }, output: "AGENTS.md\nDockerfile\n", metadata: { exit: 0 }, }, }, }), ts, ), ).toEqual([ { kind: "tool_call", ts, name: "bash", toolUseId: "call_1", input: { command: "ls -1" }, }, { kind: "tool_result", ts, toolUseId: "call_1", content: "status: completed\nexit: 0\n\nAGENTS.md\nDockerfile", isError: false, }, ]); }); it("parses finished steps into usage-aware results", () => { const ts = "2026-03-04T00:00:00.000Z"; expect( parseOpenCodeStdoutLine( JSON.stringify({ type: "step_finish", part: { reason: "stop", cost: 0.00042, tokens: { input: 10, output: 5, cache: { read: 2, write: 0 }, }, }, }), ts, ), ).toEqual([ { kind: "result", ts, text: "stop", inputTokens: 10, outputTokens: 5, cachedTokens: 2, costUsd: 0.00042, subtype: "stop", isError: false, errors: [], }, ]); }); }); function stripAnsi(value: string): string { return value.replace(/\x1b\[[0-9;]*m/g, ""); } describe("opencode_local cli formatter", () => { it("prints step, assistant, tool, and result events", () => { const spy = vi.spyOn(console, "log").mockImplementation(() => {}); try { printOpenCodeStreamEvent( JSON.stringify({ type: "step_start", sessionID: "ses_abc" }), false, ); printOpenCodeStreamEvent( JSON.stringify({ type: "text", part: { type: "text", text: "hello" }, }), false, ); printOpenCodeStreamEvent( JSON.stringify({ type: "tool_use", part: { callID: "call_1", tool: "bash", state: { status: "completed", input: { command: "ls -1" }, output: "AGENTS.md\n", metadata: { exit: 0 }, }, }, }), false, ); printOpenCodeStreamEvent( JSON.stringify({ type: "step_finish", part: { reason: "stop", cost: 0.00042, tokens: { input: 10, output: 5, cache: { read: 2, write: 0 }, }, }, }), false, ); const lines = spy.mock.calls .map((call) => call.map((v) => String(v)).join(" ")) .map(stripAnsi); expect(lines).toEqual( expect.arrayContaining([ "step started (session: ses_abc)", "assistant: hello", "tool_call: bash (call_1)", "tool_result status=completed exit=0", "AGENTS.md", "step finished: reason=stop", "tokens: in=10 out=5 cached=2 cost=$0.000420", ]), ); } finally { spy.mockRestore(); } }); });