fix(composer): three-state model availability - never brick on unknown keys

↗ view on GitHub · Amal · 2026-08-18 · 728c4a9f

The ported availability gate treated "we don't know the key state" the
same as "there are no keys": the Word add-in's composer stuck on
"No API Key" after one flaky WKWebView preflight, and the web toggle
flashed "No API Key" on every page load while the profile was in flight
(a render test even asserted the flash).

WHAT FAIL-OPEN VS FAIL-CLOSED MEANS HERE
The key-status preflight is a UX hint, not the security boundary - the
backend authoritatively rejects any model it has no key for (and, since
the allowlist commit, any router model outside the user's selection).
Failing CLOSED on a hint means one dropped fetch blocks sends the
backend would have accepted, with no recovery except reloading the
pane. Failing OPEN on an UNKNOWN state costs nothing: the worst case is
a request the backend answers with a clear error. So unknown now fails
open, while a successfully LOADED status still gates exactly as before
- fail-closed is preserved where the information is real.

HOW IT WORKS - three states in both clients
- loading: neutral, disabled trigger showing the selected model's label
  (never "No API Key"); submits are not blocked.
- loaded: unchanged - filter models by configured providers, show
  "No API Key" when a real loaded status has none.
- failed: retry once with backoff (loadWithRetry in the add-in; an
  inline retry in UserProfileContext on the web), then console.warn and
  fail open. The web context exposes `apiKeysDegraded` so ChatInput,
  TRChatPanel and TabularReviewView pass `undefined` (unknown) instead
  of the fallback profile's all-false key map to their gates.

Tests: composerAvailability.test.ts (add-in isModelAvailable fails open
on null, still gates on loaded state; loadWithRetry retries once,
reports the final error, resolves null) and ModelToggle.render.test.tsx
(loading renders neutral+disabled; unknown-after-failure renders
enabled without "No API Key"; loaded no-keys still says "No API Key").
The old test that ENSHRINED the flash (asserting "No API Key" for
apiKeys-undefined) is rewritten. Verified pre-fix via stash: 3 tests
fail on the ported fail-closed code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents 09757c44
Stats 11 files changed , +321 , -37
Part of Harden model-router selection and failure handling

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-728c4a9f.md from inside the repo you want the change in.

⬇ Download capture-commit-728c4a9f.md