Skip to content

Commit dbe055d

Browse files
committed
test(tui): add hydrate-cap keeper probes for folding and observe paths
1 parent 84d34cf commit dbe055d

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

‎src/tui/product-host.test.ts‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
type ProductHostConfig,
2020
} from "./product-host.js";
2121
import { buildModelsFirstCatalog, modelOptionId } from "./model-catalog.js";
22+
import { hydrateHistoryRows } from "./history-hydrate.js";
2223
import { MAX_RETAINED_STREAM_ROWS } from "./long-log.js";
24+
import { enterSubagentObserve } from "./shell/observe.js";
2325

2426
function makeFakeSessionPort(): {
2527
readonly sends: string[];
@@ -187,6 +189,80 @@ describe("mountProductHost", () => {
187189
}
188190
});
189191

192+
test("history.hydrate keeps a tool pair merged atomically across the cap boundary (CL-9008)", async () => {
193+
const { host, emitter } = await mountHeadless();
194+
try {
195+
const blocks = [
196+
...Array.from({ length: 605 }, (_, i) => ({
197+
type: "text",
198+
content: `row-${i}`,
199+
})),
200+
{
201+
type: "tool_call",
202+
name: "spawn_agent",
203+
arguments: '{"description":"Fix CL-9008"}',
204+
callId: "cut-1",
205+
},
206+
{
207+
type: "tool_result",
208+
name: "spawn_agent",
209+
content: "done cut-1",
210+
callId: "cut-1",
211+
},
212+
];
213+
emitter.emit("history.hydrate", blocks);
214+
// Folding merges the pair inside hydration, so the row-level slice keeps
215+
// and drops whole merged rows: 605 texts + 1 merged row → newest 600.
216+
// A block-level slice would split the pair and paint 599 rows instead.
217+
const expected = hydrateHistoryRows(blocks).slice(
218+
-MAX_RETAINED_STREAM_ROWS,
219+
);
220+
expect(expected.length).toBe(MAX_RETAINED_STREAM_ROWS);
221+
expect(host.shell.streamLog).toEqual(expected);
222+
expect(host.shell.streamLog[0]).toEqual({
223+
role: "assistant",
224+
text: "row-6",
225+
});
226+
const last = host.shell.streamLog[host.shell.streamLog.length - 1];
227+
expect(last?.pending).not.toBe(true);
228+
expect(last?.text).toBe("done cut-1");
229+
expect(host.shell.streamLogBase).toBe(0);
230+
} finally {
231+
host.dispose();
232+
}
233+
});
234+
235+
test("history.hydrate in observe mode lands the capped tail on the parent log (CL-9008)", async () => {
236+
const { host, emitter } = await mountHeadless();
237+
try {
238+
enterSubagentObserve(host.shell, {
239+
sessionId: "child-keeper",
240+
agentId: "explorer",
241+
description: "observe-mode hydrate keeper",
242+
lines: [],
243+
});
244+
expect(host.shell.parentStreamLog).toEqual([]);
245+
const visibleBefore = host.shell.streamLog.length;
246+
const total = MAX_RETAINED_STREAM_ROWS + 200;
247+
const blocks = Array.from({ length: total }, (_, i) => ({
248+
type: "text",
249+
content: `obs-row-${i}`,
250+
}));
251+
emitter.emit("history.hydrate", blocks);
252+
// Observe routes hydrate rows to the parent snapshot only; the child
253+
// view on screen is untouched.
254+
expect(host.shell.streamLog.length).toBe(visibleBefore);
255+
const expected = hydrateHistoryRows(blocks).slice(
256+
-MAX_RETAINED_STREAM_ROWS,
257+
);
258+
expect(host.shell.parentStreamLog).toEqual(expected);
259+
expect(host.shell.parentStreamLog?.length).toBe(MAX_RETAINED_STREAM_ROWS);
260+
expect(host.shell.parentStreamLogBase).toBe(0);
261+
} finally {
262+
host.dispose();
263+
}
264+
});
265+
190266
test("session.title updates the shell header", async () => {
191267
const { host, emitter } = await mountHeadless();
192268
try {

0 commit comments

Comments
 (0)