teqo-backup/docs/TESTING.md

14 KiB
Raw Blame History

Testing

Test Strategy

Three layers, all local-database-only (see local-database skill; tests refuse any database not ending in _test):

  • Unit (tests/unit/**/*.unit.spec.ts, 117 specs as of 2026-07-31, Pass 4 — +6 convention guards land with the Pass 4 remediation PRs): pure logic; runs with a deliberately invalid DATABASE_URL so nothing can touch a database.
  • Integration (tests/int/**/*.int.spec.ts, 71 specs): real Payload + Postgres (teqo_test); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Since the miss #73 guardrail (Pass 4), specs that mutate municipalities must use the allocator (no pinned slugs) — enforced by testMunicipalityAllocatorConventions. Shared consent rows with stable keys are leased (tests/helpers/testDatabaseLease.ts), never owned.
  • E2E (tests/e2e, Playwright, 17 specs / 51 cases (Pass 5, 2026-08-03; was 48 at Pass 4)): smoke-level; most campaign behavior is pinned at the int layer.

Level preference (pyramid): unit first (pure logic) → integration for Payload/DB boundaries → e2e only when the flow has real benefit the lower layers don't cover (critical user path, UX/navigation, URL contract). Canonical definition: test-driven-development skill.

CI (ci-pr.yml) is a single sequential job checks (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (affected by the PR diff: vitest --changed against the PR base via scripts/test-affected.mjs, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via scripts/e2e-affected.mjs + scripts/lib/e2e-affected-manifest.mjs, blocking; only in mode selected — OPS72: the PR never runs full e2e; production build + list/github reporters in CI) — against two isolated Postgres 17 services (int / build+e2e) with db:seed:minimal. Fail-fast is structural: a failed step stops the run; e2e runs with PLAYWRIGHT_WORKERS=4. There is no main verifier (ci.yml eliminated in OPS71): the full net including e2e runs in the deploy verify job before publishing (manual dispatch).

Local e2e (OPS72 — skill step, discretionary): before push, run the e2e specs you created + the same affected surface — pnpm test:e2e:affected runs e2e-affected.mjs against origin/main, migrates/seeds, then pnpm test:e2e with the selected specs (full suite on high-risk diffs). High-risk diffs diverge from PR CI by design: the PR runs no e2e (selected only, never full) and the local mirror runs the full suite. Forward extra Playwright args after -- for a narrowed repro. Opt into production mode with E2E_PROD=1 pnpm test:e2e:affected (CI and prod mode serve the production build via pnpm start; plain local runs use pnpm dev — dev-mode flakes like cold-compile timeouts and dev-only console warnings do not reproduce in prod mode).

Navigation discipline in e2e (miss #54, 2026-07-30): page.goto is for cold loads and URL contracts (deep links, redirects, legacy params); when the scenario is continuity inside the authenticated shell — especially after a heavy list route — navigate in-shell instead (sidebar links, buttons, form submits), the way the user does. Never fire two page.goto back-to-back with nothing settling the first navigation (an await expect(...), waitForURL, or interaction): the second aborts the first's in-flight RSC fetch (net::ERR_ABORTED, measured in campaignSavedFilters). The e2eNavigationConventions unit guard fails the build on unsettled goto pairs. Two sibling rules of the same family: wait for interactive state before clicking dynamic controls — toBeVisible proves SSR paint, not hydration (pre-hydration clicks no-op silently; checkRadixWhenHydrated is the precedent, P3-C class) — and gate WebAuthn ceremonies on expectCampaignBiometricsReady before the island's one-shot probe runs (miss #53). Dev-mode caveat (closed 2026-08-10, P4-L/OPS30): campaignLeaderships' chip-cell spec was failing deterministically under pnpm dev before B34 rewrote it; with the OPS30 prewarm/login hardening it ran ×3 green in dev + ×1 E2E_PROD=1 (evidence in TECH-DEBT).

Minimal test database (pnpm db:seed:minimal)

The integration suite and any agent/Cloud environment boot from a minimal synthetic database: schema from pnpm migrate (which seeds the 435-município catalog) plus pnpm db:seed:minimal (scripts/seed-minimal.mjs), an idempotent upsert of the rows the app refuses to run without:

  • the four fail-closed Consent keys (lideranca-autopreenchimento, apoiador-cadastro, apoiador-intencao-voto, whatsapp-inscricao);
  • one campaignUser per role (synthetic @teqo.invalid emails / 719999999Xx phones);
  • deterministic strategy pins (priority + expectedVotes) on 5 municípios (salvador-ze-1, camacari, feira-de-santana, vitoria-da-conquista, itabuna) — rows themselves come from the migration-seeded catalog;
  • 1 organization, 1 stateDeputy, 2 leadership, 3 supporter (all synthetic);
  • the campaignGoals global default.

The content contract is data in scripts/lib/seed-minimal-manifest.mjs, pinned by tests/unit/seedMinimalManifest.unit.spec.ts. Contract: a delivery that adds a migration, collection, Consent key or required boot data must update the manifest in the same PR — PR CI runs migrate → seed → int, so a broken seed fails the gate. The seed refuses non-local DATABASE_URL (same assertLocalDatabase guard family as the other seeds); agents and Cloud environments never set ALLOW_REMOTE_DB.

Safety Net Map

Modules the 2026-07 hardening phases touch, and where their behavior is pinned:

Module Pinned behaviors Test files Gaps
RBAC — src/utilities/access/* (split 2026-07-23; campaignAccess.ts is the re-export surface) per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry campaignMunicipalityAccess, campaignVotePledge, campaignSupporter, campaignUserPhone, campaignActionPlan, campaignDemandWorkflow, collectionAccessLockdown (int) no dedicated per-export suite; covered through collection behavior
Collection access lockdown campaign JWT denied on users/PII/CMS writes; anonymous read of public content; admin unaffected collectionAccessLockdown.int.spec.ts REST-transport variant untested (Local API with overrideAccess: false is equivalent)
municipalityMapData.ts bundle scoping by URL filters + advisor access; validVotesByYear incl. 2026 reuse of 2022; null on empty scope; hasZoneMunicipalities + per-zone map keys municipalityMapData.int.spec.ts
votePledgeData.ts estimated[scenario] ?? declared aggregation; role scoping; leader denial campaignVotePledge.int.spec.ts
Supporter import (token/bulk) preview→confirm flow, dedup by phone, consent fail-closed campaignSupporter.int.spec.ts supporterImportBulk/supporterImportToken internals only via flow
Leader supporter create linked-municipality scope; out-of-scope rejection; staff rejection collectionAccessLockdown.int.spec.ts
Posts visibility/caching isPostVisible fail-closed; cached listing (indirect via routes) no dedicated posts spec — backlog
List URL contracts (Pass 2 W0, pinned 2026-07-25) campaignListUrl canonicalize/clamp/redirect; supporter + actionPlan parse/serialize/href; 5 entity parsers campaignListUrl, supporterUi, actionPlanUi, campaignEntityListParsers (unit) municipality list URL pinned separately in municipalityUi.unit.spec.ts
Form-action ladders (Pass 2 W4d) runCampaignFormAction: success spread, boundary/zod/safe/generic mapping, values+revision echo campaignFormActions.unit.spec.ts per-route ladders are thin shells; behavior pinned at the wrapper
WhatsApp subscription consent (Pass 2 D3) fail-closed without whatsapp-inscricao; happy path links subscription → keyed consent submitWhatsapp.int.spec.ts
Vote trend + election number format (Pass 2 W4a survivors) computeVoteTrend pair priority/±10% band; formatElectionNumber E18 fraction defect voteTrend.unit.spec.ts
Entity list loaders (Pass 2 W0) scope (leader empty/denied), marker-narrowed q, kind/status filters, pagination shape campaignEntityListData.int.spec.ts advisor loader runs post-gate with overrideAccess: true (gate asserted upstream)
Codebase conventions (tooling guards, 2026-07-25) server-only on Payload-coupled utilities; Praça/Núcleo copy ban; eslint legacy-ignore existence; formActions wrapper usage codebaseConventions.unit.spec.ts boundary rules themselves live in ESLint (no-restricted-imports) + madge in CI

Characterization Backlog

  • src/utilities/posts.ts — dedicated spec for isPostVisible fail-closed matrix and cache wrappers (confirmed 2026-07-28, Pass 3 audit).
  • activityViewModels.ts (ex-actionPlanViewModels, renamed by C13) — activityUi.ts pinned 2026-07-25 (Pass 2 W0); view models still untested directly (confirmed 2026-07-28, Pass 3 audit).
  • campaignDemandData.ts / leadershipData.ts / organizationData.ts / stateDeputyData.ts list loaders — smoked 2026-07-25 (campaignEntityListData.int.spec.ts, Pass 2 W0).
  • Election aggregate artifact (Phase 2): snapshot test pinning artifact ↔ municipalityCatalog key consistency — delivered as bahiaElectionAggregates.unit.spec.ts.