Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 190 additions & 1 deletion apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3617,6 +3617,77 @@ describe("createAgentChatService", () => {
]);
});

it("keeps Codex reasoning deltas tied to the active turn and thinking activity", async () => {
const events: AgentChatEventEnvelope[] = [];
const { service } = createService({
onEvent: (event: AgentChatEventEnvelope) => {
events.push(event);
},
});

const session = await service.createSession({
laneId: "lane-1",
provider: "codex",
model: "gpt-5.4",
});

await service.sendMessage({
sessionId: session.id,
text: "Think through the options.",
}, { awaitDispatch: true });

await waitForEvent(
events,
(event): event is AgentChatEventEnvelope =>
event.event.type === "status"
&& event.event.turnStatus === "started"
&& event.event.turnId === "turn-1",
);

const eventsBeforeReasoningDelta = events.length;
mockState.emitCodexPayload({
jsonrpc: "2.0",
method: "item/reasoning/summaryTextDelta",
params: {
itemId: "reasoning-1",
delta: "Checking the relevant paths.",
},
});

await waitForEvent(
events,
(event): event is AgentChatEventEnvelope =>
event.event.type === "reasoning"
&& event.event.turnId === "turn-1"
&& event.event.itemId === "reasoning-1",
);

const newEvents = events.slice(eventsBeforeReasoningDelta);
// The reasoning row must be produced by the post-delta boundary, not by
// any earlier turn-start bookkeeping.
expect(newEvents).toEqual(expect.arrayContaining([
expect.objectContaining({
event: expect.objectContaining({
type: "reasoning",
text: "Checking the relevant paths.",
itemId: "reasoning-1",
turnId: "turn-1",
}),
}),
]));
// And somewhere in the turn — coalescing across the initial turn-start
// activity is fine — a thinking activity must be tied to the same turn.
expect(events).toEqual(expect.arrayContaining([
expect.objectContaining({
event: expect.objectContaining({
type: "activity",
activity: "thinking",
turnId: "turn-1",
}),
}),
]));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it("ignores unsolicited Codex turn notifications when no turn is active", async () => {
const events: Array<{ type: string; turnId?: string; text?: string }> = [];
const { service } = createService({
Expand Down Expand Up @@ -6581,6 +6652,115 @@ describe("createAgentChatService", () => {
await sendPromise;
});

it("does not duplicate Claude thinking when the final assistant message repeats streamed content", async () => {
const events: AgentChatEventEnvelope[] = [];
const setPermissionMode = vi.fn().mockResolvedValue(undefined);
const send = vi.fn().mockResolvedValue(undefined);
let streamCall = 0;
let reasoningCountAfterDelta = -1;

const stream = vi.fn(() => (async function* () {
streamCall += 1;
if (streamCall === 1) {
yield {
type: "system",
subtype: "init",
session_id: "sdk-session-thinking",
slash_commands: [],
};
return;
}

yield {
type: "stream_event",
event: {
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "" },
},
};
yield {
type: "stream_event",
event: {
type: "content_block_delta",
index: 0,
delta: {
type: "thinking_delta",
thinking: "Checking both imports before editing.",
},
},
};
await new Promise((resolve) => setTimeout(resolve, 0));
reasoningCountAfterDelta = events.filter((event) => event.event.type === "reasoning").length;
yield {
type: "assistant",
message: {
content: [{ type: "thinking", thinking: "Checking both imports before editing." }],
usage: { input_tokens: 1, output_tokens: 1 },
},
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
yield {
type: "result",
usage: { input_tokens: 1, output_tokens: 1 },
};
})());

vi.mocked(unstable_v2_createSession).mockReturnValue({
send,
stream,
close: vi.fn(),
sessionId: "sdk-session-thinking",
setPermissionMode,
} as any);

const { service } = createService({
onEvent: (event: AgentChatEventEnvelope) => events.push(event),
});

const session = await service.createSession({
laneId: "lane-1",
provider: "claude",
model: "claude-sonnet-4-6",
modelId: "anthropic/claude-sonnet-4-6",
});

await service.runSessionTurn({
sessionId: session.id,
text: "Resolve the PR comments.",
});

const reasoningEvents = events
.map((event) => event.event)
.filter((event): event is Extract<AgentChatEventEnvelope["event"], { type: "reasoning" }> => event.type === "reasoning");
expect(reasoningEvents.map((event) => event.text)).toEqual(["Checking both imports before editing."]);
// The streamed thinking_delta must be what created the reasoning row — not the
// final assistant message (which would also produce a row if dedupe broke).
expect(reasoningCountAfterDelta).toBe(1);
expect(events.some((event) => event.event.type === "activity" && event.event.activity === "thinking")).toBe(true);
const sessionOpts = vi.mocked(unstable_v2_createSession).mock.calls[0]?.[0] as {
executableArgs?: string[];
settings?: Record<string, unknown>;
} | undefined;
expect(sessionOpts?.settings).toEqual(expect.objectContaining({
showThinkingSummaries: true,
alwaysThinkingEnabled: true,
}));
expect(sessionOpts?.executableArgs).toEqual(expect.arrayContaining([
"--include-partial-messages",
"--thinking",
"adaptive",
"--thinking-display",
"summarized",
]));
const settingsArgIndex = sessionOpts?.executableArgs?.indexOf("--settings") ?? -1;
expect(settingsArgIndex).toBeGreaterThanOrEqual(0);
const settingsJson = sessionOpts?.executableArgs?.[settingsArgIndex + 1];
expect(JSON.parse(String(settingsJson))).toEqual(expect.objectContaining({
showThinkingSummaries: true,
alwaysThinkingEnabled: true,
}));
});

it("emits completed Claude tool_result rows when tool_use_summary arrives", async () => {
const events: AgentChatEventEnvelope[] = [];
const setPermissionMode = vi.fn().mockResolvedValue(undefined);
Expand Down Expand Up @@ -7292,9 +7472,18 @@ describe("createAgentChatService", () => {
expect(
events.filter((event) => event.event.type === "user_message"),
).toHaveLength(1);
const startedEvent = events.find(
(event): event is AgentChatEventEnvelope & {
event: Extract<AgentChatEventEnvelope["event"], { type: "status" }>;
} => event.event.type === "status" && event.event.turnStatus === "started",
);
expect(startedEvent).toBeTruthy();
expect(
events.some(
(event) => event.event.type === "status" && event.event.turnStatus === "started",
(event) =>
event.event.type === "activity"
&& event.event.activity === "thinking"
&& event.event.turnId === startedEvent!.event.turnId,
),
).toBe(true);

Expand Down
Loading
Loading