Skip to content

Commit 60d3acd

Browse files
test(condense): prove effort forwarding at the summarizer boundary
The runtime-effort suite mocked the summarizer, so an override's arrival at summarizeConversation was proven while its departure was not. Add a focused spec that drives the real summarizer with enough history to reach its request, and assert that the handler receives reasoningEffort when the caller forwards the override and does not receive the key when the caller forwards none. Deleting the metadata argument at the request site makes both cases fail, so the assertions have teeth. Merge the two describe("dispose") blocks in the runtime-effort suite. The second one claimed that disposal "restores" the settings-derived API configuration. It restores nothing: apiConfiguration is the profile-derived projection and never carries the override. Its unique assertions now sit in the first block, under a name that states what is checked - the override is cleared and the profile-derived configuration is left unchanged. Addresses review threads rt4474609722 and rt4474702904 on 8144b12. Test-only: no production file changed.
1 parent 8144b12 commit 60d3acd

2 files changed

Lines changed: 104 additions & 14 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// npx vitest run src/core/condense/__tests__/condense-effort-metadata.spec.ts
2+
//
3+
// DTE series 2/5 — the summarizer boundary. Task hands the task-local thinking-effort override
4+
// to summarizeConversation inside request metadata. This checks the other half of that hand-off:
5+
// the real summarizer forwards the metadata to the handler's createMessage instead of dropping
6+
// the effort. The Task-side tests cover Task-to-summarizer propagation only.
7+
8+
import { Anthropic } from "@anthropic-ai/sdk"
9+
import type { ModelInfo, ReasoningEffortExtended } from "@roo-code/types"
10+
import { TelemetryService } from "@roo-code/telemetry"
11+
12+
import type { ApiHandlerCreateMessageMetadata } from "../../../api"
13+
import { BaseProvider } from "../../../api/providers/base-provider"
14+
import { ApiStream, ApiStreamChunk } from "../../../api/transform/stream"
15+
import { ApiMessage } from "../../task-persistence/apiMessages"
16+
import { summarizeConversation } from "../index"
17+
18+
const OVERRIDE_EFFORT: ReasoningEffortExtended = "xhigh"
19+
const TASK_ID = "dte-condense-task"
20+
21+
class RecordingApiHandler extends BaseProvider {
22+
/** The metadata of the last request this handler was asked to make. */
23+
public requestedMetadata: ApiHandlerCreateMessageMetadata | undefined
24+
25+
override createMessage(
26+
_systemPrompt: string,
27+
_messages: Anthropic.Messages.MessageParam[],
28+
metadata?: ApiHandlerCreateMessageMetadata,
29+
): ApiStream {
30+
this.requestedMetadata = metadata
31+
return (async function* (): AsyncGenerator<ApiStreamChunk> {
32+
yield { type: "text", text: "summary of the conversation" }
33+
yield { type: "usage", inputTokens: 10, outputTokens: 5, totalCost: 0.01 }
34+
})()
35+
}
36+
37+
override getModel(): { id: string; info: ModelInfo } {
38+
return {
39+
id: "dte-condense-model",
40+
info: { contextWindow: 100_000, maxTokens: 5_000, supportsPromptCache: false, supportsImages: false },
41+
}
42+
}
43+
}
44+
45+
// Three messages with no summary in them: getMessagesSinceLastSummary returns all of them, so
46+
// summarizeConversation gets past its "not enough messages" guard and issues the request.
47+
function conversationHistory(): ApiMessage[] {
48+
return [
49+
{ role: "user", content: [{ type: "text", text: "read the failing test" }], ts: 1 },
50+
{ role: "assistant", content: [{ type: "text", text: "the assertion drifted" }], ts: 2 },
51+
{ role: "user", content: [{ type: "text", text: "fix it" }], ts: 3 },
52+
]
53+
}
54+
55+
describe("summarizeConversation reasoning-effort forwarding (DTE series 2/5)", () => {
56+
beforeEach(() => {
57+
if (!TelemetryService.hasInstance()) {
58+
TelemetryService.createInstance([])
59+
}
60+
})
61+
62+
it("forwards an active override to the handler request", async () => {
63+
const handler = new RecordingApiHandler()
64+
65+
const result = await summarizeConversation({
66+
messages: conversationHistory(),
67+
apiHandler: handler,
68+
systemPrompt: "system prompt",
69+
taskId: TASK_ID,
70+
isAutomaticTrigger: false,
71+
metadata: { mode: "code", taskId: TASK_ID, reasoningEffort: OVERRIDE_EFFORT },
72+
environmentDetails: "environment details",
73+
cwd: "/workspace",
74+
})
75+
76+
expect(result.error).toBeUndefined()
77+
expect(result.summary).toBe("summary of the conversation")
78+
expect(handler.requestedMetadata?.reasoningEffort).toBe(OVERRIDE_EFFORT)
79+
})
80+
81+
it("omits the key when the caller hands no override", async () => {
82+
const handler = new RecordingApiHandler()
83+
84+
const result = await summarizeConversation({
85+
messages: conversationHistory(),
86+
apiHandler: handler,
87+
systemPrompt: "system prompt",
88+
taskId: TASK_ID,
89+
isAutomaticTrigger: false,
90+
metadata: { mode: "code", taskId: TASK_ID },
91+
environmentDetails: "environment details",
92+
cwd: "/workspace",
93+
})
94+
95+
expect(result.summary).toBe("summary of the conversation")
96+
expect(handler.requestedMetadata).not.toHaveProperty("reasoningEffort")
97+
})
98+
})

