import { describe, expect, it, vi } from "vitest"; import { isCodeBuddyUnknownSessionError, parseCodeBuddyJsonl, } from "@penclipai/adapter-codebuddy-local/server"; import { parseCodeBuddyStdoutLine } from "@penclipai/adapter-codebuddy-local/ui"; import { printCodeBuddyStreamEvent } from "@penclipai/adapter-codebuddy-local/cli"; describe("codebuddy parser", () => { it("extracts session, summary, usage, cost, and terminal error message", () => { const stdout = [ JSON.stringify({ type: "system", subtype: "init", session_id: "cb_123", model: "glm-5.0" }), JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "hello" }], }, }), JSON.stringify({ type: "result", subtype: "success", session_id: "cb_123", usage: { input_tokens: 100, cached_input_tokens: 25, output_tokens: 40, }, total_cost_usd: 0.001, result: "Task complete", }), JSON.stringify({ type: "error", message: "model access denied" }), ].join("\n"); const parsed = parseCodeBuddyJsonl(stdout); expect(parsed.sessionId).toBe("cb_123"); expect(parsed.summary).toBe("hello"); expect(parsed.usage).toEqual({ inputTokens: 100, cachedInputTokens: 25, outputTokens: 40, }); expect(parsed.costUsd).toBeCloseTo(0.001, 6); expect(parsed.errorMessage).toBe("model access denied"); }); it("parses multiplexed stdout-prefixed json lines", () => { const stdout = [ 'stdout{"type":"system","subtype":"init","session_id":"cb_prefixed","model":"glm-5.0"}', 'stdout{"type":"assistant","message":{"content":[{"type":"output_text","text":"prefixed hello"}]}}', 'stdout{"type":"result","subtype":"success","usage":{"input_tokens":3,"output_tokens":2,"cached_input_tokens":1},"total_cost_usd":0.0001}', ].join("\n"); const parsed = parseCodeBuddyJsonl(stdout); expect(parsed.sessionId).toBe("cb_prefixed"); expect(parsed.summary).toBe("prefixed hello"); expect(parsed.usage).toEqual({ inputTokens: 3, cachedInputTokens: 1, outputTokens: 2, }); expect(parsed.costUsd).toBeCloseTo(0.0001, 6); }); }); describe("codebuddy stale session detection", () => { it("treats missing/unknown session messages as an unknown session error", () => { expect( isCodeBuddyUnknownSessionError( "", "No conversation found with session ID: cb_123", ), ).toBe(true); expect( isCodeBuddyUnknownSessionError( "", "resume session not found", ), ).toBe(true); }); }); describe("codebuddy ui stdout parser", () => { it("parses assistant, thinking, and tool lifecycle events", () => { const ts = "2026-04-02T00:00:00.000Z"; expect( parseCodeBuddyStdoutLine( JSON.stringify({ type: "assistant", message: { content: [ { type: "output_text", text: "I will run a command." }, { type: "thinking", text: "Checking repository state" }, { type: "tool_call", name: "shellToolCall", input: { command: "ls -1" } }, { type: "tool_result", tool_use_id: "tool_1", output: "AGENTS.md\n", status: "ok" }, ], }, }), ts, ), ).toEqual([ { kind: "assistant", ts, text: "I will run a command." }, { kind: "thinking", ts, text: "Checking repository state" }, { kind: "tool_call", ts, name: "shellToolCall", input: { command: "ls -1" } }, { kind: "tool_result", ts, toolUseId: "tool_1", content: "AGENTS.md\n", isError: false }, ]); }); it("parses result usage and shell tool compaction", () => { const ts = "2026-04-02T00:00:00.000Z"; expect( parseCodeBuddyStdoutLine( JSON.stringify({ type: "tool_call", subtype: "started", call_id: "call_shell_1", tool_call: { shellToolCall: { command: "curl -s https://example.com", workingDirectory: "/tmp", }, }, }), ts, ), ).toEqual([ { kind: "tool_call", ts, name: "shellToolCall", toolUseId: "call_shell_1", input: { command: "curl -s https://example.com" }, }, ]); expect( parseCodeBuddyStdoutLine( JSON.stringify({ type: "tool_call", subtype: "completed", call_id: "call_shell_1", tool_call: { shellToolCall: { result: { success: { exitCode: 0, stdout: "ok", stderr: "", }, }, }, }, }), ts, ), ).toEqual([ { kind: "tool_result", ts, toolUseId: "call_shell_1", content: "exit 0\n\nok", isError: false, }, ]); expect( parseCodeBuddyStdoutLine( JSON.stringify({ type: "result", subtype: "success", result: "Done", usage: { input_tokens: 10, output_tokens: 5, cached_input_tokens: 2, }, total_cost_usd: 0.00042, is_error: false, }), ts, ), ).toEqual([ { kind: "result", ts, text: "Done", inputTokens: 10, outputTokens: 5, cachedTokens: 2, costUsd: 0.00042, subtype: "success", isError: false, errors: [], }, ]); }); }); function stripAnsi(value: string): string { return value.replace(/\x1b\[[0-9;]*m/g, ""); } describe("codebuddy cli formatter", () => { it("prints init, user, assistant, tool, and result events", () => { const spy = vi.spyOn(console, "log").mockImplementation(() => {}); try { printCodeBuddyStreamEvent( JSON.stringify({ type: "system", subtype: "init", session_id: "cb_abc", model: "glm-5.0" }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "user", message: { content: [{ type: "text", text: "run tests" }], }, }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "hello" }], }, }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "thinking", subtype: "delta", text: "looking at package.json", }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "tool_call", subtype: "started", call_id: "call_1", tool_call: { readToolCall: { args: { path: "README.md" }, }, }, }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "tool_call", subtype: "completed", call_id: "call_1", tool_call: { readToolCall: { result: { success: { content: "README contents" } }, }, }, }), false, ); printCodeBuddyStreamEvent( JSON.stringify({ type: "result", subtype: "success", result: "Done", usage: { input_tokens: 10, output_tokens: 5, cached_input_tokens: 2 }, total_cost_usd: 0.00042, }), false, ); const lines = spy.mock.calls .map((call) => call.map((value) => String(value)).join(" ")) .map(stripAnsi); expect(lines).toEqual( expect.arrayContaining([ "CodeBuddy init (session: cb_abc, model: glm-5.0)", "user: run tests", "assistant: hello", "thinking: looking at package.json", "tool_call: readToolCall (call_1)", "tool_result (call_1)", '{\n "success": {\n "content": "README contents"\n }\n}', "result: subtype=success", "tokens: in=10 out=5 cached=2 cost=$0.000420", "assistant: Done", ]), ); } finally { spy.mockRestore(); } }); });