[AZ-457] [AZ-459] [AZ-465] [AZ-481] Batch 2 - auth/enum/i18n/CI tests

Implements 22 blackbox test scenarios across the four batch-2 tasks:

AZ-457 - Auth & token handling (11 scenarios, fast + e2e):
- src/api/client.test.ts: FT-P-02, NFT-SEC-04, NFT-PERF-02, NFT-RES-01,
  NFT-RES-08 (apiClient surface)
- src/auth/AuthContext.test.tsx: FT-P-01 (it.fails - Step 4 drift),
  FT-P-03, NFT-SEC-01, NFT-SEC-02
- src/auth/ProtectedRoute.test.tsx: FT-N-04, NFT-RES-08 (router half)
- e2e/tests/auth.e2e.ts: FT-P-02 e2e, NFT-SEC-01/02/03 (cookie attrs
  via Playwright context.cookies(), gated by suite stack)

AZ-459 - Wire-contract enums (4 scenarios):
- tests/wire_contract.test.ts: FT-P-04 (AnnotationStatus, it.fails),
  FT-P-05 (MediaStatus + Affiliation it.fails; CombatReadiness skip
  per verification_pending), FT-P-06 (AnnotationSource control +
  spec value-set membership), FT-N-15 (typed-enum shape + skip for
  value-set verification)
- e2e/tests/wire_contract.e2e.ts: FT-P-06 against real annotations/
  service, drift-gated via AZAION_RUN_DRIFT_E2E
- scripts/run-tests.sh STC-FN15: ripgrep static for MediaType
  magic-literal hygiene

AZ-465 - i18n (4 scenarios, all static + quarantined fast):
- scripts/check-i18n-coverage.mjs: FT-P-22 (en vs ua key parity) +
  FT-P-23 (no raw user strings outside t() in src/**/*.tsx); refined
  JSX text-node regex with negative lookbehind to drop TS generics
  + arrow-function false positives
- tests/i18n-allowlist.json: snapshot of current pre-existing raw
  strings (CI gates growth per AZ-465 Constraints)
- tests/i18n.test.tsx: FT-P-24 + FT-P-25 it.skip (QUARANTINE - i18n
  detector + persistence not wired today; control tests assert the
  gap so the skip flips to a real test once Step 4 lands)

AZ-481 - CI image labels (3 scenarios, static against
  .woodpecker/build-arm.yml):
- scripts/check-ci-image-labels.mjs: NFT-RES-LIM-11 (tag scheme
  ${CI_COMMIT_BRANCH}-arm), NFT-RES-LIM-12 (revision/created/source
  PASS, image.title reported as DRIFT - foundation/CI-CD owns the
  fix), NFT-RES-LIM-13 (revision = $CI_COMMIT_SHA)

Cross-cutting:
- scripts/run-tests.sh: src_grep now excludes *.test.{ts,tsx} +
  *.spec.{ts,tsx} so production-source static checks (STC-SEC4,
  STC-FN15, etc.) don't false-positive on test prose
- tsconfig.json: exclude src/**/*.{test,spec}.{ts,tsx} so production
  tsc -b doesn't see jest-dom matchers
- _docs/03_implementation/batch_02_report.md: full per-task AC
  coverage matrix + drift inventory + verification run
- _docs/_autodev_state.md: 22 tasks remain after batch 2

Verification (host):
  fast    : 7 files, 38 passed | 4 skipped (quarantined)
  static  : 19/19 checks PASS (was 13 in batch 1; +6 from batch 2)
  e2e     : not run on host (Risk 4 - requires suite docker stack)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 03:27:55 +03:00
