P0.5.2 — δ Fallback Chain — Verification

Step 5 of the 5-step chain. Test evidence for the Phase 0 library-only single-member fallback stub mandated by ADR-005 §Decision. Closes Phase 0 at 28/28.

1. Gate results

cd .worktrees/claude/p0-5-2-fallback
npm run build       # clean (no TS errors)
npm run lint        # clean (no ESLint warnings/errors)
npm test            # 26 suites / 1084 tests passing

Test count comparison

Metric Before P0.5.2 After P0.5.2 Δ
Test suites 25 26 +1 (src/__tests__/domains/router/fallback.test.ts)
Tests 1051 1084 +33

The +33 (vs. packet target of 10–15) exceeds the lower bound because two additional describes were added during impl: routeRequest — upstream forwarding subtests (seam-injection passes) and default dispatcher tests (coverage 100% on fallback.ts). All 33 tests pass first try after the initial ESLint curly fix.

Coverage (fallback.ts, focused)

-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
 fallback.ts |     100 |    85.71 |     100 |     100 | 217-219, 310
-------------|---------|----------|---------|---------|-------------------
  • Statements: 100%
  • Branches: 85.71% — uncovered are optional-chain / nullish-coalescing defenses in the FallbackChainExhaustedError ctor’s attempts[N-1] access (N is always ≥ 1 in Phase 0 so the branch .error.message ?? 'unknown' is defensively untestable).
  • Functions: 100%
  • Lines: 100%

2. Acceptance criteria verification

Every AC from the contract §6 is verified by at least one passing test.

AC Contract ref Test case in fallback.test.ts Status
AC1 happy path returns RouteResult returns RouteResult with model='claude' and content on success + projects upstream CompletionResult fields into RouteResult + RouteResult.model is abstract 'claude', not upstream version string + RouteResult is frozen PASS
AC2 delegates to scoring consults scoreIntent before calling upstream + uses default scoreIntent when scoringFn not injected PASS
AC3 delegates to upstream (1×) calls completionFn exactly once on success PASS
AC4 forwards prompt forwards prompt unchanged to upstream completionFn PASS
AC5–AC6 forwards maxTokens/systemPrompt/model/apiKey forwards maxTokens / systemPrompt / model / apiKey to upstream + omits optional keys from upstream options when not provided + forwards injected fetchFn / logger / delayFn to upstream PASS
AC7 FallbackChainExhaustedError on failure throws FallbackChainExhaustedError when upstream throws AnthropicApiError PASS
AC8 attempts.length === 1 FallbackChainExhaustedError has exactly 1 attempt PASS
AC9 attempts[0].model === ‘claude’ attempts[0].model === 'claude' (Phase 0 invariant) PASS
AC10 preserves AnthropicApiError preserves AnthropicApiError in attempts[0].error (same instance) PASS
AC11 preserves AnthropicConfigError preserves AnthropicConfigError in attempts[0].error (same instance) + exposes cause pointing to the last attempt error PASS
AC12 ZERO cascade does not retry after upstream failure — completionFn called exactly 1× + does not retry after AnthropicConfigError PASS
AC13 determinism two identical calls route to the same model + different prompts route to the same model (Phase 0 single-member) PASS
AC14 ROUTER_PHASE_0_SHAPE marker asserts single-member chain (members === 1) + asserts no circuit breaker (hasCircuitBreaker === false) + asserts modelsSupported contains only 'claude' + is deeply frozen PASS
AC15 tools passthrough passes tools array through to completionFn via default dispatcher + accepts empty tools array without error + dispatches to createCompletionWithTools when tools non-empty PASS
AC16 error message format includes attempt count and model name + error name is 'FallbackChainExhaustedError' + error code is 'FALLBACK_CHAIN_EXHAUSTED' PASS
AC17 non-Error thrown values wraps non-Error thrown string into Error instance PASS

3. Invariant verification (ADR-005 §Decision)

