From e58142f6dd108280c6059b42fbe7da7a11fb4907 Mon Sep 17 00:00:00 2001 From: "Zoo (VP)" Date: Sun, 20 Sep 2026 08:25:02 +0900 Subject: [PATCH 1/4] fix(anthropic): keep tool_result blocks contiguous by grouping image blocks separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a turn issued multiple tool_use blocks and tool results carried image blocks (read_file on images, screenshots, feedback images), presentAssistantMessage pushed each tool_result followed immediately by its images, yielding tool_result/image interleaving in userMessageContent. Task.ts then consumed the array verbatim, and the Anthropic API rejected the request with a 400: "tool_use ids were found without tool_result blocks immediately after" (Zoo-Code-Org/Zoo-Code#1307). pushToolResultToUserContent is the single choke point every tool_result push flows through, so it now maintains the invariant: when a new tool_result is appended, image blocks already accumulated are moved after it. The resulting user message is tool_results first (in tool_use order), then images, then other blocks — matching the shape the issue expects. A single tool result with images and image-free messages are unchanged. --- .../presentAssistantMessage-images.spec.ts | 136 +++++++++++++++++- src/core/task/Task.ts | 18 +++ src/core/task/__tests__/Task.spec.ts | 62 +++++++- 3 files changed, 208 insertions(+), 8 deletions(-) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts index 7cb4c427d8..0aa3f08bb1 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts @@ -5,6 +5,8 @@ import { Anthropic } from "@anthropic-ai/sdk" import { presentAssistantMessage } from "../presentAssistantMessage" import { Task } from "../../task/Task" +type UserContentBlock = Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam + // Mock dependencies vi.mock("../../task/Task") vi.mock("../../tools/validateToolUse", () => ({ @@ -62,7 +64,8 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } - // Add pushToolResultToUserContent method after mockTask is created so it can reference mockTask + // Mirrors Task.pushToolResultToUserContent: duplicate guard plus keeping + // tool_result blocks contiguous by moving image blocks after the new result. mockTask.pushToolResultToUserContent = vi.fn().mockImplementation((toolResult: any) => { const existingResult = mockTask.userMessageContent.find( (block: any) => block.type === "tool_result" && block.tool_use_id === toolResult.tool_use_id, @@ -71,6 +74,14 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = return false } mockTask.userMessageContent.push(toolResult) + const images = mockTask.userMessageContent.filter((block: UserContentBlock) => block.type === "image") + if (images.length > 0) { + const withoutImages = mockTask.userMessageContent.filter( + (block: UserContentBlock) => block.type !== "image", + ) + withoutImages.push(...images) + mockTask.userMessageContent.splice(0, mockTask.userMessageContent.length, ...withoutImages) + } return true }) }) @@ -319,4 +330,127 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = expect(toolResult.content).toContain("was interrupted and not executed") }) }) + + describe("tool_result contiguity (GitHub #1307)", () => { + it("groups images after all tool_results when parallel tools return images", async () => { + const toolCallId1 = "tool_call_img_1" + const toolCallId2 = "tool_call_img_2" + + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId1, + name: "ask_followup_question", + params: { question: "Q1?", follow_up: [] }, + nativeArgs: { question: "Q1?", follow_up: [] }, + }, + { + type: "tool_use", + id: toolCallId2, + name: "ask_followup_question", + params: { question: "Q2?", follow_up: [] }, + nativeArgs: { question: "Q2?", follow_up: [] }, + }, + ] + + // The tool asks the followup question via task.ask("followup", ...); + // distinct image data per question so ordering is observable. + mockTask.ask = vi.fn().mockImplementation((type: string, message: string) => { + if (type !== "followup") { + return Promise.resolve({ response: "yesButtonClicked", text: "approved", images: undefined }) + } + const prefix = message.includes("Q1") ? "a" : "b" + return Promise.resolve({ + response: "yesButtonClicked", + text: `answer ${prefix}`, + images: [`data:image/png;base64,${prefix}-result`], + }) + }) + + await presentAssistantMessage(mockTask) + + // Anthropic rejects user messages where image blocks sit between + // tool_result blocks, so images from both tools must be grouped + // after the last tool_result instead of interleaved. + expect(mockTask.userMessageContent.map((block: UserContentBlock) => block.type)).toEqual([ + "tool_result", + "tool_result", + "image", + "image", + ]) + expect( + mockTask.userMessageContent + .filter( + (block: UserContentBlock): block is Anthropic.ToolResultBlockParam => + block.type === "tool_result", + ) + .map((block: Anthropic.ToolResultBlockParam) => block.tool_use_id), + ).toEqual([toolCallId1, toolCallId2]) + expect( + mockTask.userMessageContent + .filter((block: UserContentBlock): block is Anthropic.ImageBlockParam => block.type === "image") + .map( + (block: Anthropic.ImageBlockParam) => + (block.source as { type: "base64"; media_type: string; data: string }).data, + ), + ).toEqual(["a-result", "b-result"]) + }) + + it("relocates an earlier tool's images after a later tool_result with no images", async () => { + const toolCallId1 = "tool_call_img_3" + const toolCallId2 = "tool_call_img_4" + + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId1, + name: "ask_followup_question", + params: { question: "Q1?", follow_up: [] }, + nativeArgs: { question: "Q1?", follow_up: [] }, + }, + { + type: "tool_use", + id: toolCallId2, + name: "ask_followup_question", + params: { question: "Q2?", follow_up: [] }, + nativeArgs: { question: "Q2?", follow_up: [] }, + }, + ] + + mockTask.ask = vi.fn().mockImplementation((type: string, message: string) => { + if (type === "followup" && message.includes("Q1")) { + return Promise.resolve({ + response: "yesButtonClicked", + text: "answer a", + images: ["data:image/png;base64,a-result"], + }) + } + return Promise.resolve({ response: "yesButtonClicked", text: "answer b", images: undefined }) + }) + + await presentAssistantMessage(mockTask) + + expect(mockTask.userMessageContent.map((block: UserContentBlock) => block.type)).toEqual([ + "tool_result", + "tool_result", + "image", + ]) + expect( + mockTask.userMessageContent + .filter( + (block: UserContentBlock): block is Anthropic.ToolResultBlockParam => + block.type === "tool_result", + ) + .map((block: Anthropic.ToolResultBlockParam) => block.tool_use_id), + ).toEqual([toolCallId1, toolCallId2]) + expect( + mockTask.userMessageContent + .filter((block: UserContentBlock): block is Anthropic.ImageBlockParam => block.type === "image") + .map( + (block: Anthropic.ImageBlockParam) => + (block.source as { type: "base64"; media_type: string; data: string }).data, + ), + ).toEqual(["a-result"]) + }) + }) }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 55798437c3..a5e44e261a 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -469,6 +469,12 @@ export class Task extends EventEmitter implements TaskLike { * Push a tool_result block to userMessageContent, preventing duplicates. * Duplicate tool_use_ids cause API errors. * + * Keeps all tool_result blocks contiguous, as the Anthropic API requires: + * image blocks attached to earlier tool results must not sit between two + * tool_results ("tool_use ids were found without tool_result blocks + * immediately after"), so any images preceding the new result are moved + * after it. + * * @param toolResult - The tool_result block to add * @returns true if added, false if duplicate was skipped */ @@ -484,6 +490,18 @@ export class Task extends EventEmitter implements TaskLike { return false } this.userMessageContent.push(toolResult) + + // The new result is always appended at the end, so every image block + // currently in the array precedes it and would split the tool_result + // run once another result arrives. Move them after the new result. + const images = this.userMessageContent.filter( + (block): block is Anthropic.ImageBlockParam => block.type === "image", + ) + if (images.length > 0) { + const withoutImages = this.userMessageContent.filter((block) => block.type !== "image") + this.userMessageContent.splice(0, this.userMessageContent.length, ...withoutImages, ...images) + } + return true } didRejectTool = false diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index c35acf864d..472ea7f23d 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6270,7 +6270,7 @@ describe("pushToolResultToUserContent", () => { expect(task.userMessageContent[0]).toEqual(errorResult) }) - it("should not interfere with other content types in userMessageContent", () => { + it("should preserve other content types while keeping tool_results contiguous", () => { const task = new Task({ provider: mockProvider, apiConfiguration: mockApiConfig, @@ -6279,10 +6279,11 @@ describe("pushToolResultToUserContent", () => { }) // Add text and image blocks manually - task.userMessageContent.push( - { type: "text", text: "Some text" }, - { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data" } }, - ) + const image: Anthropic.ImageBlockParam = { + type: "image", + source: { type: "base64", media_type: "image/png", data: "base64data" }, + } + task.userMessageContent.push({ type: "text", text: "Some text" }, image) const toolResult: Anthropic.ToolResultBlockParam = { type: "tool_result", @@ -6294,8 +6295,55 @@ describe("pushToolResultToUserContent", () => { expect(added).toBe(true) expect(task.userMessageContent).toHaveLength(3) + // The image must not sit between two tool_results, so it moves after + // the new result; other blocks keep their relative order. expect(task.userMessageContent[0].type).toBe("text") - expect(task.userMessageContent[1].type).toBe("image") - expect(task.userMessageContent[2]).toEqual(toolResult) + expect(task.userMessageContent[1]).toEqual(toolResult) + expect(task.userMessageContent[2]).toEqual(image) + }) + + it("keeps tool_result blocks contiguous when a later result has no images (GitHub #1307)", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const toolResult1: Anthropic.ToolResultBlockParam = { + type: "tool_result", + tool_use_id: "id-1", + content: "Result 1", + } + const toolResult2: Anthropic.ToolResultBlockParam = { + type: "tool_result", + tool_use_id: "id-2", + content: "Result 2", + } + const image1: Anthropic.ImageBlockParam = { + type: "image", + source: { type: "base64", media_type: "image/png", data: "img1" }, + } + const image2: Anthropic.ImageBlockParam = { + type: "image", + source: { type: "base64", media_type: "image/png", data: "img2" }, + } + + // First tool result arrives with its images appended right after it (as + // presentAssistantMessage does); a second result without images follows. + task.pushToolResultToUserContent(toolResult1) + task.userMessageContent.push(image1, image2) + task.pushToolResultToUserContent(toolResult2) + + expect(task.userMessageContent.map((block) => block.type)).toEqual([ + "tool_result", + "tool_result", + "image", + "image", + ]) + expect(task.userMessageContent[0]).toEqual(toolResult1) + expect(task.userMessageContent[1]).toEqual(toolResult2) + expect(task.userMessageContent[2]).toEqual(image1) + expect(task.userMessageContent[3]).toEqual(image2) }) }) From bebf26a7bc3fcc58c4e06ab7ad607b30aed19e71 Mon Sep 17 00:00:00 2001 From: "Zoo (VP)" Date: Sun, 20 Sep 2026 09:45:54 +0900 Subject: [PATCH 2/4] fix(anthropic): keep all tool_result blocks ahead of other user-message content --- .../presentAssistantMessage-images.spec.ts | 13 ++++----- src/core/task/Task.ts | 27 ++++++++++--------- src/core/task/__tests__/Task.spec.ts | 9 ++++--- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts index 0aa3f08bb1..253a9ffe1b 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts @@ -74,13 +74,14 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = return false } mockTask.userMessageContent.push(toolResult) - const images = mockTask.userMessageContent.filter((block: UserContentBlock) => block.type === "image") - if (images.length > 0) { - const withoutImages = mockTask.userMessageContent.filter( - (block: UserContentBlock) => block.type !== "image", + const results = mockTask.userMessageContent.filter( + (block: UserContentBlock) => block.type === "tool_result", + ) + if (results.length !== mockTask.userMessageContent.length) { + const others = mockTask.userMessageContent.filter( + (block: UserContentBlock) => block.type !== "tool_result", ) - withoutImages.push(...images) - mockTask.userMessageContent.splice(0, mockTask.userMessageContent.length, ...withoutImages) + mockTask.userMessageContent.splice(0, mockTask.userMessageContent.length, ...results, ...others) } return true }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index a5e44e261a..bdad69c21c 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -469,11 +469,12 @@ export class Task extends EventEmitter implements TaskLike { * Push a tool_result block to userMessageContent, preventing duplicates. * Duplicate tool_use_ids cause API errors. * - * Keeps all tool_result blocks contiguous, as the Anthropic API requires: - * image blocks attached to earlier tool results must not sit between two - * tool_results ("tool_use ids were found without tool_result blocks - * immediately after"), so any images preceding the new result are moved - * after it. + * Keeps all tool_result blocks first and contiguous, as the Anthropic API + * requires: a user message responding to tool_use blocks must begin with + * the tool_result blocks, and any other content (images, text) between or + * ahead of them is rejected ("tool_use ids were found without tool_result + * blocks immediately after"). All tool_result blocks are therefore kept + * ahead of other content in this array. * * @param toolResult - The tool_result block to add * @returns true if added, false if duplicate was skipped @@ -491,15 +492,15 @@ export class Task extends EventEmitter implements TaskLike { } this.userMessageContent.push(toolResult) - // The new result is always appended at the end, so every image block - // currently in the array precedes it and would split the tool_result - // run once another result arrives. Move them after the new result. - const images = this.userMessageContent.filter( - (block): block is Anthropic.ImageBlockParam => block.type === "image", + // Partition into [tool_results, other blocks], preserving order within + // each group, so results stay first and contiguous regardless of what + // other content (images, text) arrived between pushes. + const results = this.userMessageContent.filter( + (block): block is Anthropic.ToolResultBlockParam => block.type === "tool_result", ) - if (images.length > 0) { - const withoutImages = this.userMessageContent.filter((block) => block.type !== "image") - this.userMessageContent.splice(0, this.userMessageContent.length, ...withoutImages, ...images) + if (results.length !== this.userMessageContent.length) { + const others = this.userMessageContent.filter((block) => block.type !== "tool_result") + this.userMessageContent.splice(0, this.userMessageContent.length, ...results, ...others) } return true diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 472ea7f23d..63abfa8c4b 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6295,10 +6295,11 @@ describe("pushToolResultToUserContent", () => { expect(added).toBe(true) expect(task.userMessageContent).toHaveLength(3) - // The image must not sit between two tool_results, so it moves after - // the new result; other blocks keep their relative order. - expect(task.userMessageContent[0].type).toBe("text") - expect(task.userMessageContent[1]).toEqual(toolResult) + // Results must lead the message (Anthropic rejects other content ahead + // of or between tool_results), so the pre-existing text and image move + // after the result; blocks keep their relative order within each group. + expect(task.userMessageContent[0]).toEqual(toolResult) + expect(task.userMessageContent[1].type).toBe("text") expect(task.userMessageContent[2]).toEqual(image) }) From 73038d0219bae1ce57a779bf92ac882654cc8d0f Mon Sep 17 00:00:00 2001 From: myk1yt Date: Fri, 30 Oct 2026 09:53:52 +0900 Subject: [PATCH 3/4] test(anthropic): bind the real tool_result grouping and cover mixed interleave Addresses edelauna's two review asks on #1705. 1. presentAssistantMessage-images.spec.ts bound a hand-copied partition of Task.pushToolResultToUserContent, so the #1307 tests were tautological: deleting the grouping from Task.ts still passed them. The mock now binds the real Task.prototype.pushToolResultToUserContent via vi.importActual. Verified: removing the partition from Task.ts fails both #1307 tests. 2. Task.spec.ts only covered "images after the first result". Added the mixed interleave [image, text, r1, text, r2] -> [r1, r2, image, text, text2], which fails under an image-only grouping and only passes with the full tool_result-first partition. Also carries main's new policySnapshot argument at the 2 integration call sites, and drops this spec's no-explicit-any suppressions from 16 to 14. --- .../presentAssistantMessage-images.spec.ts | 48 +++++++++---------- src/core/task/__tests__/Task.spec.ts | 36 ++++++++++++++ src/eslint-suppressions.json | 2 +- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts index 01fe92e502..f18d6f829b 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts @@ -1,6 +1,7 @@ // npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts -import { describe, it, expect, beforeEach, vi } from "vitest" +import { describe, it, expect, beforeEach, beforeAll, vi } from "vitest" +import type { Anthropic } from "@anthropic-ai/sdk" import { presentAssistantMessage } from "../presentAssistantMessage" import { validateToolUse } from "../../tools/validateToolUse" import type { RequestPolicySnapshot } from "../../prompts/tools/effective-tool-policy" @@ -33,6 +34,21 @@ const baseSnapshot: RequestPolicySnapshot = { customModes: [], } +// The real Task.prototype.pushToolResultToUserContent, imported past the +// module-level automock. Binding the real implementation (rather than a copy of +// its partition logic) is what makes the #1307 tests non-tautological: deleting +// the grouping from Task.ts makes these tests fail. +type RealPushToolResult = ( + this: { userMessageContent: UserContentBlock[] }, + toolResult: Anthropic.ToolResultBlockParam, +) => boolean +let realPushToolResultToUserContent: RealPushToolResult + +beforeAll(async () => { + const actual = await vi.importActual("../../task/Task") + realPushToolResultToUserContent = actual.Task.prototype.pushToolResultToUserContent +}) + describe("presentAssistantMessage - Image Handling in Native Tool Calling", () => { let mockTask: any @@ -76,27 +92,11 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } - // Mirrors Task.pushToolResultToUserContent: duplicate guard plus keeping - // tool_result blocks contiguous by moving image blocks after the new result. - mockTask.pushToolResultToUserContent = vi.fn().mockImplementation((toolResult: any) => { - const existingResult = mockTask.userMessageContent.find( - (block: any) => block.type === "tool_result" && block.tool_use_id === toolResult.tool_use_id, - ) - if (existingResult) { - return false - } - mockTask.userMessageContent.push(toolResult) - const results = mockTask.userMessageContent.filter( - (block: UserContentBlock) => block.type === "tool_result", - ) - if (results.length !== mockTask.userMessageContent.length) { - const others = mockTask.userMessageContent.filter( - (block: UserContentBlock) => block.type !== "tool_result", - ) - mockTask.userMessageContent.splice(0, mockTask.userMessageContent.length, ...results, ...others) - } - return true - }) + // Bind the REAL Task.prototype method to this structural double: it only + // reads and rewrites `userMessageContent`, which the double provides. + mockTask.pushToolResultToUserContent = vi.fn((toolResult: Anthropic.ToolResultBlockParam) => + realPushToolResultToUserContent.call(mockTask, toolResult), + ) }) it("should preserve images in tool_result for native tool calling", async () => { @@ -378,7 +378,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = }) }) - await presentAssistantMessage(mockTask) + await presentAssistantMessage(mockTask, baseSnapshot) // Anthropic rejects user messages where image blocks sit between // tool_result blocks, so images from both tools must be grouped @@ -439,7 +439,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = return Promise.resolve({ response: "yesButtonClicked", text: "answer b", images: undefined }) }) - await presentAssistantMessage(mockTask) + await presentAssistantMessage(mockTask, baseSnapshot) expect(mockTask.userMessageContent.map((block: UserContentBlock) => block.type)).toEqual([ "tool_result", diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 0a92793b0d..ba23073eae 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -8329,4 +8329,40 @@ describe("pushToolResultToUserContent", () => { expect(task.userMessageContent[2]).toEqual(image1) expect(task.userMessageContent[3]).toEqual(image2) }) + + it("moves text as well as images behind the tool_results, preserving order within each group", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const toolResult1: Anthropic.ToolResultBlockParam = { + type: "tool_result", + tool_use_id: "id-1", + content: "Result 1", + } + const toolResult2: Anthropic.ToolResultBlockParam = { + type: "tool_result", + tool_use_id: "id-2", + content: "Result 2", + } + const image: Anthropic.ImageBlockParam = { + type: "image", + source: { type: "base64", media_type: "image/png", data: "img1" }, + } + const text1: Anthropic.TextBlockParam = { type: "text", text: "text one" } + const text2: Anthropic.TextBlockParam = { type: "text", text: "text two" } + + // Interleaved arrival: image, text, r1, more text, r2. Grouping only + // images would leave text1 ahead of r1, which Anthropic rejects; every + // non-tool_result block must move behind the contiguous result run. + task.userMessageContent.push(image, text1) + task.pushToolResultToUserContent(toolResult1) + task.userMessageContent.push(text2) + task.pushToolResultToUserContent(toolResult2) + + expect(task.userMessageContent).toEqual([toolResult1, toolResult2, image, text1, text2]) + }) }) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 8a9e1fa4df..00a3db4ee1 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -571,7 +571,7 @@ }, "core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 16 + "count": 14 } }, "core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts": { From 163a3644640b12b4922b9102b97c12bf7bb60301 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sun, 1 Nov 2026 03:35:09 +0000 Subject: [PATCH 4/4] refactor(task): drop redundant guard in pushToolResultToUserContent --- .coderabbit.yaml | 13 +++++++++++++ src/core/task/Task.ts | 6 ++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 620c466331..48236896ac 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -96,6 +96,19 @@ reviews: a call-count check. For code with fallback behavior, verify both the cold-start case (no prior state) and the warm case (prior state exists) are covered, as they exercise different branches. + A test double must not re-implement production logic. If a mock, stub, or inline helper + copies a predicate, filter, sort, or transform that also exists in production code, a + change to the production copy cannot fail the test. Bind the real function instead, for + example with `vi.importActual` or `vi.spyOn`, and replace only the external boundary + (network, filesystem, VS Code API). + For code that filters, groups, reorders, or deduplicates a collection, require a test + with mixed input, such as interleaved item types and repeated items, that asserts the + full output array. A test that uses only one item type cannot fail when the production + predicate becomes narrower. Ask for the case that fails when the predicate covers a + subset of the intended items. + When Stryker reports a surviving mutant on changed code, prefer a test that kills it. + If the mutant is equivalent, prefer to delete the redundant branch before adding a + `Stryker disable` comment. - path: "apps/vscode-e2e/**" instructions: >- diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 594f632b59..dd86cc7d44 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -607,10 +607,8 @@ export class Task extends EventEmitter implements TaskLike { const results = this.userMessageContent.filter( (block): block is Anthropic.ToolResultBlockParam => block.type === "tool_result", ) - if (results.length !== this.userMessageContent.length) { - const others = this.userMessageContent.filter((block) => block.type !== "tool_result") - this.userMessageContent.splice(0, this.userMessageContent.length, ...results, ...others) - } + const others = this.userMessageContent.filter((block) => block.type !== "tool_result") + this.userMessageContent.splice(0, this.userMessageContent.length, ...results, ...others) return true }