# 04 — QA & Reliability

**Goal:** eradicate "silly bugs," pass QA cleanly, and reach the state where your code is trusted on sight.

> **The core principle:** Silly bugs are not random — they are **systematic**, and systems kill them. Every silly bug you've ever shipped came from one of: an unenumerated state, an untyped boundary, an untested edge, or a self-review you rushed. Fix the four causes and the bugs die with them.

---

## Part 1 — The self-audit protocol (before you say "done")

### 1.1 The pre-PR gate (run every time, in order — ~15 min)
1. **Diff-as-stranger pass:** re-read your own diff as someone who has never seen the ticket. Would the *why* survive without you? If a hunk looks mysterious to you now, it will look criminal to a reviewer in 6 months.
2. **The adversarial pass:** ask "how would this break?" — then the three variants: *empty input / rapid repeated action / mid-flight navigation*. If you can't answer for all three, the code isn't done.
3. **State enumeration check:** list every UI state the change can be in (loading / empty / error / success / offline / stale-while-refetching). Is each handled, visibly? An unhandled state is a bug waiting for a user to find it.
4. **The delete-test:** delete the code you just wrote (in your head). What breaks? If nothing obvious breaks, the code may be doing less than you think — or nothing at all.
5. **The 10-minute rule:** if you can't explain the change in 10 lines to a colleague, you don't understand it well enough to merge it. Rewrite until you can.

### 1.2 The fresh-eyes pass (the single highest-value habit)
**Never merge the same hour you wrote it.** A 20-minute gap (lunch, another task, a walk) resets your brain's familiarity blind spot — you will catch typos, dead code, and wrong variable names that were invisible minutes earlier. For large changes: sleep on it; review first thing next morning with coffee before any other code.

### 1.3 The "QA-proof" handoff
Give QA the ammunition to find real bugs instead of rediscovering yours:
- PR description includes: what changed, the **state/edge checklist** (1.1.3) you already ran, known limitations, and the test scenarios from the plan (03).
- QA's first question ("what did you test?") has a written answer. QA that trusts your handoff spends its time on deep edge cases, not your happy path — that's the relationship that makes releases clean.

---

## Part 2 — The silly-bug eradicators (systemic, one-time investments)

