Spaces:
Paused
Paused
| 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<stdout>\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(); | |
| } | |
| }); | |
| }); | |