‎src/core/task/__tests__/Task.runtime-thinking-effort.test.ts‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe("Task runtime thinking effort (DTE series 2/5)", () => {
166166
})
167167

168168
describe("dispose", () => {
169-
it("clears an active override without rebuilding or double-disposing the handler", async () => {
169+
it("clears the override and leaves the profile-derived apiConfiguration unchanged", async () => {
170170
task.setRuntimeThinkingEffort("xhigh", "test-source")
171171
const access = getPrivateAccess(task)
172172
const handlerBefore = (task as unknown as { api: unknown }).api
@@ -177,6 +177,11 @@ describe("Task runtime thinking effort (DTE series 2/5)", () => {
177177

178178
expect(access.runtimeThinkingEffort).toBeUndefined()
179179
expect(access.runtimeThinkingEffortSource).toBeUndefined()
180+
// The public read reports the same cleared state. Nothing is restored into
181+
// apiConfiguration here: it is the profile-derived projection and never carried
182+
// the override, so disposal can only leave the profile value it already had.
183+
expect(task.getRuntimeThinkingEffort()).toEqual({ effort: undefined, source: undefined })
184+
expect(task.apiConfiguration.reasoningEffort).toBe(SETTINGS_EFFORT)
180185
// A disposed task never serves another request: disposal must not rebuild
181186
// a replacement handler it would then have to collect and dispose.
182187
expect(vi.mocked(buildApiHandler).mock.calls.length).toBe(buildsBefore)
@@ -367,19 +372,6 @@ describe("Task runtime thinking effort (DTE series 2/5)", () => {
367372
})
368373
})
369374

370-
describe("dispose", () => {
371-
it("clears the task-local override and restores the settings-derived API configuration at task end", async () => {
372-
task.setRuntimeThinkingEffort("xhigh", "source")
373-
await task.dispose()
374-
375-
expect(task.getRuntimeThinkingEffort()).toEqual({ effort: undefined, source: undefined })
376-
const access = getPrivateAccess(task)
377-
expect(access.runtimeThinkingEffort).toBeUndefined()
378-
expect(access.runtimeThinkingEffortSource).toBeUndefined()
379-
expect(task.apiConfiguration.reasoningEffort).toBe(SETTINGS_EFFORT)
380-
})
381-
})
382-
383375
describe("disposal race (lifecycle guard)", () => {
384376
it("drops a runtime-effort update once disposal has started", async () => {
385377
task.setRuntimeThinkingEffort("xhigh", "test-source")

0 commit comments

Comments
 (0)