{"version":3,"file":"messages-transcript-integrity.test.d.ts","sourceRoot":"","sources":["../../src/core/messages-transcript-integrity.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Regression tests for tool-call transcript integrity.\n *\n * The release fix disables all historical todo_write folding/compaction. Every\n * assistant tool-call span and its matching tool result must be preserved in\n * chronological order so the wire payload is always provider-clean.\n */\n\nimport type { AgentMessage } from \"@apholdings/jensen-agent-core\";\nimport type { AssistantMessage, ToolResultMessage } from \"@apholdings/jensen-ai\";\nimport {\n\ttype Message,\n\tvalidateChatCompletionsTranscript,\n\tvalidateResponsesTranscript,\n\tvalidateToolSpanIntegrity,\n} from \"@apholdings/jensen-ai\";\nimport { describe, expect, it } from \"vitest\";\nimport { convertToLlm } from \"./messages.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst usage = {\n\tinput: 0,\n\toutput: 0,\n\tcacheRead: 0,\n\tcacheWrite: 0,\n\ttotalTokens: 0,\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n};\n\nfunction mkAssistant(\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown>; text?: string }>,\n\tstopReason: \"toolUse\" | \"stop\" = \"toolUse\",\n): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tapi: \"openai-completions\",\n\t\tprovider: \"openrouter\",\n\t\tmodel: \"deepseek/deepseek-v4-flash-0731\",\n\t\tusage,\n\t\tstopReason,\n\t\ttimestamp: 1000,\n\t\tcontent: content as AssistantMessage[\"content\"],\n\t};\n}\n\nfunction makeAssistantToolCall(toolName: string, id: string, args?: Record<string, unknown>): AssistantMessage {\n\treturn mkAssistant([{ type: \"toolCall\", name: toolName, id, arguments: args ?? {} }]);\n}\n\nfunction makeAssistantText(text: string): AssistantMessage {\n\treturn mkAssistant([{ type: \"text\", text }], \"stop\");\n}\n\nfunction makeBashExec(output: string): AgentMessage {\n\treturn {\n\t\trole: \"bashExecution\",\n\t\tcommand: \"echo hello\",\n\t\toutput,\n\t\texitCode: 0,\n\t\tcancelled: false,\n\t\ttruncated: false,\n\t\ttimestamp: Date.now(),\n\t} satisfies AgentMessage;\n}\n\nfunction makeToolResult(toolName: string, toolCallId: string, isError = false): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",\n\t\ttoolCallId,\n\t\ttoolName,\n\t\tcontent: [{ type: \"text\", text: \"done\" }],\n\t\tisError,\n\t\ttimestamp: 2000,\n\t};\n}\n\ntype FMsg =\n\t| ReturnType<typeof makeAssistantToolCall>\n\t| ReturnType<typeof makeBashExec>\n\t| ReturnType<typeof makeToolResult>\n\t| ReturnType<typeof makeAssistantText>;\n\n// ---------------------------------------------------------------------------\n// convertToLlm orphan detection\n// ---------------------------------------------------------------------------\n\ndescribe(\"convertToLlm: no folding, every span preserved\", () => {\n\tit(\"preserves every tool result including todo_write\", () => {\n\t\tconst messages: FMsg[] = [\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"call_abc\"),\n\t\t\tmakeBashExec(\"stdout output\"),\n\t\t\tmakeToolResult(\"todo_write\", \"call_abc\"),\n\t\t\tmakeAssistantToolCall(\"read_file\", \"call_xyz\"),\n\t\t\tmakeToolResult(\"read_file\", \"call_xyz\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(2);\n\t\texpect(toolResults[0].toolCallId).toBe(\"call_abc\");\n\t\texpect(toolResults[1].toolCallId).toBe(\"call_xyz\");\n\t});\n\n\tit(\"keeps non-todo_write toolResults intact\", () => {\n\t\tconst llmMessages = convertToLlm([\n\t\t\tmakeAssistantToolCall(\"read_file\", \"call_rf\"),\n\t\t\tmakeToolResult(\"read_file\", \"call_rf\"),\n\t\t]);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(1);\n\t\texpect(toolResults[0].toolCallId).toBe(\"call_rf\");\n\t});\n\n\tit(\"incomplete todo_write span (no result yet) keeps both halves\", () => {\n\t\tconst llmMessages = convertToLlm([makeAssistantToolCall(\"todo_write\", \"call_incomplete\")]);\n\t\texpect(llmMessages).toHaveLength(1);\n\t\texpect(llmMessages[0].role).toBe(\"assistant\");\n\t\tif (llmMessages[0].role === \"assistant\") {\n\t\t\tconst toolCalls = llmMessages[0].content.filter((b) => b.type === \"toolCall\");\n\t\t\texpect(toolCalls).toHaveLength(1);\n\t\t\texpect(toolCalls[0].id).toBe(\"call_incomplete\");\n\t\t}\n\t});\n\n\tit(\"todo_write + bash — todo result visible\", () => {\n\t\tconst messages: FMsg[] = [\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"call_tw\"),\n\t\t\tmakeBashExec(\"ls output\"),\n\t\t\tmakeToolResult(\"todo_write\", \"call_tw\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(1);\n\t\texpect(toolResults[0].toolCallId).toBe(\"call_tw\");\n\t});\n\n\tit(\"todo_write error result also survives\", () => {\n\t\tconst llmMessages = convertToLlm([\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"call_err\"),\n\t\t\tmakeBashExec(\"error\"),\n\t\t\tmakeToolResult(\"todo_write\", \"call_err\", true),\n\t\t]);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(1);\n\t\texpect(toolResults[0].isError).toBe(true);\n\t});\n\n\tit(\"multiple parallel tools — all results preserved\", () => {\n\t\tconst assistant = mkAssistant([\n\t\t\t{ type: \"toolCall\", name: \"todo_write\", id: \"c1\" },\n\t\t\t{ type: \"toolCall\", name: \"bash\", id: \"c2\" },\n\t\t\t{ type: \"toolCall\", name: \"read_file\", id: \"c3\" },\n\t\t]);\n\t\tconst messages: FMsg[] = [\n\t\t\tassistant,\n\t\t\tmakeBashExec(\"ls -la\"),\n\t\t\tmakeToolResult(\"todo_write\", \"c1\"),\n\t\t\tmakeToolResult(\"bash\", \"c2\"),\n\t\t\tmakeToolResult(\"read_file\", \"c3\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(3);\n\t\tconst ids = toolResults.map((m) => m.toolCallId);\n\t\texpect(ids).toContain(\"c1\");\n\t\texpect(ids).toContain(\"c2\");\n\t\texpect(ids).toContain(\"c3\");\n\t});\n\n\tit(\"sequential spans remain grouped\", () => {\n\t\tconst llmMessages = convertToLlm([\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"c_tw\"),\n\t\t\tmakeToolResult(\"todo_write\", \"c_tw\"),\n\t\t\tmakeAssistantToolCall(\"read_file\", \"c_rf\"),\n\t\t\tmakeToolResult(\"read_file\", \"c_rf\"),\n\t\t]);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(2);\n\t\texpect(toolResults.map((r) => r.toolCallId)).toEqual([\"c_tw\", \"c_rf\"]);\n\t});\n});\n\n// ---------------------------------------------------------------------------\n// Provider-level transcript validation catches orphans\n// ---------------------------------------------------------------------------\n\ndescribe(\"transcript validation catches orphan results pre-transport\", () => {\n\tit(\"detects orphan toolResult in chat completions flow\", () => {\n\t\tconst assistant = makeAssistantToolCall(\"read_file\", \"call_ok\");\n\t\tconst orphanTw = {\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: \"orphan_tw\",\n\t\t\ttoolName: \"todo_write\",\n\t\t\tcontent: [{ type: \"text\", text: \"done\" }],\n\t\t\ttimestamp: 3000,\n\t\t} as unknown as Message;\n\t\tconst result = validateChatCompletionsTranscript([assistant, orphanTw], \"openrouter\", \"gpt-5.6-luna\");\n\t\texpect(\"code\" in result).toBe(true);\n\t\tif (\"code\" in result) {\n\t\t\texpect(result.code).toBe(\"INVALID_TOOL_TRANSCRIPT\");\n\t\t\texpect(result.orphanToolResultIds).toContain(\"orphan_tw\");\n\t\t}\n\t});\n\n\tit(\"valid complete span passes chat completions validator\", () => {\n\t\tconst assistant = makeAssistantToolCall(\"read_file\", \"call_ok\");\n\t\tconst result = makeToolResult(\"read_file\", \"call_ok\");\n\t\texpect(validateChatCompletionsTranscript([assistant, result], \"openrouter\", \"gpt-5.6-luna\")).toEqual({\n\t\t\tok: true,\n\t\t});\n\t});\n\n\tit(\"valid complete span passes responses validator\", () => {\n\t\tconst assistant = makeAssistantToolCall(\"read_file\", \"call_resp\");\n\t\tconst result = makeToolResult(\"read_file\", \"call_resp\");\n\t\texpect(validateResponsesTranscript([assistant, result], \"opencode-go\", \"gpt-5.6-luna\")).toEqual({ ok: true });\n\t});\n\n\tit(\"duplicate tool results detected by chat completions validator\", () => {\n\t\tconst assistant = makeAssistantToolCall(\"read_file\", \"call_dup\");\n\t\tconst r1 = makeToolResult(\"read_file\", \"call_dup\");\n\t\tconst r2 = makeToolResult(\"read_file\", \"call_dup\");\n\t\tconst result = validateChatCompletionsTranscript([assistant, r1, r2], \"openrouter\", \"gpt-5.6-luna\");\n\t\texpect(\"code\" in result).toBe(true);\n\t});\n\n\tit(\"span checker detects orphan tool results\", () => {\n\t\tconst assistant = makeAssistantToolCall(\"read_file\", \"call_ok\");\n\t\tconst orphanTw = {\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: \"orphan_tw\",\n\t\t\ttoolName: \"todo_write\",\n\t\t\tcontent: [{ type: \"text\", text: \"done\" }],\n\t\t\ttimestamp: 3000,\n\t\t} as unknown as Message;\n\t\tconst spanCheck = validateToolSpanIntegrity([assistant, orphanTw]);\n\t\texpect(spanCheck.valid).toBe(false);\n\t\texpect(spanCheck.orphanToolResultIds).toContain(\"orphan_tw\");\n\t});\n});\n\n// ---------------------------------------------------------------------------\n// Full pipeline: convertToLlm → validate (simulates wire payload)\n// ---------------------------------------------------------------------------\n\ndescribe(\"full pipeline: produces provider-clean payload end-to-end\", () => {\n\tit(\"chat completions: todo_write → read_file → clean wire payload\", () => {\n\t\tconst messages: FMsg[] = [\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"call_tw_001\"),\n\t\t\tmakeToolResult(\"todo_write\", \"call_tw_001\"),\n\t\t\tmakeAssistantToolCall(\"read_file\", \"call_rf_001\"),\n\t\t\tmakeToolResult(\"read_file\", \"call_rf_001\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\texpect(llmMessages.filter((m) => m.role === \"toolResult\")).toHaveLength(2);\n\t\tconst spanCheck = validateToolSpanIntegrity(llmMessages);\n\t\texpect(spanCheck.valid).toBe(true);\n\t\tconst validation = validateChatCompletionsTranscript(\n\t\t\tllmMessages,\n\t\t\t\"openrouter\",\n\t\t\t\"deepseek/deepseek-v4-flash-0731\",\n\t\t);\n\t\texpect(validation).toEqual({ ok: true });\n\t});\n\n\tit(\"responses API: GPT-5.6 Luna — canonical function_call/output ordering\", () => {\n\t\tconst messages: FMsg[] = [\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"call_tw_luna\"),\n\t\t\tmakeToolResult(\"todo_write\", \"call_tw_luna\"),\n\t\t\tmakeAssistantText(\"Analysis complete.\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\texpect(llmMessages.filter((m) => m.role === \"toolResult\")).toHaveLength(1);\n\t\tconst validation = validateResponsesTranscript(llmMessages, \"openrouter\", \"openai/gpt-5.6-luna\");\n\t\texpect(validation).toEqual({ ok: true });\n\t});\n\n\tit(\"historical text placeholder span does not break validation\", () => {\n\t\tconst sessionMessages: FMsg[] = [\n\t\t\tmkAssistant(\n\t\t\t\t[\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: \"A previous summary.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\t\"stop\",\n\t\t\t) as FMsg,\n\t\t\tmakeAssistantToolCall(\"read_file\", \"call_new\"),\n\t\t\tmakeToolResult(\"read_file\", \"call_new\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(sessionMessages);\n\t\tconst spanCheck = validateToolSpanIntegrity(llmMessages);\n\t\texpect(spanCheck.valid).toBe(true);\n\t\tconst validation = validateChatCompletionsTranscript(\n\t\t\tllmMessages,\n\t\t\t\"openrouter\",\n\t\t\t\"deepseek/deepseek-v4-flash-0731\",\n\t\t);\n\t\texpect(validation).toEqual({ ok: true });\n\t});\n\n\tit(\"todo_write followed by multiple sequential tools — canonical spans\", () => {\n\t\tconst messages: FMsg[] = [\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"tw_01\"),\n\t\t\tmakeToolResult(\"todo_write\", \"tw_01\"),\n\t\t\tmakeAssistantToolCall(\"read_file\", \"rf_01\"),\n\t\t\tmakeToolResult(\"read_file\", \"rf_01\"),\n\t\t\tmakeAssistantText(\"Complete.\"),\n\t\t];\n\t\tconst llmMessages = convertToLlm(messages);\n\t\texpect(llmMessages.filter((m) => m.role === \"assistant\")).toHaveLength(3);\n\t\texpect(llmMessages.filter((m) => m.role === \"toolResult\")).toHaveLength(2);\n\t\tconst validation = validateChatCompletionsTranscript(\n\t\t\tllmMessages,\n\t\t\t\"openrouter\",\n\t\t\t\"deepseek/deepseek-v4-flash-0731\",\n\t\t);\n\t\texpect(validation).toEqual({ ok: true });\n\t});\n\n\tit(\"todo_write followed by parallel tools remains valid\", () => {\n\t\tconst llmMessages = convertToLlm([\n\t\t\tmakeAssistantToolCall(\"todo_write\", \"tw_par\"),\n\t\t\tmakeAssistantToolCall(\"read_file\", \"rf_p1\"),\n\t\t\tmakeToolResult(\"todo_write\", \"tw_par\"),\n\t\t\tmakeToolResult(\"read_file\", \"rf_p1\"),\n\t\t]);\n\t\texpect(llmMessages.filter((m) => m.role === \"toolResult\")).toHaveLength(2);\n\t});\n\n\tit(\"todo_write stale-revision error remains paired (non-todo-write result preserved)\", () => {\n\t\tconst llmMessages = convertToLlm([\n\t\t\tmakeAssistantToolCall(\"todo_update\", \"tu_stale\"),\n\t\t\tmakeToolResult(\"todo_update\", \"tu_stale\", true),\n\t\t]);\n\t\tconst toolResults = llmMessages.filter((m) => m.role === \"toolResult\");\n\t\texpect(toolResults).toHaveLength(1);\n\t\texpect(toolResults[0].toolCallId).toBe(\"tu_stale\");\n\t});\n});\n"]}