# Invariant Evidence
I1 Exactly one upstream call per routeRequest, success or failure calls completionFn exactly once on success + does not retry after upstream failure
I2 Winner always ‘claude’ in Phase 0 consults scoreIntent + uses default scoreIntent — both return ‘claude’
I3 RouteResult.model === ‘claude’ (abstract ID, not version) RouteResult.model is abstract 'claude', not upstream version string
I4 Throws FallbackChainExhaustedError on failure throws FallbackChainExhaustedError when upstream throws AnthropicApiError
I5 err.attempts.length === 1 FallbackChainExhaustedError has exactly 1 attempt
I6 err.attempts[0].model === ‘claude’ attempts[0].model === 'claude' (Phase 0 invariant)
I7 Preserves original error instance preserves AnthropicApiError ... (same instance) + preserves AnthropicConfigError ... (same instance)
I8 No timers / circuit breaker state Grepped fallback.ts — no setTimeout, setInterval, Date.now(), Date().
I9 No MCP tool registered Grepped src/server.ts on worktree — no new imports from src/domains/router/*; tool list unchanged at 14.
I10 No env var reads Grepped fallback.ts — no process.env.
I11 ROUTER_PHASE_0_SHAPE asserts invariants All 4 shape tests pass.
I12 Deterministic routing two identical calls route to the same model + different prompts route to the same model
I13 Tools passthrough dispatches to createCompletionWithTools dispatches to createCompletionWithTools when tools non-empty

4. Forbidden-checks (§forbiddens from the task prompt)

Forbidden Check Status
No MCP tool registered src/server.ts diff against main — no change OK
No env vars src/config.ts diff against main — no change OK
No circuit breaker / timers / retries in Phase 0 grep -nE 'setTimeout\|setInterval\|Date\.now\|new Date' src/domains/router/fallback.ts → 0 hits OK
No multi-model fallback attempts.length === 1 test + ROUTER_PHASE_0_SHAPE.members === 1 test OK
No real Anthropic API in tests All tests inject completionFn OR a stubbed fetchFn OK
No modification of scoring.ts git diff origin/main -- src/domains/router/scoring.ts → empty OK
No modification of src/domains/integrations/claude.ts git diff origin/main -- src/domains/integrations/claude.ts → empty OK
No modification of src/server.ts / src/config.ts git diff origin/main -- src/server.ts src/config.ts → empty OK

5. Non-regression

  • All 25 pre-existing test suites remain green. Pre-existing intermittent startup — subprocess smoke flake (documented in memory as “flagged by Waves F and G sub-agents; predates”) intermittently surfaced once on first run; second + third runs clean. Not caused by P0.5.2.
  • src/__tests__/domains/router/scoring.test.ts — 23 tests, all green — unaffected.
  • src/__tests__/domains/integrations/claude.test.ts — claude wrapper tests unchanged.
  • ESLint config + tsconfig unchanged.

6. Deviation from packet

The packet sketched 14 test cases; the actual file landed 33 because:

  1. Multiple ACs collapsed into shared describes got split out for readability during impl (e.g. forwards X to upstream became three separate assertions + a negative test for omission + a separate seams test).
  2. default dispatcher tests were added to lift fallback.ts coverage from 86.36% to 100% on stmts/funcs/lines — needed to exercise the createCompletion / createCompletionWithTools dispatch branch when completionFn is NOT injected.

Neither deviation changes scope; both strengthen evidence.

7. Phase 0 completion

This task is the last of 28 Phase 0 sub-tasks.

Phase 0 status Before After
Non-deferred sub-tasks complete 26/28 28/28
δ concept colibri_code none (deferred) partial (Wave I close commit will graduate)
Phase 0 shipped code axes 7/15 (α β γ ε ζ η ν) 8/15 (+ δ)
MCP tool surface 14 14 (zero added)

Per ADR-006 graduation rules, shipping a Phase 0 stub lifts the concept’s colibri_code field from none to partial. The frontmatter update belongs to the Wave I close commit, not this PR.

8. Conclusion

All acceptance criteria pass. All ADR-005 §Decision invariants are enforced by tests. Zero tool-surface, env-var, DB, or timer additions. Phase 0 closes at 28/28 on the boundary mandated by ADR-005. Ready to open PR.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.