import { describe, expect, it, vi } from "vitest"; import { isCursorUnknownSessionError, parseCursorJsonl } from "@paperclipai/adapter-cursor-local/server"; import { parseCursorStdoutLine } from "@paperclipai/adapter-cursor-local/ui"; import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli"; describe("cursor parser", () => { it("extracts session, summary, usage, cost, and terminal error message", () => { const stdout = [ JSON.stringify({ type: "system", subtype: "init", session_id: "chat_123", model: "gpt-5" }), JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "hello" }], }, }), JSON.stringify({ type: "result", subtype: "success", session_id: "chat_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 = parseCursorJsonl(stdout); expect(parsed.sessionId).toBe("chat_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":"chat_prefixed","model":"gpt-5"}', '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 = parseCursorJsonl(stdout); expect(parsed.sessionId).toBe("chat_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("cursor stale session detection", () => { it("treats missing/unknown session messages as an unknown session error", () => { expect(isCursorUnknownSessionError("", "unknown session id chat_123")).toBe(true); expect(isCursorUnknownSessionError("", "chat abc not found")).toBe(true); }); }); describe("cursor ui stdout parser", () => { it("parses assistant, thinking, and tool lifecycle events", () => { const ts = "2026-03-05T00:00:00.000Z"; expect( parseCursorStdoutLine( 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: "bash", 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: "bash", input: { command: "ls -1" } }, { kind: "tool_result", ts, toolUseId: "tool_1", content: "AGENTS.md\n", isError: false }, ]); }); it("parses result usage and errors", () => { const ts = "2026-03-05T00:00:00.000Z"; expect( parseCursorStdoutLine( 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: [], }, ]); }); it("parses stdout-prefixed json lines", () => { const ts = "2026-03-05T00:00:00.000Z"; expect( parseCursorStdoutLine( 'stdout{"type":"assistant","message":{"content":[{"type":"thinking","text":"streamed"}]}}', ts, ), ).toEqual([{ kind: "thinking", ts, text: "streamed" }]); }); it("compacts shellToolCall and shell tool result for run log", () => { const ts = "2026-03-05T00:00:00.000Z"; const longCommand = "curl -s -X POST \"$PAPERCLIP_API_URL/api/issues/abc/checkout\" -H \"Authorization: Bearer $PAPERCLIP_API_KEY\""; expect( parseCursorStdoutLine( JSON.stringify({ type: "tool_call", subtype: "started", call_id: "call_shell_1", tool_call: { shellToolCall: { command: longCommand, workingDirectory: "/tmp", timeout: 30000, toolCallId: "tool_xyz", simpleCommands: ["curl"], parsingResult: { parsingFailed: false, executableCommands: [] }, }, }, }), ts, ), ).toEqual([ { kind: "tool_call", ts, name: "shellToolCall", toolUseId: "call_shell_1", input: { command: longCommand }, }, ]); expect( parseCursorStdoutLine( JSON.stringify({ type: "tool_call", subtype: "completed", call_id: "call_shell_1", tool_call: { shellToolCall: { result: { success: { command: longCommand, exitCode: 0, stdout: '{"id":"abc","status":"in_progress"}', stderr: "", executionTime: 100, }, }, }, }, }), ts, ), ).toEqual([ { kind: "tool_result", ts, toolUseId: "call_shell_1", content: "exit 0\n\n{\"id\":\"abc\",\"status\":\"in_progress\"}", isError: false, }, ]); }); it("parses user, top-level thinking, and top-level tool_call events", () => { const ts = "2026-03-05T00:00:00.000Z"; expect( parseCursorStdoutLine( JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: "Please inspect README.md" }], }, }), ts, ), ).toEqual([{ kind: "user", ts, text: "Please inspect README.md" }]); expect( parseCursorStdoutLine( JSON.stringify({ type: "thinking", subtype: "delta", text: "planning next command", }), ts, ), ).toEqual([{ kind: "thinking", ts, text: "planning next command", delta: true }]); expect( parseCursorStdoutLine( JSON.stringify({ type: "thinking", subtype: "delta", text: " with preserved leading space", }), ts, ), ).toEqual([{ kind: "thinking", ts, text: " with preserved leading space", delta: true }]); expect( parseCursorStdoutLine( JSON.stringify({ type: "tool_call", subtype: "started", call_id: "call_1", tool_call: { readToolCall: { args: { path: "README.md" }, }, }, }), ts, ), ).toEqual([{ kind: "tool_call", ts, name: "readToolCall", toolUseId: "call_1", input: { path: "README.md" } }]); expect( parseCursorStdoutLine( JSON.stringify({ type: "tool_call", subtype: "completed", call_id: "call_1", tool_call: { readToolCall: { result: { success: { content: "README contents" } }, }, }, }), ts, ), ).toEqual([ { kind: "tool_result", ts, toolUseId: "call_1", content: '{\n "success": {\n "content": "README contents"\n }\n}', isError: false, }, ]); }); }); function stripAnsi(value: string): string { return value.replace(/\x1b\[[0-9;]*m/g, ""); } describe("cursor cli formatter", () => { it("prints init, user, assistant, tool, and result events", () => { const spy = vi.spyOn(console, "log").mockImplementation(() => {}); try { printCursorStreamEvent( JSON.stringify({ type: "system", subtype: "init", session_id: "chat_abc", model: "gpt-5" }), false, ); printCursorStreamEvent( JSON.stringify({ type: "user", message: { content: [{ type: "text", text: "run tests" }], }, }), false, ); printCursorStreamEvent( JSON.stringify({ type: "assistant", message: { content: [{ type: "output_text", text: "hello" }], }, }), false, ); printCursorStreamEvent( JSON.stringify({ type: "thinking", subtype: "delta", text: "looking at package.json", }), false, ); printCursorStreamEvent( JSON.stringify({ type: "assistant", message: { content: [{ type: "tool_call", name: "bash", input: { command: "ls -1" } }], }, }), false, ); printCursorStreamEvent( JSON.stringify({ type: "assistant", message: { content: [{ type: "tool_result", output: "AGENTS.md", status: "ok" }], }, }), false, ); printCursorStreamEvent( JSON.stringify({ type: "tool_call", subtype: "started", call_id: "call_1", tool_call: { readToolCall: { args: { path: "README.md" }, }, }, }), false, ); printCursorStreamEvent( JSON.stringify({ type: "tool_call", subtype: "completed", call_id: "call_1", tool_call: { readToolCall: { result: { success: { content: "README contents" } }, }, }, }), false, ); printCursorStreamEvent( 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((v) => String(v)).join(" ")) .map(stripAnsi); expect(lines).toEqual( expect.arrayContaining([ "Cursor init (session: chat_abc, model: gpt-5)", "user: run tests", "assistant: hello", "thinking: looking at package.json", "tool_call: bash", "tool_call: readToolCall (call_1)", "tool_result (call_1)", '{\n "success": {\n "content": "README contents"\n }\n}', "tool_result", "AGENTS.md", "result: subtype=success", "tokens: in=10 out=5 cached=2 cost=$0.000420", "assistant: Done", ]), ); } finally { spy.mockRestore(); } }); });