Skip to content

Commit f9d09e5

Browse files
committed
Cover post-report wait paths the fence left untested
Salvage, idle-compact meter, and a second empty continuation after a terminal report reply must wait without re-inferring. Prior cases only exercised a single empty ping with no continuation channel.
1 parent 3708398 commit f9d09e5

1 file changed

Lines changed: 76 additions & 2 deletions

File tree

‎src/subagent/nudge-director.test.ts‎

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function inferenceDone(
5757
} as unknown as ReactorInboundEvent;
5858
}
5959

60-
function inferenceDoneText(text: string): ReactorInboundEvent {
60+
function inferenceDoneText(text: string, inputTokens = 0): ReactorInboundEvent {
6161
return {
6262
type: "inference.done",
6363
turn: {
@@ -66,7 +66,7 @@ function inferenceDoneText(text: string): ReactorInboundEvent {
6666
timestamp: 0,
6767
content: [{ type: "text", text }],
6868
},
69-
usage: { input: 0, output: 1, cacheRead: 0, cacheWrite: 0, thinking: 0 },
69+
usage: { input: inputTokens, output: 1, cacheRead: 0, cacheWrite: 0, thinking: 0 },
7070
source: { model: "test-model" },
7171
} as unknown as ReactorInboundEvent;
7272
}
@@ -477,4 +477,78 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => {
477477
expect(followup.some((action) => action.type === "infer")).toBe(true);
478478
expect(followup.some((action) => action.type === "wait")).toBe(false);
479479
});
480+
481+
test("empty continuation after incomplete-report-stop salvage waits instead of re-inferring", async () => {
482+
const director = new SubAgentDirector("system", [], undefined, 1000);
483+
const caps = capabilities();
484+
485+
await director.decide(inferenceDone(["read-1"]), state, caps);
486+
await director.decide(toolDone("read-1"), state, caps);
487+
await director.decide(inferenceDoneText("Still looking at the files..."), state, caps);
488+
const salvage = actions(
489+
await director.decide(inferenceDoneText("Still narrating, no envelope."), state, caps),
490+
);
491+
expect(salvage).toContainEqual({ type: "checkpoint", message: "subagent-incomplete-report" });
492+
expect(salvage.some((action) => action.type === "reply")).toBe(true);
493+
494+
const afterEmpty = actions(await director.decide(messageReceived(""), state, caps));
495+
expect(afterEmpty.some((action) => action.type === "infer")).toBe(false);
496+
expect(afterEmpty.some((action) => action.type === "reply")).toBe(false);
497+
expect(afterEmpty).toContainEqual({ type: "wait" });
498+
});
499+
500+
test("idle-compact meter path after a report reply waits instead of re-inferring", async () => {
501+
let continuations = 0;
502+
const director = new SubAgentDirector(
503+
"system",
504+
[],
505+
() => {
506+
continuations++;
507+
},
508+
1000,
509+
);
510+
const caps = capabilities();
511+
512+
// Under-threshold tooling so tool.done does not compact before the report.
513+
await director.decide(inferenceDone(["read-1"]), longState, caps);
514+
await director.decide(toolDone("read-1"), longState, caps);
515+
516+
const complete = actions(
517+
await director.decide(inferenceDoneText(REPORT_ENVELOPE, 999_999), longState, caps),
518+
);
519+
expect(complete).toContainEqual({ type: "checkpoint", message: "subagent-complete" });
520+
expect(complete.some((action) => action.type === "reply")).toBe(true);
521+
// noteIdleTurn arms a continuation so the idle-compact path can run.
522+
expect(continuations).toBe(1);
523+
524+
const compact = actions(await director.decide(messageReceived(""), longState, caps));
525+
expect(compact).toEqual([
526+
{ type: "compact", compactor: "pruning-compactor", reason: "context-threshold" },
527+
]);
528+
expect(continuations).toBe(2);
529+
530+
// Post-compact empty re-entry is meter-only; reportReplied keeps it waiting.
531+
const afterMeter = actions(await director.decide(messageReceived(""), longState, caps));
532+
expect(afterMeter.some((action) => action.type === "infer")).toBe(false);
533+
expect(afterMeter.some((action) => action.type === "reply")).toBe(false);
534+
expect(afterMeter).toContainEqual({ type: "wait" });
535+
});
536+
537+
test("repeated empty continuations after a report reply keep waiting", async () => {
538+
const director = new SubAgentDirector("system", [], undefined, 1000);
539+
const caps = capabilities();
540+
541+
await director.decide(inferenceDone(["read-1"]), state, caps);
542+
await director.decide(toolDone("read-1"), state, caps);
543+
await director.decide(inferenceDoneText(REPORT_ENVELOPE), state, caps);
544+
545+
const firstEmpty = actions(await director.decide(messageReceived(""), state, caps));
546+
expect(firstEmpty.some((action) => action.type === "infer")).toBe(false);
547+
expect(firstEmpty).toContainEqual({ type: "wait" });
548+
549+
const secondEmpty = actions(await director.decide(messageReceived(""), state, caps));
550+
expect(secondEmpty.some((action) => action.type === "infer")).toBe(false);
551+
expect(secondEmpty.some((action) => action.type === "reply")).toBe(false);
552+
expect(secondEmpty).toContainEqual({ type: "wait" });
553+
});
480554
});

0 commit comments

Comments
 (0)