parent 496b089102
commit ab22223580
18 changed files with 1910 additions and 4 deletions
@@ -1,79 +0,0 @@
# Test — Auth & Token Handling
**Task**: AZ-457_test_auth_token_handling
**Name**: Auth & token-handling blackbox suite
**Description**: Implement every blackbox test that exercises the in-memory bearer, the refresh cookie, the 401→refresh→retry path, and the redirect-to-/login flow. Spans `fast` (MSW) and `e2e` (real `admin/`) profiles.
**Complexity**: 5 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 01_api-transport + 02_auth (Blackbox Tests)
**Tracker**: AZ-457
**Epic**: AZ-455
## Problem
The SPA's auth surface (`<AuthContext>` + `src/api/client.ts` + `<ProtectedRoute>`) is the gate every other test depends on. It mixes a memory-only bearer (AC-02 / O2), a HttpOnly refresh cookie (AC-03), a 401-retry loop (AC-23), and a redirect on refresh failure (C06 from autodev Step 4). Tests must lock down the wire contract AND the storage contract without leaking into production.
## Outcome
- 11 test scenarios pass in their declared profile, all referencing `results_report.md` rows for expected observables.
- The fast suite uses MSW to drive the `/api/admin/auth/refresh` path; e2e uses the real `admin/` service.
- No test stubs `src/api/client.ts`, `<AuthContext>`, or `<ProtectedRoute>` internals — every assertion is observable at the DOM, network, or browser-storage surface.
## Scope
### Included
| Scenario | Profile | Source file | results_report row |
|----------|---------|-------------|--------------------|
| FT-P-01 — bootstrap refresh sends `credentials:'include'` | fast | blackbox-tests.md | 02 |
| FT-P-02 — 401 → refresh → retry sequence | fast + e2e | blackbox-tests.md | 03, 12 |
| FT-P-03 — refresh transparency, no `<ProtectedRoute>` unmount | fast | blackbox-tests.md | 11 |
| FT-N-04 — unauthenticated `/admin` → redirect to `/login` | fast | blackbox-tests.md | row(s) per blackbox-tests.md FT-N-04 |
| NFT-SEC-01 — bearer never in localStorage/sessionStorage | fast | security-tests.md | 01 |
| NFT-SEC-02 — refresh cookie not in `document.cookie` | fast | security-tests.md | 04 |
| NFT-SEC-03 — refresh cookie has `Secure; HttpOnly; SameSite=Strict` | e2e | security-tests.md | 05 |
| NFT-SEC-04 — `credentials:'include'` on every authed fetch | fast | security-tests.md | 06 |
| NFT-PERF-02 — exactly one refresh round trip per cycle | fast | performance-tests.md | 12 |
| NFT-RES-01 — 401→refresh→retry is transparent end-to-end | fast | resilience-tests.md | 03, 11 |
| NFT-RES-08 — refresh cookie expired → redirect to `/login` | fast | resilience-tests.md | row(s) per NFT-RES-08 |
### Excluded
- SSE bearer rotation (covered in 03_test_sse_lifecycle).
- `/settings` / `/admin` RBAC beyond the unauthenticated-redirect case (covered in 12_test_protected_route_rbac).
- ConfirmDialog and destructive-action gating (covered in 11_test_destructive_ux).
## Acceptance Criteria
**AC-1: All 11 scenarios implemented**
Given the test infrastructure from AZ-456 is in place,
When `bun run test:fast` and `bun run test:e2e` execute,
Then every scenario above is present, runs in its declared profile, and references its `results_report.md` row in test comments.
**AC-2: Black-box discipline**
No test imports anything from `src/api/client.ts` (other than the public `setToken` / `getToken` / `setNavigateToLogin` accessors per AC-23 / autodev Step 4 testability changes); no test imports `<AuthContext>` internals; no test asserts on React state directly.
**AC-3: Storage assertions are exhaustive**
NFT-SEC-01 / NFT-SEC-02 assert that **for the duration of the test** the bearer never appears in `localStorage`, `sessionStorage`, or `document.cookie` — not just at one snapshot.
**AC-4: Redirect assertion uses the accessor**
FT-N-04 and NFT-RES-08 install a `setNavigateToLogin(spy)` and assert `spy` was called exactly once with no arguments; they do NOT globally stub `window.location`.
## System Under Test Boundary
- The system under test is the assembled SPA: React tree + `src/api/client.ts` + `<AuthContext>` + `<ProtectedRoute>` + Router.
- Allowed stubs: the suite's `admin/` `auth/refresh` and `auth/login` endpoints — stubbed via MSW in `fast`, real service in `e2e`.
- Disallowed: stubbing `src/api/client.ts`, `<AuthContext>`, `<ProtectedRoute>`, or any other internal SPA module. If `<AuthContext>` is not behaving as expected, the test FAILS — it does not bypass it.
- Expected observables compared against `_docs/00_problem/input_data/expected_results/results_report.md` rows 02, 03, 04, 05, 06, 11, 12, and the rows for FT-N-04 / NFT-RES-08 as listed in `traceability-matrix.md`.
## Constraints
- One test file per top-level concern; co-locate next to source (e.g., `src/api/client.test.ts`, `src/auth/AuthContext.test.tsx`, `src/auth/ProtectedRoute.test.tsx`).
- MSW handlers live in `tests/msw/handlers/admin.ts`; per-test overrides via `server.use(...)` only.
- E2E auth uses the `op_alice` seed user (test-data.md).
## Risks & Mitigation
**Risk 1 — Refresh-cookie HttpOnly invisibility**
- *Risk*: AC-03 requires `HttpOnly` — by spec, the cookie is invisible to JS, so the fast profile cannot directly assert presence.
- *Mitigation*: in `fast`, MSW asserts the response `Set-Cookie` header carries the three flags (the server contract). In `e2e`, the test uses Playwright's `context.cookies()` (which sees HttpOnly cookies) to verify presence + flags. NFT-SEC-03 runs e2e-only.
@@ -1,76 +0,0 @@
# Test — Wire-Contract Enum Compliance
**Task**: AZ-459_test_wire_contract_enums
**Name**: Enum compliance + MediaType hygiene
**Description**: Implement every blackbox test that asserts the SPA's on-wire enum values (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType`, `AnnotationSource`) match the contract pinned in `enum_spec_snapshot.json`, plus the MediaType magic-literal hygiene check.
**Complexity**: 2 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 07_dataset + 06_annotations + cross-cutting (Blackbox Tests)
**Tracker**: AZ-459
**Epic**: AZ-455
## Problem
`AC-04` and `AC-29` require the UI to send/accept the exact numeric values from the suite spec. Today the UI carries drift (documented in `enum_spec_snapshot.json` § `ui_drift_summary`) — for `AnnotationStatus` the UI sends `Edited=1` while the contract pins `Edited=20`. Tests must FAIL on the drift so the regression is visible until the Phase B fix lands; they must NOT silently accept the wrong values.
## Outcome
- 4 test scenarios pass (or fail loudly to document the drift) per the contract pin.
- Tests load `_docs/00_problem/input_data/enum_spec_snapshot.json` once per test file and compare runtime values against the pinned values.
- `verification_pending: true` enums (`CombatReadiness`, `MediaType`) are quarantined with a clear marker until the Step 4 .NET-service inspection lands.
## Scope
### Included
| Scenario | Profile | Source file | results_report row |
|----------|---------|-------------|--------------------|
| FT-P-04 — AnnotationStatus enum on the wire | static + fast | blackbox-tests.md | 14 |
| FT-P-05 — MediaStatus / Affiliation / CombatReadiness enums match the spec | static + fast | blackbox-tests.md | 15, 16, 17 |
| FT-P-06 — detection wire payload — affiliation + combatReadiness in spec value sets | fast + e2e | blackbox-tests.md | 18, 19 |
| FT-N-15 — MediaType magic-literal / magic-string hygiene | static + fast | blackbox-tests.md | per FT-N-15 |
### Excluded
- Annotation save body shape (covered in 05_test_annotations_endpoint).
- DetectionClasses ordering / hotkey path (covered in 17_test_detection_classes).
## Acceptance Criteria
**AC-1: Snapshot-driven assertion**
Given `_docs/00_problem/input_data/enum_spec_snapshot.json` is the contract pin,
When any test in this task runs,
Then it loads the snapshot and asserts the runtime wire value matches the pinned value per row 14, 15, 16, 17, 18, 19 of `results_report.md`.
**AC-2: Drift surfaces, not silently passes**
Given the UI today drifts on `AnnotationStatus` (Edited=1 vs spec 20) and similar,
When the test runs against today's UI,
Then it FAILS with a message naming the enum and the observed-vs-expected pair. (It does NOT pass by re-reading the UI's value as authoritative.)
**AC-3: verification_pending markers**
Given `CombatReadiness` and `MediaType` carry `verification_pending: true` in the snapshot,
When the test sees the flag,
Then it emits a clear QUARANTINE marker (CSV `Result: QUARANTINE`) and a comment naming the resolution path (Step 4 .NET-service inspection).
**AC-4: MediaType magic-literal hygiene (FT-N-15)**
Given the source tree at HEAD,
When the static check scans `src/` for hardcoded numeric `MediaType` literals,
Then any literal not wrapped in the typed enum is flagged.
## System Under Test Boundary
- System under test: the actual fetch URLs and payloads issued by the SPA, the rendered DOM that surfaces enum-derived state, and (for FT-N-15) the source tree itself.
- Allowed stubs: MSW handlers that capture the outbound request and assert the numeric values.
- Disallowed: importing `src/types/index.ts` and comparing the snapshot to it — that's a tautology. The test compares the snapshot to the RUNTIME wire output. Importing the typed enum SHAPES for declaration is fine per `P9` / black-box discipline (the enums are the wire contract).
- Expected observables compared against `_docs/00_problem/input_data/expected_results/results_report.md` rows 14-19.
## Constraints
- Snapshot must be read once per module; cache via Vitest module-level import.
- FT-N-15 runs as a `ripgrep`-driven static check from `scripts/run-tests.sh --static-only`.
## Risks & Mitigation
**Risk 1 — verification_pending lifts mid-implementation**
- *Risk*: If Step 4 .NET-service inspection lands while these tests are being implemented, the snapshot rewrites and a previously-QUARANTINEd test may flip to PASS or FAIL.
- *Mitigation*: tests read the snapshot at runtime — they auto-adapt. The QUARANTINE marker is conditional on the runtime `verification_pending` flag.
-66
View File
@@ -1,66 +0,0 @@
# Test — i18n Coverage & Persistence
**Task**: AZ-465_test_i18n
**Name**: i18n key parity + t() coverage + detector + persistence
**Description**: Implement the 4 blackbox tests that pin the i18n contract: en↔ua key parity (static), `t()` coverage (no raw user-visible strings), boot-time language detector, and persistence across reload.
**Complexity**: 3 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 03_shared-ui + 10_app-shell (i18n) (Blackbox Tests)
**Tracker**: AZ-465
**Epic**: AZ-455
## Problem
A missing translation key or a hardcoded user-visible string only surfaces when a user switches language — by which point it's a customer-visible defect. Static checks + a behavioral test for detect/persist catch these at commit time.
## Outcome
- 4 scenarios pass per the contract.
- The "no raw strings" check is enforceable in CI and produces a clear allow-list mechanism for legitimate non-i18n text (e.g. brand names).
## Scope
### Included
| Scenario | Profile | Source file | results_report row |
|----------|---------|-------------|--------------------|
| FT-P-22 — i18n key parity en ↔ ua | static | blackbox-tests.md | 45 |
| FT-P-23 — no raw user-visible strings outside `t(...)` | static | blackbox-tests.md | 46 |
| FT-P-24 — i18n detector path used at first boot | fast + e2e | blackbox-tests.md | 47 |
| FT-P-25 — i18n persistence across reload | fast + e2e | blackbox-tests.md | 48 |
### Excluded
- Adding a third language (project is en + ua per scope).
- RTL support (not required by any AC).
## Acceptance Criteria
**AC-1: Key parity**
Static check: `keys(en.json) == keys(ua.json)` (set equality). Test FAILS on any drift.
**AC-2: t() coverage**
Static check via ripgrep + AST walker: every JSX text node and string-literal `aria-*` / `title` / `placeholder` either lives in an i18n key, is in the allow-list (brand names, version strings), or fails the check.
**AC-3: Detector path**
First boot with no persisted language preference: SPA reads `navigator.language` and renders the matching bundle.
**AC-4: Persistence**
After user switches to UA and reloads, the UA bundle is rendered without explicit user action.
## System Under Test Boundary
- System under test: `src/i18n/i18n.ts` + every React component rendering user-visible text.
- Allowed stubs: none beyond the standard test renderer.
- Disallowed: reading the i18n state directly — the test asserts the rendered DOM text.
- Expected observables per rows 45-48.
## Constraints
- Allow-list file lives at `tests/i18n-allowlist.json`; CI enforces it must not grow without a code-review reason.
## Risks & Mitigation
**Risk 1 — AST walker false positives**
- *Risk*: the t() coverage walker may misclassify dynamic strings (e.g. `t(\`key_${id}\`)`) or ternaries.
- *Mitigation*: explicit allow-list per file, plus a comment marker `// i18n-ok: <reason>` honored by the walker.
@@ -1,54 +0,0 @@
# Test — CI Image Tag Scheme & OCI Labels
**Task**: AZ-481_test_ci_image_labels
**Name**: CI image tag `${branch}-arm` + OCI labels + revision label
**Description**: Implement the 3 blackbox tests pinning the CI-produced image surface: tag scheme is `${branch}-arm` (NFT-RES-LIM-11), required OCI labels are present (NFT-RES-LIM-12), and the `org.opencontainers.image.revision` label equals `$CI_COMMIT_SHA` (NFT-RES-LIM-13).
**Complexity**: 2 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 00_foundation (CI/CD) (Blackbox Tests)
**Tracker**: AZ-481
**Epic**: AZ-455
## Problem
Without a tag canary, two builds on the same branch can overwrite each other silently (no `-arm` suffix); without OCI labels, the image is not traceable to a source revision. Both regressions are operationally catastrophic and surface only after a deploy.
## Outcome
- 3 scenarios pass.
## Scope
### Included
| Scenario | Profile | Source file |
|----------|---------|-------------|
| NFT-RES-LIM-11 — CI image tag scheme is `${branch}-arm` | static (CI config) + e2e (against pushed image) | resource-limit-tests.md |
| NFT-RES-LIM-12 — OCI labels present on the pushed image | static + e2e | resource-limit-tests.md |
| NFT-RES-LIM-13 — Revision label equals `$CI_COMMIT_SHA` | e2e | resource-limit-tests.md |
### Excluded
- nginx route / image-base assertions (covered in 25_test_prod_image_nginx_ram).
## Acceptance Criteria
**AC-1: Tag scheme**
NFT-RES-LIM-11 — CI config (`.gitlab-ci.yml` / equivalent) builds with tag pattern `${CI_COMMIT_REF_SLUG}-arm`; the static check asserts the pattern is intact. E2e additionally asserts `docker manifest inspect $REGISTRY/$IMAGE:${BRANCH}-arm` succeeds.
**AC-2: OCI labels present**
NFT-RES-LIM-12 — `docker inspect $IMAGE` reports the required OCI labels: `org.opencontainers.image.source`, `org.opencontainers.image.revision`, `org.opencontainers.image.title`, `org.opencontainers.image.created` — all non-empty.
**AC-3: Revision binding**
NFT-RES-LIM-13 — `docker inspect --format='{{index .Config.Labels "org.opencontainers.image.revision"}}'` returns exactly `$CI_COMMIT_SHA`.
## System Under Test Boundary
- System under test: CI config (`.gitlab-ci.yml` / equivalent) + the pushed image's metadata.
- Allowed stubs: a tag fixture for static-mode validation; in e2e the test inspects an actual locally-built image.
- Disallowed: bypassing CI to inject labels for the test.
## Constraints
- E2e tests run only on CI; locally `bun run test:e2e -- --skip-tags @requires-ci` skips them.
- CI environment provides `CI_COMMIT_SHA` and `CI_COMMIT_REF_SLUG`.