From 95f6dbd1e22b837f3f97f6b317c77b5cd8394be9 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Sat, 23 May 2026 22:15:14 +1000 Subject: [PATCH] fix(d9): inject fake auth tokens in Suite 13f before() (CI Linux) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D9 CI failure root cause (logged in run 26332283523): Suite 13f "Fallback engine — HTTP integration" had 3 tests fail on Linux Node 20 runners with: "No Anthropic OAuth token found. Run claude auth login or set CLAUDE_CODE_OAUTH_TOKEN." Tests pass on macOS Node 25 local because the host has a real Anthropic OAuth entry in the macOS keychain. The anthropic plugin's readAuthArtifact() returns that real token, the mock spawn fires, fallback chain works. CI Linux runners have no keychain and no ~/.claude/.credentials.json, so readAuthArtifact() returns null and the anthropic plugin throws ProviderError(AUTH_MISSING) BEFORE the mock spawn function gets a chance to fire. AUTH_MISSING is NOT a hard trigger per ADR 0004 § Decision § No fallback for client-side errors, so the chain stops at the first hop. The 502 the client receives reports anthropic's AUTH_MISSING, not the SPAWN_FAILED the test set up. Three failing tests: 1. "no fallback config + mock anthropic: POST → 200 + Hops: 0" (single-hop; anthropic auth failed before mock spawn) 2. "fallback config: anthropic SPAWN_FAILED → openai succeeds → 200 + Hops: 1" (anthropic AUTH_MISSING stops chain; openai never tried) 3. "fallback config: both providers SPAWN_FAILED → exhausted + header" (anthropic AUTH_MISSING; triedProviders.length=1; no Fallback-Exhausted header emitted) Fix: Suite 13f before() now injects fake auth for both anthropic + codex for the entire suite lifetime, restoring originals in after(). process.env.CLAUDE_CODE_OAUTH_TOKEN = 'fake-anthropic-token-...' process.env.OPENAI_CODEX_AUTH_PATH = Auth artifact injection at suite-level (not per-test) is correct because every test in Suite 13f needs both anthropic and codex to pass their auth checks before mock spawn fires. The earlier per-test codex auth injection at line 3672 was correct in pattern but anthropic was missed. Long comment in before() explains the failure mode so future readers do not regress to assuming "tests pass on my Mac = tests pass on CI." Verification: Node 25.8.0 (Mac): 277/277 pass. Node 20.20.2 (Mac): 277/277 pass — verified via @brew node@20 to match CI matrix. hygiene grep: fake tokens are clearly fake-* placeholders; no real credentials. Reviewer: this is the codex round-2 fold-in pattern applied to a testing scenario — environment-specific dependency that worked locally but not on a clean CI runner. Same evidence-first lesson: "works on my machine ≠ works on CI." Memory entry ~/.cc-rules/memory/feedback/evidence_first_under_speed_pressure.md should grow a corollary "tests that depend on local secrets / OS features must inject fakes at suite level not assume host state." Co-Authored-By: Claude Opus 4.7 (noreply@anthropic.com) --- test-features.mjs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test-features.mjs b/test-features.mjs index 1593928..0bb3b36 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -3545,6 +3545,9 @@ describe('Fallback engine — observability / header annotation (D9)', () => { describe('Fallback engine — HTTP integration (D9)', () => { let server; let port; + let savedAnthropicToken; + let savedCodexAuthPath; + let suiteCodexAuthFile; before(async () => { __resetSpawnImpl(); @@ -3552,6 +3555,29 @@ describe('Fallback engine — HTTP integration (D9)', () => { mistralResetSpawnImpl(); __resetFallbackConfig(); + // CRITICAL — inject suite-level fake auth tokens for anthropic + codex. + // Without this, on hosts that lack the real auth artifacts (CI Linux + // runners are the load-bearing case; they have no macOS keychain and no + // ~/.claude/.credentials.json), the provider plugins throw + // ProviderError(AUTH_MISSING) BEFORE the mock spawn function fires. The + // chain then stops at the first hop because AUTH_MISSING is NOT a hard + // trigger (per ADR 0004 § Decision § No fallback for client-side + // errors), so the fallback path under test never executes. Bug + // discovered on D9 CI 2026-05-23: 3 tests in this suite failed on + // Node 20 Linux while passing on macOS Node 25 because the macOS + // keychain provided real anthropic auth and the test never reached + // the fake-auth-required code path. + savedAnthropicToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'fake-anthropic-token-for-d9-http-suite'; + + const { writeFileSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { join: pathJoin } = await import('node:path'); + suiteCodexAuthFile = pathJoin(tmpdir(), `olp-test-d9-codex-auth-${Date.now()}.json`); + writeFileSync(suiteCodexAuthFile, JSON.stringify({ accessToken: 'fake-codex-token-for-d9-http-suite' }), 'utf8'); + savedCodexAuthPath = process.env.OPENAI_CODEX_AUTH_PATH; + process.env.OPENAI_CODEX_AUTH_PATH = suiteCodexAuthFile; + // Start test server with all 3 providers enabled const testProviders = loadProviders({ enabled: { anthropic: true, openai: true, mistral: true } }); const { loadedProviders: lp, cacheStore: cs } = await import('./server.mjs'); @@ -3575,11 +3601,28 @@ describe('Fallback engine — HTTP integration (D9)', () => { }); }); - after(() => { + after(async () => { __resetFallbackConfig(); __resetSpawnImpl(); codexResetSpawnImpl(); mistralResetSpawnImpl(); + + // Restore env vars injected by before() + if (savedAnthropicToken !== undefined) { + process.env.CLAUDE_CODE_OAUTH_TOKEN = savedAnthropicToken; + } else { + delete process.env.CLAUDE_CODE_OAUTH_TOKEN; + } + if (savedCodexAuthPath !== undefined) { + process.env.OPENAI_CODEX_AUTH_PATH = savedCodexAuthPath; + } else { + delete process.env.OPENAI_CODEX_AUTH_PATH; + } + if (suiteCodexAuthFile) { + const { unlinkSync } = await import('node:fs'); + try { unlinkSync(suiteCodexAuthFile); } catch { /* ignore */ } + } + if (!server) return; return new Promise(r => server.close(r)); });