Skip to content

Commit 148b5a5

Browse files
luiggibcnclaude
andcommitted
test: add coverage for sanitizeReasoningFromMessages (#1988)
- Non-Claude provider: strips reasoning parts, preserves text parts - Claude provider: passes reasoning parts through unchanged - redacted-reasoning parts stripped for non-Claude providers - String content messages unaffected regardless of provider Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aceeb32 commit 148b5a5

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

apps/desktop/src/main/ai/session/__tests__/runner.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,124 @@ describe('runAgentSession', () => {
312312
const callArgs = mockStreamText.mock.calls[0][0];
313313
expect(callArgs.stopWhen).toEqual({ type: 'stepCount', count: 500 });
314314
});
315+
316+
// ===========================================================================
317+
// Reasoning / thinking_blocks sanitization (#1988)
318+
// ===========================================================================
319+
320+
it('should strip reasoning parts from messages when provider is not Claude', async () => {
321+
mockStreamText.mockReturnValue(
322+
createMockStreamResult([], { text: '', totalUsage: { inputTokens: 0, outputTokens: 0 } }),
323+
);
324+
325+
const messagesWithReasoning = [
326+
{ role: 'user' as const, content: 'Hello' },
327+
// Simulate an assistant message that contains reasoning parts (e.g. from a prior Claude session)
328+
{
329+
role: 'assistant' as const,
330+
content: [
331+
{ type: 'reasoning', text: 'Let me think…' },
332+
{ type: 'text', text: 'The answer is 42.' },
333+
] as unknown as string,
334+
},
335+
];
336+
337+
// Non-Claude model — reasoning parts must be stripped
338+
await runAgentSession(
339+
createMockConfig({
340+
model: 'gpt-4o' as any,
341+
initialMessages: messagesWithReasoning,
342+
}),
343+
);
344+
345+
const { messages } = mockStreamText.mock.calls[0][0];
346+
const assistantMsg = messages.find((m: { role: string }) => m.role === 'assistant');
347+
expect(assistantMsg).toBeDefined();
348+
// Content should be an array with only the text part
349+
expect(Array.isArray(assistantMsg.content)).toBe(true);
350+
const parts = assistantMsg.content as Array<{ type: string }>;
351+
expect(parts.every((p) => p.type !== 'reasoning')).toBe(true);
352+
expect(parts.some((p) => p.type === 'text')).toBe(true);
353+
});
354+
355+
it('should preserve reasoning parts when provider is Claude', async () => {
356+
mockStreamText.mockReturnValue(
357+
createMockStreamResult([], { text: '', totalUsage: { inputTokens: 0, outputTokens: 0 } }),
358+
);
359+
360+
const messagesWithReasoning = [
361+
{ role: 'user' as const, content: 'Hello' },
362+
{
363+
role: 'assistant' as const,
364+
content: [
365+
{ type: 'reasoning', text: 'Let me think…' },
366+
{ type: 'text', text: 'The answer is 42.' },
367+
] as unknown as string,
368+
},
369+
];
370+
371+
// Claude model — reasoning parts must be passed through unchanged
372+
await runAgentSession(
373+
createMockConfig({
374+
model: 'claude-sonnet-4-5' as any,
375+
initialMessages: messagesWithReasoning,
376+
}),
377+
);
378+
379+
const { messages } = mockStreamText.mock.calls[0][0];
380+
const assistantMsg = messages.find((m: { role: string }) => m.role === 'assistant');
381+
expect(assistantMsg).toBeDefined();
382+
expect(Array.isArray(assistantMsg.content)).toBe(true);
383+
const parts = assistantMsg.content as Array<{ type: string }>;
384+
expect(parts.some((p) => p.type === 'reasoning')).toBe(true);
385+
});
386+
387+
it('should handle redacted-reasoning parts the same as reasoning for non-Claude providers', async () => {
388+
mockStreamText.mockReturnValue(
389+
createMockStreamResult([], { text: '', totalUsage: { inputTokens: 0, outputTokens: 0 } }),
390+
);
391+
392+
const messagesWithRedacted = [
393+
{ role: 'user' as const, content: 'Hello' },
394+
{
395+
role: 'assistant' as const,
396+
content: [
397+
{ type: 'redacted-reasoning', data: 'encrypted…' },
398+
{ type: 'text', text: 'Done.' },
399+
] as unknown as string,
400+
},
401+
];
402+
403+
await runAgentSession(
404+
createMockConfig({
405+
model: 'gpt-4o' as any,
406+
initialMessages: messagesWithRedacted,
407+
}),
408+
);
409+
410+
const { messages } = mockStreamText.mock.calls[0][0];
411+
const assistantMsg = messages.find((m: { role: string }) => m.role === 'assistant');
412+
const parts = assistantMsg.content as Array<{ type: string }>;
413+
expect(parts.every((p) => p.type !== 'redacted-reasoning')).toBe(true);
414+
});
415+
416+
it('should not modify string content messages regardless of provider', async () => {
417+
mockStreamText.mockReturnValue(
418+
createMockStreamResult([], { text: '', totalUsage: { inputTokens: 0, outputTokens: 0 } }),
419+
);
420+
421+
await runAgentSession(
422+
createMockConfig({
423+
model: 'gpt-4o' as any,
424+
initialMessages: [
425+
{ role: 'user', content: 'Hello' },
426+
{ role: 'assistant', content: 'Hi there.' },
427+
],
428+
}),
429+
);
430+
431+
const { messages } = mockStreamText.mock.calls[0][0];
432+
const assistantMsg = messages.find((m: { role: string }) => m.role === 'assistant');
433+
expect(assistantMsg.content).toBe('Hi there.');
434+
});
315435
});

0 commit comments

Comments
 (0)