From c736f1d1857ef0db2ee27d47f9888dbdf1056a80 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 5 Sep 2026 01:43:01 -0400 Subject: [PATCH] test(cli): pin the CLI's own minimum-budget floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot round 1 on #2256: the `budgetMs: 0` case passes under the old `remainingMs <= 0` bound too, so nothing in the CLI suite detected the floor added to `sendPlans`. The new case gives it a budget that is positive and below the floor, and asserts on *this loop's* exhaustion message — the one naming the server URL. That distinction is the test: with the floor removed the plan is handed a 4ms budget and core's identical floor declines it one level down, so a bare "budget was exhausted" match passes either way. Verified as a detector — 1 failed / 14 passed with the CLI floor reverted, 15 passed with it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017wEXtbs8UEHUMxDEAxbs99 Signed-off-by: cliffhall --- .../clear-stored-auth-for-relogin.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/clients/cli/__tests__/clear-stored-auth-for-relogin.test.ts b/clients/cli/__tests__/clear-stored-auth-for-relogin.test.ts index 9b5e587d..be763289 100644 --- a/clients/cli/__tests__/clear-stored-auth-for-relogin.test.ts +++ b/clients/cli/__tests__/clear-stored-auth-for-relogin.test.ts @@ -6,6 +6,7 @@ import { getStateFilePath, resetNodeOAuthStorageCache, } from "@inspector/core/auth/node/storage-node.js"; +import { MIN_REVOCATION_REQUEST_BUDGET_MS } from "@inspector/core/auth/revocation.js"; import { clearStoredAuthForRelogin } from "../src/clear-stored-auth-for-relogin.js"; const AS_ISSUER = "https://as.example.com"; @@ -379,6 +380,35 @@ describe("clearStoredAuthForRelogin", () => { } }); + // The zero-budget case above passes under the old `remainingMs <= 0` bound + // too, so it says nothing about the floor. This one is the floor's own + // detector: a budget that is genuinely positive, and genuinely too small to + // complete a request, must be spent on no request at all (#2252). The + // budget only shrinks as the loop runs, so a slow machine cannot flip it. + it("issues no request for a positive budget below the minimum", async () => { + seedBothSpellings("live-r", "stale-r"); + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response(null, { status: 200 })); + try { + const outcome = await clearStoredAuthForRelogin("https://example.com", { + budgetMs: MIN_REVOCATION_REQUEST_BUDGET_MS - 1, + }); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(outcome).toMatchObject({ status: "failed" }); + // The *plan* was never attempted, so the report has to be this loop's + // own — naming the server URL — and not the per-grant one from inside + // `executeOAuthRevocation`. Without that distinction the assertion + // passes on the old `<= 0` bound too: the plan would be handed a 4ms + // budget, and core's identical floor would decline it one level down. + expect(outcome?.status === "failed" ? outcome.detail : "").toContain( + 'budget was exhausted before "', + ); + } finally { + fetchSpy.mockRestore(); + } + }); + it("reports a later failure over an earlier success", async () => { seedBothSpellings("live-r", "stale-r"); const fetchSpy = vi