Skip to content

QA plan: SPA API key intake and badge (#63)

Story: SPA: API key intake + always-visible badge (RAG009) (#63)
Capability: Web UI (#16)
Spec: specs/ui.md
ADR: RAG009 — key stored in localStorage, badge always visible

TypeCoverage
Unit (Vitest + RTL)First-load with no key in localStorage: key intake prompt visible, question textarea disabled
UnitSaving a key via the intake input stores it under the stable localStorage key name
UnitAfter key is saved, question textarea is enabled; intake prompt is dismissed
UnitBadge renders masked key (rks_a1b2…1234 format), remaining requests count, and expiry date
UnitBadge is present in the DOM at all times (not inside a collapsible panel or modal)
UnitBadge reflects values returned by the /query response or a separate badge-refresh call
UnitKey in localStorage is attached as Authorization: Bearer on every /query POST
SecuritylocalStorage key name is stable and documented; no other key names used
SecurityBadge masks all but first 8 and last 4 chars of the key; full key never appears in visible text
e2e (Playwright)First load: intake prompt appears; paste key; textarea enables; question submits with bearer token
e2eBadge is visible after key entry; content shows remaining count and expiry
  • Story #17 (React chat UI) scaffolding in place
  • API key format rks_<key_id>_<secret> documented
  • All unit tests pass
  • First-load flow: textarea disabled until key saved (Vitest assertion)
  • Badge always-visible: document.querySelector('[data-testid="key-badge"]') is in the document at all times after key entry (never inside a display:none parent)
  • XSS safety: badge renders the masked key as text, not as innerHTML with the raw key value
  • TypeScript: tsc --noEmit exits 0

Unit: Vitest headless. e2e: Playwright against QA env. localStorage is reset between test cases.

Vitest, React Testing Library, Playwright. localStorage manipulation via vi.stubGlobal or RTL’s localStorage API in unit tests.

  • TEST_KEY = "rks_a1b2c3d4_AAABBBCCCDDDEEEFFFGGGHHHIIIJJJ00" (synthetic; matches format)
  • localStorage seeded with and without the test key for the two initial-state tests
test("question textarea is disabled without a stored key", () => {
// Given localStorage has no API key
localStorage.clear();
render(<App />);
// When the component mounts
// Then the question textarea is disabled
expect(screen.getByRole("textbox", { name: /question/i })).toBeDisabled();
});
test("badge is always visible after key entry", async () => {
// Given a key is stored in localStorage
localStorage.setItem("rag_api_key", TEST_KEY);
render(<App />);
// When no interaction has occurred
// Then the badge is in the document and not hidden
const badge = screen.getByTestId("key-badge");
expect(badge).toBeVisible();
});
  • localStorage availability: JSDOM supports it in Vitest but behavior differs from a real browser for quota errors. Keep key values short; no quota concern at this scale.
  • Badge masking logic: off-by-one on the mask boundary (rks_a1b2…1234) could expose more of the key than intended. Add a unit test that verifies the masked output for a known input.
  • RAG009 notes XSS risk with localStorage. Key is treated as public given the demo scale and per-key spend cap. Document this explicitly in the badge component.

Spec: first-load with no key shows intake prompt and disables textarea; badge always visible with masked key, remaining requests, expiry. Both are direct exit criteria above.