From 3d47c1e6b4f842b6005a75f9dc15b053a8deb9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 24 Oct 2026 09:29:38 +0200 Subject: [PATCH] test(guards): hold the whole regenerate route to its two model stages The browser's abort for a forced regenerate waits three calls at the response timeout, fewer than the five the background generator may make. That is correct: the route runs its own pipeline, refuses an unparseable reply or a screened payload instead of retrying, and never reaches the background generator, so its longest run is the generation, the grounding retry and the one reasoning repeat. The guard only read two of the pipeline's files, so a retry stage added anywhere else on the route would have lengthened the run past the browser's abort unnoticed. It now sweeps the route and the whole briefing-request folder for stages and for any call into the background generator, and the advisor states why its count is the shorter one. Refs #1274 --- src/__tests__/ai-call-timeout-guard.test.ts | 27 ++++++++++++++----- .../insights/use-insights-advisor.ts | 6 +++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/__tests__/ai-call-timeout-guard.test.ts b/src/__tests__/ai-call-timeout-guard.test.ts index 1dbad7b14..7704967fe 100644 --- a/src/__tests__/ai-call-timeout-guard.test.ts +++ b/src/__tests__/ai-call-timeout-guard.test.ts @@ -273,12 +273,27 @@ describe("outer bounds around model work allow every call they wait on", () => { }); it("the on-demand regenerate makes no more calls in a row than the browser waits for", () => { - // The generation and its grounding retry, and the one repeat inside the - // client: FORCE_ADVISOR_MODEL_CALLS in the advisor hook. - expect([ - ...stagesIn("lib/insights/briefing-request/completion.ts"), - ...stagesIn("lib/insights/briefing-request/grounding.ts"), - ]).toEqual(["generate", "grounding-retry"]); + // The route's own pipeline: the generation and its grounding retry, and + // the one repeat inside the client (FORCE_ADVISOR_MODEL_CALLS in the + // advisor hook). An unparseable reply or a screened payload is refused, + // not retried, and the background generator is never reached from here, + // which is why this is shorter than COMPREHENSIVE_MODEL_CALLS. A new + // stage anywhere on the route means recounting the browser's abort. + const pipeline = [ + "app/api/insights/generate/route.ts", + ...walkSourceFiles(join(SRC, "lib/insights/briefing-request"), { + floor: 8, + }) + .filter((f) => !f.includes("__tests__")) + .map((f) => `lib/insights/briefing-request/${f}`), + ]; + expect(pipeline.flatMap(stagesIn).sort()).toEqual([ + "generate", + "grounding-retry", + ]); + for (const rel of pipeline) { + expect(code(rel), rel).not.toMatch(/\bgenerateComprehensiveInsight\b/); + } expect(code("components/insights/use-insights-advisor.ts")).toMatch( /\bFORCE_ADVISOR_MODEL_CALLS\s*=\s*3\b/, ); diff --git a/src/components/insights/use-insights-advisor.ts b/src/components/insights/use-insights-advisor.ts index 1ab2c1b77..929ed2717 100644 --- a/src/components/insights/use-insights-advisor.ts +++ b/src/components/insights/use-insights-advisor.ts @@ -128,6 +128,12 @@ const ADVISOR_TIMEOUT_MS = 8_000; * answered nothing is asked once more inside the first, so three calls at the * per-call timeout, at least the briefing's own 180 s default each. Never * shorter than the server: the abort is only for a request nobody answers. + * + * Fewer than `COMPREHENSIVE_MODEL_CALLS` on purpose: the route runs its own + * pipeline (`src/lib/insights/briefing-request/`), not the background + * generator, and answers an unparseable reply or a screened payload with a + * refusal instead of the generator's JSON retry and screen repair. + * `ai-call-timeout-guard.test.ts` holds that pipeline to these two stages. */ const FORCE_ADVISOR_MODEL_CALLS = 3;