From ee3aa0e036f476270169b2fb6b19d4d7c0d1be1b Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Sun, 3 May 2026 14:36:24 +0700 Subject: [PATCH] test(session-file-repair): add regression for trailing stop-assistant after tool-result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a test covering the exact bug scenario described in the fix document: user → assistant(toolCall) → toolResult → assistant(text, stopReason=stop). The repair pass must leave this sequence untouched; previously, overly broad trimming logic could drop the final text response, breaking multi-turn context for the next user message. The test verifies repaired=false and byte-identity of the session file. Co-Authored-By: Claude Sonnet 4.6 --- src/agents/session-file-repair.test.ts | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/agents/session-file-repair.test.ts b/src/agents/session-file-repair.test.ts index f0151e96843..1efb6fc6c41 100644 --- a/src/agents/session-file-repair.test.ts +++ b/src/agents/session-file-repair.test.ts @@ -465,6 +465,60 @@ describe("repairSessionFileIfNeeded", () => { expect(after).toBe(original); }); + it("preserves final text assistant turn that follows a tool-call/tool-result pair", async () => { + // Regression: a trailing assistant message with stopReason "stop" that follows a + // tool-call turn and its matching tool-result must never be trimmed by the repair + // pass. This is the exact sequence produced by any agent run that calls at least + // one tool before returning a final text response, and it must survive intact so + // subsequent user messages are parented to the correct leaf node. + const { file } = await createTempSessionPath(); + const { header, message } = buildSessionHeaderAndMessage(); + const toolCallAssistant = { + type: "message", + id: "msg-asst-tc", + parentId: "msg-1", + timestamp: new Date().toISOString(), + message: { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "get_tasks", input: {} }], + stopReason: "toolUse", + }, + }; + const toolResult = { + type: "message", + id: "msg-tool-result", + parentId: "msg-asst-tc", + timestamp: new Date().toISOString(), + message: { + role: "toolResult", + toolCallId: "call_1", + toolName: "get_tasks", + content: [{ type: "text", text: "Task A, Task B" }], + isError: false, + }, + }; + const finalAssistant = { + type: "message", + id: "msg-asst-final", + parentId: "msg-tool-result", + timestamp: new Date().toISOString(), + message: { + role: "assistant", + content: [{ type: "text", text: "Here are your tasks: Task A, Task B." }], + stopReason: "stop", + }, + }; + const original = `${JSON.stringify(header)}\n${JSON.stringify(message)}\n${JSON.stringify(toolCallAssistant)}\n${JSON.stringify(toolResult)}\n${JSON.stringify(finalAssistant)}\n`; + await fs.writeFile(file, original, "utf-8"); + + const result = await repairSessionFileIfNeeded({ sessionFile: file }); + + expect(result.repaired).toBe(false); + + const after = await fs.readFile(file, "utf-8"); + expect(after).toBe(original); + }); + it("preserves assistant-only session history after the header", async () => { const { file } = await createTempSessionPath(); const { header } = buildSessionHeaderAndMessage();