From 290af8018670c868b19ba147f3db2e713847a101 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 22:55:29 +0200 Subject: [PATCH] fix: simplify excluded custom message tests --- packages/agent/test/agent.test.ts | 78 ++++-------- .../agent/test/harness/compaction.test.ts | 94 +++------------ .../test/branch-summarization.test.ts | 47 -------- packages/coding-agent/test/compaction.test.ts | 90 ++++---------- .../agent-session-bash-persistence.test.ts | 77 +++++------- .../test/suite/agent-session-queue.test.ts | 114 +++++++----------- .../suite/agent-session-retry-events.test.ts | 31 ++--- 7 files changed, 151 insertions(+), 380 deletions(-) delete mode 100644 packages/coding-agent/test/branch-summarization.test.ts diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 16675a38f..18f0d91cd 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -560,10 +560,16 @@ describe("Agent", () => { await firstPrompt.catch(() => {}); }); - it("continue() should reserve the active run while awaiting async LLM conversion", async () => { + it("continue() should reserve the active run before async LLM conversion and use the filtered context tail", async () => { const convertStarted = createDeferred(); const releaseConvert = createDeferred(); let convertCallCount = 0; + let providerMessages: Message[] = []; + const displayOnlyMessage = { + role: "displayOnly", + content: "status", + timestamp: Date.now(), + } as unknown as AgentMessage; const agent = new Agent({ convertToLlm: async (messages) => { convertCallCount++; @@ -571,11 +577,14 @@ describe("Agent", () => { convertStarted.resolve(); await releaseConvert.promise; } - return messages.filter( - (message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult", - ) as Message[]; + return messages + .filter((message) => (message as { role: string }).role !== "displayOnly") + .filter( + (message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult", + ) as Message[]; }, - streamFn: () => { + streamFn: (_model, context) => { + providerMessages = context.messages; const stream = new MockAssistantStream(); queueMicrotask(() => { stream.push({ type: "done", reason: "stop", message: createAssistantMessage("Processed") }); @@ -587,9 +596,16 @@ describe("Agent", () => { { role: "user", content: [{ type: "text", text: "Initial" }], - timestamp: Date.now(), + timestamp: Date.now() - 10, }, + createAssistantMessage("Initial response"), + displayOnlyMessage, ]; + agent.followUp({ + role: "user", + content: [{ type: "text", text: "Queued follow-up" }], + timestamp: Date.now(), + }); const continuePromise = agent.continue(); await convertStarted.promise; @@ -601,6 +617,8 @@ describe("Agent", () => { await continuePromise; expect(convertCallCount).toBe(2); + expect(providerMessages[providerMessages.length - 1]?.role).toBe("user"); + expect(agent.state.messages).toContain(displayOnlyMessage); }); it("continue() should process queued follow-up messages after an assistant turn", async () => { @@ -641,54 +659,6 @@ describe("Agent", () => { expect(agent.state.messages[agent.state.messages.length - 1].role).toBe("assistant"); }); - it("continue() should process queued follow-up messages after filtered state messages", async () => { - let providerCallCount = 0; - let providerMessages: Message[] = []; - const displayOnlyMessage = { - role: "displayOnly", - content: "status", - timestamp: Date.now(), - } as unknown as AgentMessage; - const agent = new Agent({ - convertToLlm: (messages) => - messages - .filter((message) => (message as { role: string }).role !== "displayOnly") - .filter( - (message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult", - ) as Message[], - streamFn: (_model, context) => { - providerCallCount++; - providerMessages = context.messages; - const stream = new MockAssistantStream(); - queueMicrotask(() => { - stream.push({ type: "done", reason: "stop", message: createAssistantMessage("Processed") }); - }); - return stream; - }, - }); - - agent.state.messages = [ - { - role: "user", - content: [{ type: "text", text: "Initial" }], - timestamp: Date.now() - 10, - }, - createAssistantMessage("Initial response"), - displayOnlyMessage, - ]; - agent.followUp({ - role: "user", - content: [{ type: "text", text: "Queued follow-up" }], - timestamp: Date.now(), - }); - - await agent.continue(); - - expect(providerCallCount).toBe(1); - expect(providerMessages[providerMessages.length - 1]?.role).toBe("user"); - expect(agent.state.messages).toContain(displayOnlyMessage); - }); - it("continue() should keep one-at-a-time steering semantics from assistant tail", async () => { let responseCount = 0; const agent = new Agent({ diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index 7877bf56e..19826e013 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -234,42 +234,6 @@ describe("harness compaction", () => { isSplitTurn: false, }); - const cutUser = createMessageEntry(createUserMessage("inspect file")); - const cutAssistant = createMessageEntry( - { - ...createAssistantMessage("calling tool"), - content: [{ type: "toolCall", id: "call-2", name: "read", arguments: { path: "file.ts" } }], - }, - cutUser.id, - ); - const cutToolResult = createMessageEntry( - { - role: "toolResult", - toolCallId: "call-2", - toolName: "read", - content: [{ type: "text", text: "x".repeat(1000) }], - isError: false, - timestamp: Date.now(), - }, - cutAssistant.id, - ); - const excludedCustom: CustomMessageEntry = { - type: "custom_message", - id: createId(), - parentId: cutToolResult.id, - timestamp: new Date().toISOString(), - customType: "status", - content: "tool finished", - display: true, - excludeFromContext: true, - }; - const cutAssistantFinal = createMessageEntry(createAssistantMessage("done"), excludedCustom.id); - expect(findCutPoint([cutUser, cutAssistant, cutToolResult, excludedCustom, cutAssistantFinal], 0, 5, 2)).toEqual({ - firstKeptEntryIndex: 4, - turnStartIndex: 0, - isSplitTurn: true, - }); - const user = createMessageEntry(createUserMessage("user")); const compaction = createCompactionEntry("summary", user.id, user.id); const assistant = createMessageEntry(createAssistantMessage("assistant"), compaction.id); @@ -425,8 +389,8 @@ describe("harness compaction", () => { expect([...preparation!.fileOps.written]).toContain("written.ts"); }); - it("skips excluded custom messages during compaction token estimates", () => { - const customMessage: CustomMessageEntry = { + it("ignores excluded custom messages during compaction and branch-summary preparation", () => { + const excludedCustom: CustomMessageEntry = { type: "custom_message", id: createId(), parentId: null, @@ -436,26 +400,27 @@ describe("harness compaction", () => { display: true, excludeFromContext: true, }; - const user = createMessageEntry(createUserMessage("keep"), customMessage.id); + const user = createMessageEntry(createUserMessage("keep"), excludedCustom.id); - const preparation = getOrThrow( - prepareCompaction([customMessage, user], { enabled: true, reserveTokens: 0, keepRecentTokens: 1 }), + const simplePreparation = getOrThrow( + prepareCompaction([excludedCustom, user], { enabled: true, reserveTokens: 0, keepRecentTokens: 1 }), ); + const branchPreparation = prepareBranchEntries([excludedCustom, user], 10); - expect(preparation?.tokensBefore).toBe(1); - expect(preparation?.messagesToSummarize).toEqual([]); - }); + expect(simplePreparation?.tokensBefore).toBe(1); + expect(simplePreparation?.messagesToSummarize).toEqual([]); + expect(branchPreparation.messages.map((message) => message.role)).toEqual(["user"]); + expect(branchPreparation.totalTokens).toBe(1); - it("ignores excluded custom messages when finding split-turn prefixes", () => { - const user = createMessageEntry(createUserMessage("inspect file")); + const splitUser = createMessageEntry(createUserMessage("inspect file")); const assistantWithToolCall = createMessageEntry( { ...createAssistantMessage("calling tool"), content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "file.ts" } }], }, - user.id, + splitUser.id, ); - const customMessage: CustomMessageEntry = { + const splitExcludedCustom: CustomMessageEntry = { type: "custom_message", id: createId(), parentId: assistantWithToolCall.id, @@ -474,47 +439,26 @@ describe("harness compaction", () => { isError: false, timestamp: Date.now(), }, - customMessage.id, + splitExcludedCustom.id, ); const assistantFinal = createMessageEntry(createAssistantMessage("done"), toolResult.id); - - const preparation = getOrThrow( - prepareCompaction([user, assistantWithToolCall, customMessage, toolResult, assistantFinal], { + const splitPreparation = getOrThrow( + prepareCompaction([splitUser, assistantWithToolCall, splitExcludedCustom, toolResult, assistantFinal], { enabled: true, reserveTokens: 0, keepRecentTokens: 1, }), ); - expect(preparation).toBeDefined(); - expect(preparation?.isSplitTurn).toBe(true); - expect(preparation?.firstKeptEntryId).toBe(assistantFinal.id); - expect(preparation?.turnPrefixMessages.map((message) => message.role)).toEqual([ + expect(splitPreparation?.isSplitTurn).toBe(true); + expect(splitPreparation?.firstKeptEntryId).toBe(assistantFinal.id); + expect(splitPreparation?.turnPrefixMessages.map((message) => message.role)).toEqual([ "user", "assistant", "toolResult", ]); }); - it("skips excluded custom messages before branch summary token budgeting", () => { - const user = createMessageEntry(createUserMessage("keep")); - const customMessage: CustomMessageEntry = { - type: "custom_message", - id: createId(), - parentId: user.id, - timestamp: new Date().toISOString(), - customType: "status", - content: "x".repeat(1000), - display: true, - excludeFromContext: true, - }; - - const preparation = prepareBranchEntries([user, customMessage], 10); - - expect(preparation.messages.map((message) => message.role)).toEqual(["user"]); - expect(preparation.totalTokens).toBe(1); - }); - it("prepares custom and branch summary entries for summarization", () => { const branchSummary: BranchSummaryEntry = { type: "branch_summary", diff --git a/packages/coding-agent/test/branch-summarization.test.ts b/packages/coding-agent/test/branch-summarization.test.ts deleted file mode 100644 index 8050ddac9..000000000 --- a/packages/coding-agent/test/branch-summarization.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { prepareBranchEntries } from "../src/core/compaction/branch-summarization.ts"; -import type { CustomMessageEntry, SessionMessageEntry } from "../src/core/session-manager.ts"; - -function userEntry(id: string, parentId: string | null, text: string): SessionMessageEntry { - return { - type: "message", - id, - parentId, - timestamp: "2025-01-01T00:00:00Z", - message: { - role: "user", - content: [{ type: "text", text }], - timestamp: 1, - }, - }; -} - -function customEntry( - id: string, - parentId: string | null, - content: string, - excludeFromContext: boolean, -): CustomMessageEntry { - return { - type: "custom_message", - id, - parentId, - timestamp: "2025-01-01T00:00:00Z", - customType: "status", - content, - display: true, - excludeFromContext, - }; -} - -describe("branch summarization", () => { - it("skips excluded custom messages before token budgeting", () => { - const user = userEntry("user", null, "keep"); - const excluded = customEntry("custom", user.id, "x".repeat(1000), true); - - const preparation = prepareBranchEntries([user, excluded], 10); - - expect(preparation.messages.map((message) => message.role)).toEqual(["user"]); - expect(preparation.totalTokens).toBe(1); - }); -}); diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index b709e8df2..3a9294aa8 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -4,6 +4,7 @@ import { getModel } from "@earendil-works/pi-ai"; import { readFileSync } from "fs"; import { join } from "path"; import { beforeEach, describe, expect, it } from "vitest"; +import { prepareBranchEntries } from "../src/core/compaction/branch-summarization.ts"; import { type CompactionSettings, calculateContextTokens, @@ -15,7 +16,6 @@ import { prepareCompaction, shouldCompact, } from "../src/core/compaction/index.ts"; -import type { CustomMessage } from "../src/core/messages.ts"; import { buildSessionContext, type CompactionEntry, @@ -202,27 +202,6 @@ describe("Token calculation", () => { const usage = createMockUsage(0, 0, 0, 0); expect(calculateContextTokens(usage)).toBe(0); }); - - it("should ignore excluded custom messages in context token estimates", () => { - const excludedCustom: CustomMessage = { - role: "custom", - customType: "status", - content: "x".repeat(1000), - display: true, - excludeFromContext: true, - timestamp: Date.now(), - }; - const visibleCustom: CustomMessage = { ...excludedCustom, excludeFromContext: false }; - const assistant = createAssistantMessage("assistant", createMockUsage(10, 5)); - - expect(estimateContextTokens([excludedCustom])).toMatchObject({ tokens: 0, trailingTokens: 0 }); - expect(estimateContextTokens([visibleCustom]).tokens).toBeGreaterThan(0); - expect(estimateContextTokens([assistant, excludedCustom])).toMatchObject({ - tokens: 15, - usageTokens: 15, - trailingTokens: 0, - }); - }); }); describe("getLastAssistantUsage", () => { @@ -346,31 +325,6 @@ describe("findCutPoint", () => { expect(result.turnStartIndex).toBe(2); // Turn 2 starts at index 2 } }); - - it("should not select excluded custom messages as cut points", () => { - const user = createMessageEntry(createUserMessage("inspect file")); - const assistantWithToolCall = createMessageEntry({ - ...createAssistantMessage("calling tool"), - content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "file.ts" } }], - }); - const toolResult = createMessageEntry({ - role: "toolResult", - toolCallId: "call-1", - toolName: "read", - content: [{ type: "text", text: "x".repeat(1000) }], - isError: false, - timestamp: Date.now(), - }); - const excludedCustom = createCustomMessageEntry("tool finished", true); - const assistantFinal = createMessageEntry(createAssistantMessage("done")); - const entries = [user, assistantWithToolCall, toolResult, excludedCustom, assistantFinal]; - - const result = findCutPoint(entries, 0, entries.length, 2); - - expect(result.firstKeptEntryIndex).toBe(4); - expect(result.turnStartIndex).toBe(0); - expect(result.isSplitTurn).toBe(true); - }); }); describe("buildSessionContext", () => { @@ -460,27 +414,29 @@ describe("buildSessionContext", () => { }); describe("prepareCompaction with custom messages", () => { - it("should ignore excluded custom messages in token estimates and summarized messages", () => { + it("should ignore excluded custom messages in compaction and branch-summary preparation", () => { const excludedCustom = createCustomMessageEntry("x".repeat(1000), true); const user = createMessageEntry(createUserMessage("keep")); - const preparation = prepareCompaction([excludedCustom, user], { + const simplePreparation = prepareCompaction([excludedCustom, user], { enabled: true, reserveTokens: 0, keepRecentTokens: 1, }); + const branchPreparation = prepareBranchEntries([excludedCustom, user], 10); - expect(preparation).toBeDefined(); - expect(preparation!.tokensBefore).toBe(1); - expect(preparation!.messagesToSummarize).toEqual([]); - }); + expect(simplePreparation).toBeDefined(); + expect(simplePreparation!.tokensBefore).toBe(1); + expect(simplePreparation!.messagesToSummarize).toEqual([]); + expect(branchPreparation.messages.map((message) => message.role)).toEqual(["user"]); + expect(branchPreparation.totalTokens).toBe(1); - it("should ignore excluded custom messages when finding split-turn prefixes", () => { - const user = createMessageEntry(createUserMessage("inspect file")); + resetEntryCounter(); + const splitUser = createMessageEntry(createUserMessage("inspect file")); const assistantWithToolCall = createMessageEntry({ ...createAssistantMessage("calling tool"), content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "file.ts" } }], }); - const excludedCustom = createCustomMessageEntry("tool is running", true); + const splitExcludedCustom = createCustomMessageEntry("tool is running", true); const toolResult = createMessageEntry({ role: "toolResult", toolCallId: "call-1", @@ -490,17 +446,19 @@ describe("prepareCompaction with custom messages", () => { timestamp: Date.now(), }); const assistantFinal = createMessageEntry(createAssistantMessage("done")); + const splitPreparation = prepareCompaction( + [splitUser, assistantWithToolCall, splitExcludedCustom, toolResult, assistantFinal], + { + enabled: true, + reserveTokens: 0, + keepRecentTokens: 1, + }, + ); - const preparation = prepareCompaction([user, assistantWithToolCall, excludedCustom, toolResult, assistantFinal], { - enabled: true, - reserveTokens: 0, - keepRecentTokens: 1, - }); - - expect(preparation).toBeDefined(); - expect(preparation!.isSplitTurn).toBe(true); - expect(preparation!.firstKeptEntryId).toBe(assistantFinal.id); - expect(preparation!.turnPrefixMessages.map((message) => message.role)).toEqual([ + expect(splitPreparation).toBeDefined(); + expect(splitPreparation!.isSplitTurn).toBe(true); + expect(splitPreparation!.firstKeptEntryId).toBe(assistantFinal.id); + expect(splitPreparation!.turnPrefixMessages.map((message) => message.role)).toEqual([ "user", "assistant", "toolResult", diff --git a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts index 419364f03..d1112d404 100644 --- a/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts +++ b/packages/coding-agent/test/suite/agent-session-bash-persistence.test.ts @@ -145,41 +145,17 @@ describe("AgentSession bash and persistence characterization", () => { }; const harness = await createHarness({ tools: [echoTool] }); harnesses.push(harness); + let providerUserTexts: string[] = []; harness.setResponses([ - fauxAssistantMessage([fauxToolCall("echo", { text: "hello" })], { stopReason: "toolUse" }), + (context) => { + providerUserTexts = context.messages + .filter((message) => message.role === "user") + .map((message) => getMessageText(message)); + return fauxAssistantMessage([fauxToolCall("echo", { text: "hello" })], { stopReason: "toolUse" }); + }, fauxAssistantMessage("done"), ]); - await harness.session.sendCustomMessage({ - customType: "note", - content: "hello", - display: true, - details: { a: 1 }, - }); - await harness.session.prompt("start"); - - const entries = harness.sessionManager.getEntries(); - expect(entries.map((entry) => entry.type)).toEqual([ - "custom_message", - "message", - "message", - "message", - "message", - ]); - expect(harness.session.messages.map((message) => message.role)).toEqual([ - "custom", - "user", - "assistant", - "toolResult", - "assistant", - ]); - }); - - it("excludes flagged custom messages from LLM context while preserving them", async () => { - const harness = await createHarness(); - harnesses.push(harness); - let userTexts: string[] = []; - await harness.session.sendCustomMessage({ customType: "status", content: "status panel", @@ -187,26 +163,37 @@ describe("AgentSession bash and persistence characterization", () => { details: { a: 1 }, excludeFromContext: true, }); - harness.setResponses([ - (context) => { - userTexts = context.messages - .filter((message) => message.role === "user") - .map((message) => getMessageText(message)); - return fauxAssistantMessage("done"); - }, - ]); + await harness.session.sendCustomMessage({ + customType: "note", + content: "hello", + display: true, + details: { a: 1 }, + }); + await harness.session.prompt("start"); - await harness.session.prompt("next prompt"); - - expect(userTexts).toEqual(["next prompt"]); + expect(providerUserTexts).toEqual(["hello", "start"]); const entries = harness.sessionManager.getEntries(); - expect(entries[0]?.type).toBe("custom_message"); - if (entries[0]?.type !== "custom_message") return; - expect(entries[0].excludeFromContext).toBe(true); + expect(entries.map((entry) => entry.type)).toEqual([ + "custom_message", + "custom_message", + "message", + "message", + "message", + "message", + ]); + expect(entries[0]).toMatchObject({ type: "custom_message", excludeFromContext: true }); expect(harness.sessionManager.buildSessionContext().messages[0]).toMatchObject({ role: "custom", excludeFromContext: true, }); + expect(harness.session.messages.map((message) => message.role)).toEqual([ + "custom", + "custom", + "user", + "assistant", + "toolResult", + "assistant", + ]); }); it("does not emit message_end for bash execution messages", async () => { diff --git a/packages/coding-agent/test/suite/agent-session-queue.test.ts b/packages/coding-agent/test/suite/agent-session-queue.test.ts index 119b688fb..129f4c4bc 100644 --- a/packages/coding-agent/test/suite/agent-session-queue.test.ts +++ b/packages/coding-agent/test/suite/agent-session-queue.test.ts @@ -258,96 +258,68 @@ describe("AgentSession queue characterization", () => { expect(getAssistantTexts(harness)).toEqual(["", "original turn complete", "batched follow-up response"]); }); - it("records excluded custom messages with triggerTurn without starting a provider turn", async () => { - const harness = await createHarness(); - harnesses.push(harness); + it("records excluded custom messages immediately without starting provider turns", async () => { + const idleHarness = await createHarness(); + harnesses.push(idleHarness); let providerCalled = false; - harness.setResponses([ + idleHarness.setResponses([ () => { providerCalled = true; return fauxAssistantMessage("unexpected"); }, ]); - await harness.session.sendCustomMessage( + await idleHarness.session.sendCustomMessage( { customType: "status", content: "display only", display: true, details: {}, excludeFromContext: true }, { triggerTurn: true }, ); expect(providerCalled).toBe(false); - expect(harness.session.messages).toHaveLength(1); - expect(harness.session.messages[0]).toMatchObject({ + expect(idleHarness.session.messages[0]).toMatchObject({ role: "custom", customType: "status", excludeFromContext: true, }); - expect(harness.getPendingResponseCount()).toBe(1); - }); + expect(idleHarness.getPendingResponseCount()).toBe(1); - it("records excluded custom messages with deliverAs steer while streaming", async () => { - const waiting = await createWaitingHarness(); - const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting; - harnesses.push(harness); - let recordedBeforeRelease = false; + for (const deliverAs of ["steer", "followUp"] as const) { + const waiting = await createWaitingHarness(); + const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting; + harnesses.push(harness); + let providerCalledForQueuedMessage = false; + harness.setResponses([ + fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }), + fauxAssistantMessage("done"), + () => { + providerCalledForQueuedMessage = true; + return fauxAssistantMessage("unexpected queued turn"); + }, + ]); - harness.setResponses([ - fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }), - fauxAssistantMessage("done"), - ]); + await waitForToolStart; + await harness.session.sendCustomMessage( + { + customType: "status", + content: `${deliverAs} display only`, + display: true, + details: {}, + excludeFromContext: true, + }, + { deliverAs }, + ); + const recordedBeforeRelease = harness.session.messages.some( + (message) => message.role === "custom" && message.customType === "status", + ); + releaseToolExecution(); + await promptPromise; - await waitForToolStart; - await harness.session.sendCustomMessage( - { customType: "status", content: "steer display only", display: true, details: {}, excludeFromContext: true }, - { deliverAs: "steer" }, - ); - recordedBeforeRelease = harness.session.messages.some( - (message) => message.role === "custom" && message.customType === "status", - ); - releaseToolExecution(); - await promptPromise; - - expect(recordedBeforeRelease).toBe(true); - expect( - harness.session.messages.filter((message) => message.role === "custom" && message.customType === "status"), - ).toHaveLength(1); - }); - - it("records excluded custom messages with deliverAs followUp while streaming without starting another turn", async () => { - const waiting = await createWaitingHarness(); - const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting; - harnesses.push(harness); - let providerCalledForFollowUp = false; - let recordedBeforeRelease = false; - - harness.setResponses([ - fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }), - fauxAssistantMessage("done"), - () => { - providerCalledForFollowUp = true; - return fauxAssistantMessage("unexpected follow-up"); - }, - ]); - - await waitForToolStart; - await harness.session.sendCustomMessage( - { - customType: "status", - content: "follow-up display only", - display: true, - details: {}, - excludeFromContext: true, - }, - { deliverAs: "followUp" }, - ); - recordedBeforeRelease = harness.session.messages.some( - (message) => message.role === "custom" && message.customType === "status", - ); - releaseToolExecution(); - await promptPromise; - - expect(recordedBeforeRelease).toBe(true); - expect(providerCalledForFollowUp).toBe(false); - expect(harness.getPendingResponseCount()).toBe(1); + expect(recordedBeforeRelease).toBe(true); + expect( + harness.session.messages.filter((message) => message.role === "custom" && message.customType === "status"), + ).toHaveLength(1); + expect(providerCalledForQueuedMessage).toBe(false); + expect(harness.getPendingResponseCount()).toBe(1); + } }); it("persists excluded custom messages from message_end hooks after the triggering message", async () => { diff --git a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts index 073db9422..a8d3ae553 100644 --- a/packages/coding-agent/test/suite/agent-session-retry-events.test.ts +++ b/packages/coding-agent/test/suite/agent-session-retry-events.test.ts @@ -31,28 +31,6 @@ describe("AgentSession retry and event characterization", () => { }); it("retries after a transient error and succeeds", async () => { - const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } } }); - harnesses.push(harness); - const retryEvents: string[] = []; - harness.session.subscribe((event) => { - if (event.type === "auto_retry_start") retryEvents.push(`start:${event.attempt}`); - if (event.type === "auto_retry_end") retryEvents.push(`end:${event.success}`); - }); - - harness.setResponses([ - fauxAssistantMessage("", { stopReason: "error", errorMessage: "overloaded_error" }), - fauxAssistantMessage("recovered"), - ]); - - await harness.session.prompt("test"); - - expect(retryEvents).toEqual(["start:1", "end:true"]); - expect(harness.eventsOfType("agent_end").map((event) => event.willRetry)).toEqual([true, false]); - expect(harness.faux.state.callCount).toBe(2); - expect(harness.session.isRetrying).toBe(false); - }); - - it("retries when an excluded custom message follows the transient error", async () => { const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } }, extensionFactories: [ @@ -73,6 +51,12 @@ describe("AgentSession retry and event characterization", () => { ], }); harnesses.push(harness); + const retryEvents: string[] = []; + harness.session.subscribe((event) => { + if (event.type === "auto_retry_start") retryEvents.push(`start:${event.attempt}`); + if (event.type === "auto_retry_end") retryEvents.push(`end:${event.success}`); + }); + harness.setResponses([ fauxAssistantMessage("", { stopReason: "error", errorMessage: "overloaded_error" }), fauxAssistantMessage("recovered"), @@ -80,7 +64,10 @@ describe("AgentSession retry and event characterization", () => { await harness.session.prompt("test"); + expect(retryEvents).toEqual(["start:1", "end:true"]); + expect(harness.eventsOfType("agent_end").map((event) => event.willRetry)).toEqual([true, false]); expect(harness.faux.state.callCount).toBe(2); + expect(harness.session.isRetrying).toBe(false); expect( harness.session.messages.some((message) => message.role === "custom" && message.customType === "status"), ).toBe(true);