| Eradicator | Kills | Setup |
|---|---|---|
| **Strict TS, zero `any`** (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`) | `undefined` crashes, shape drift | One-time config (02, Step 4); CI-enforced |
| **Generated API client from OpenAPI** | Handwritten type drift, wrong field names | Contract-first pipeline (02, Step 3) |
| **Runtime validation at the boundary** (zod on server-action inputs / route handlers) | Garbage-in from forms, API payloads | Validate once at the edge; trust inward |
| **Error boundaries** per route/feature | White-screen-of-death for the whole app | React `error boundaries` with a fallback + report |
| **Typed query keys** (factory functions) | Stale data because a key string was mistyped | `queryKeys.feature.detail(id)` |
| **Race-condition hygiene** (AbortController / ignore-flag in effects; stale-closure lint) | Old responses overwriting new ones; update loops | `useEffect` cleanup always; `@tanstack/query` handles its own |
| **State-machine thinking** for complex UI (enumerate states + transitions explicitly, no boolean soup) | Impossible states ("loading AND error") | Type the state: `type ViewState = {status:'loading'} \| {status:'error',e} \| {status:'ready',data}` |
| **Double-submit & idempotency guards** | Duplicate orders/toasts/requests | Disable-while-pending; idempotency keys on mutations |
| **The empty/error/offline trio as a component standard** | Skeleton-less loading, silent failures | Design-system states (02, Step 6) |

---

## Part 3 — The extreme edge-case checklist (run before "ready for QA")

### 3.1 Input & data edges
- [ ] Empty array / null / `undefined` from every fetch — rendered gracefully?
- [ ] Whitespace-only / unicode / emoji / RTL text / extremely long strings (100k chars)?
- [ ] Huge data volumes — list virtualization or pagination? (50k rows kills naive renders)
- [ ] Numbers: `0`, negatives, decimals, `NaN`, `Infinity`, very large (float precision), timezone offsets?
- [ ] Dates across DST boundaries and timezones (ISO-8601 with `Z` everywhere; format at the edge)?
- [ ] File upload: empty file, huge file, wrong type, upload interrupted, retried?

### 3.2 Network & lifecycle edges
- [ ] Loading state on slow network (3G throttle) — skeleton, not blank?
- [ ] Error state per request type (network down ≠ 500 ≠ 404 ≠ rate-limit) — distinct, actionable messages?
- [ ] **Race:** user navigates away mid-request — no setState on unmounted component, no flash of stale data on return?
- [ ] **Race:** rapid filter changes — does the last *response*, not the last *request*, win?
- [ ] **Double-submit:** button spam, Enter key in forms, retry-after-timeout → exactly one mutation?
- [ ] **Idempotency:** retried POST does not duplicate the resource
- [ ] **Offline:** service worker / offline state handled; reconnection reconciles?
- [ ] Back/forward cache (bfcache): page restored from cache is consistent?

### 3.3 Interaction & accessibility edges
- [ ] Keyboard-only: tab order sensible, focus visible, modals trap + restore focus, Esc closes
- [ ] Screen reader: labels present, live regions announce async updates, no aria misuse
- [ ] `prefers-reduced-motion` respected
- [ ] Touch: tap targets ≥ 44px, no hover-only interactions, no 300ms lag
- [ ] Zoom 200%: layout doesn't break or clip
- [ ] **Rapid interaction during loading:** clicking a disabled-but-rendering button, submitting mid-refetch
- [ ] Text selection / paste into controlled inputs behaves
- [ ] Browser back/forward with client-side routing: state, scroll, and URL agree

### 3.4 The senior edge (what actually ships bugs at 10 YOE)
- [ ] **The feature flag off-path:** code behind the flag is still exercised in CI (flagged-off tests) — flags rot silently
- [ ] **The 2nd render:** is state initialized correctly on remount (route param change reusing the component)?
- [ ] **The empty state of your own abstraction:** the utility you wrote for one case, called with the case you didn't imagine
- [ ] **The silent catch:** a `catch` that swallows and pretends success (log it, surface it, or don't catch)
- [ ] **The "temporary" code:** TODOs older than the sprint — either done or tracked with an owner

---

## Part 4 — The testing tiers (make QA an echo, not a discovery)

| Tier | Catches | Volume rule | Tooling (modern) |
|---|---|---|---|
| **Unit** (pure logic, reducers, utils) | Logic bugs | Dense on pure code | Vitest |
| **Integration** (component + behavior) | State, events, a11y regressions | Per component, user-flow-shaped | React Testing Library + Vitest |
| **E2E** (critical journeys only) | Wiring across the system | ≤ 10 journeys, kept green | Playwright |
| **Property-based** (pure functions) | Edge inputs humans never imagine | Where logic is complex | fast-check |

**The 3 rules that keep tests alive (not decorative):**
1. **Test behavior, not implementation** — assert what the user sees/experiences, so refactors don't break tests.
2. **Every bug fix ships with its regression test** — a bug without a test is a bug that will return. Non-negotiable, including "silly" ones (those are the ones that return most).
3. **Red-green discipline:** the test must fail *before* the fix (prove it catches the bug), then pass after. A test that never failed proves nothing.

**AI-assisted test generation:** prompt Claude Code/Copilot with the component + your edge list — *"generate tests for these states: [list]"* — then read every generated test as your own spec. Generated tests that encode the wrong behavior are worse than no tests.

---

## Part 5 — The pre-merge gate (the final 5 minutes)

- [ ] Pre-PR gate passed (1.1) + fresh-eyes pass done (1.2)
- [ ] Strict TS + lint clean in CI (no suppressions without a comment + ticket)
- [ ] New behavior has tests; tests were red before the fix
- [ ] Edge checklist (3.1–3.4) walked for *this change's* blast radius
- [ ] Error paths are visible (not silent catches); loading/empty/error states present
- [ ] Race conditions considered for async work (cleanup, latest-wins)
- [ ] No debug logs / commented code / TODO without owner
- [ ] QA handoff written (states tested, known limits)

**The confidence definition:** you merge with the same calm you'd have demoing the feature to a room of stakeholders on a bad network, live. If you wouldn't demo it, it isn't done — go back to Part 1.

→ Next: [05 — Career Accelerator](05-career-accelerator.md) · Back: [03 — Flawless Execution](03-flawless-execution.md)
