From cf6ea332b76cd6ab2da0d8ffb2ef5acfa1e50c70 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 8 Apr 2026 07:52:58 -0500 Subject: [PATCH] test(retry): add tests for third-party block provider guard (#3772) Verify claude-code fallback only fires for anthropic provider and does not reroute non-anthropic providers on similar error text. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/core/retry-handler.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/pi-coding-agent/src/core/retry-handler.test.ts b/packages/pi-coding-agent/src/core/retry-handler.test.ts index a85b3f234..74a21b662 100644 --- a/packages/pi-coding-agent/src/core/retry-handler.test.ts +++ b/packages/pi-coding-agent/src/core/retry-handler.test.ts @@ -293,6 +293,53 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => { }); }); + describe("third-party block claude-code fallback (#3772)", () => { + it("switches to claude-code provider when current provider is anthropic", async () => { + const ccModel = createMockModel("claude-code", "claude-opus-4-6"); + const { deps, emittedEvents, onModelChangeFn } = createMockDeps({ + model: createMockModel("anthropic", "claude-opus-4-6"), + findModelResult: (provider: string, modelId: string) => { + if (provider === "claude-code" && modelId === "claude-opus-4-6") return ccModel; + return undefined; + }, + }); + deps.isClaudeCodeReady = () => true; + + const handler = new RetryHandler(deps); + const msg = errorMessage("third-party apps cannot draw from extra usage"); + + const result = await handler.handleRetryableError(msg); + + assert.equal(result, true, "should retry via claude-code fallback"); + const switchEvent = emittedEvents.find((e) => e.type === "fallback_provider_switch"); + assert.ok(switchEvent, "Expected fallback_provider_switch event"); + assert.ok(switchEvent!.to.startsWith("claude-code/"), "Should switch to claude-code provider"); + }); + + it("does NOT switch to claude-code when current provider is not anthropic", async () => { + const ccModel = createMockModel("claude-code", "gpt-4o"); + const { deps, emittedEvents } = createMockDeps({ + model: createMockModel("openai", "gpt-4o"), + findModelResult: (provider: string, modelId: string) => { + if (provider === "claude-code" && modelId === "gpt-4o") return ccModel; + return undefined; + }, + }); + deps.isClaudeCodeReady = () => true; + + const handler = new RetryHandler(deps); + const msg = errorMessage("third-party apps are not supported for this plan"); + + const result = await handler.handleRetryableError(msg); + + // Should NOT have triggered the claude-code fallback + const switchEvent = emittedEvents.find( + (e) => e.type === "fallback_provider_switch" && e.to?.startsWith("claude-code/"), + ); + assert.equal(switchEvent, undefined, "Should NOT switch non-anthropic provider to claude-code"); + }); + }); + describe("quota_exhausted credential backoff (#3430)", () => { it("does NOT call markUsageLimitReached for quota_exhausted errors", async () => { // "Extra usage is required" is an account-level billing gate.