πŸ§ͺ Testing & QA

πŸ“‹ Summary

Midshift has a serious, well-engineered automated test effort for a product of its size: 687 fast unit tests that all pass in about 16 seconds, a browser-driven "journey" suite of roughly 780 tests that exercises every screen against a simulated back end (467 of the 483 tests that completed in this audit passed; the remaining failures are an expired-date test and one layout check, not product defects), database policy tests, and a scripted acceptance walk that five test accounts run against a live site. The business-logic library is well covered (67% of its lines, and 85–100% for the most important parsing, matching and link-building rules). The weakness is where the tests do not reach: the server's API routes, where permissions are checked and data is written, have under 1% unit coverage, and the browser tests run against an in-memory imitation of those routes rather than the real ones, so they cannot catch a permission bug in the real code. The two most serious security findings in this report (Module 9) sit in exactly that gap. The browser suite and the database tests are also not part of the automated pipeline; they run only when someone runs them by hand.

🎯 Rating

🟑 ●●●○○ 3 / 5 β€” Amber. Strong unit testing of domain logic and broad UI journey coverage, but the authorisation and write path β€” the riskiest code β€” is effectively untested against the real implementation, and the slower suites are not enforced in CI.

πŸ” Findings

1. Test runs (measured 2026-10-07)

SuiteCommandResultDurationTag
Unit (Vitest 3.2.7)npx vitest run --coverage70 files, 687 tests, 687 passed, 0 failed, 0 skipped15.7 s (17 s wall)[Observed]
Journey (Playwright 1.62.1, QA harness)npm run journeys24 of 25 files completed: 467 passed, 7 failed, 9 skipped. _ui-metrics (293 page-walk tests) did not finish within two time-boxed runs (86 min and ~78 min, single worker as the runner enforces)~45 min for the 24 files; > 2.5 h in total[Observed]
Live end-to-end (e2e/*.spec.ts, 8 specs)playwright test with BASE_URLNot runβ€”[Unavailable β€” requires a reachable deployment and test credentials; some specs write data]
Acceptance walk (e2e/walk/, 15 files).github/workflows/walk.yml (manual)Not runβ€”[Unavailable β€” requires a deployment and five persona accounts]
RLS / data-migration SQL tests (supabase/tests/)scripts/rls-check.sh, scripts/data-migration-check.shNot runβ€”[Unavailable β€” needs a local PostgreSQL server; only the psql client is installed in the audit environment]
Typecheck / lint (static gates)tsc --noEmit, eslintClean (exit 0, 0 warnings)β€”[Observed]

Flaky/skipped tests: none in the unit suite. The live e2e and walk specs contain 34 conditional test.skip(...) guards that skip teardown or dependent steps when an earlier step did not run β€” legitimate, not suppressed failures. [Observed] grep -rn "test.skip". No .only committed. [Observed]

Journey suite β€” failures and skips explained

FileResultCauseProduct defect?
chain-dates-timeline6 failed, 2 skipped, 30 passedThree tests (each at two viewport widths) expect a fixture job starting 5 Oct 2026 to read "In production" (start date in the future). The status derivation in src/lib/campaign-derive.ts:56 uses the real system clock instead of the harness's pinned date (NEXT_PUBLIC_QA_TODAY=2026-09-04, read through todayIso()), so since 5 Oct 2026 the job reads "Active" and the dependent steps cascade. A time-bomb that breaks the project's own "one calendar" rule (CLAUDE.md, Verification). Skips are the chain's a11y step after the failures.❌ No β€” test/clock inconsistency; but it shows status derivation and the harness disagree about "today" [Observed]
chain-help1 failed, 11 passedLayout-spacing assertion on Help sub-headings ("a sub-heading sits nearer its own lines than the block above it"); reports e.g. "32 above, 8 to its lines". Cause not diagnosed β€” possibly the assertion's own logic or font rendering in this container.Cosmetic at most [Observed failure; cause Inferred-unknown]
chain-home-tracking7 skipped, 26 passedTests skip themselves when the Ask button is absent: "Ask is parked until the API exists". The README advertises Ask as a feature; it is not rendered in the QA build.Possible documentation drift β€” production exposure [Unavailable]
_ui-metricsNot completed293 page-walk tests on one worker; exceeded 86 min and ~78 min budgets[Unavailable β€” needs a longer run or a sharded CI job]

That a date-bound test began failing two days before this audit, and nobody noticed, is itself evidence that the journey suite is not run routinely (it is not in CI). [Inferred]

2. Measured coverage (V8, unit suite, src/**)

ScopeFilesLine coverage
All of src/314 instrumented11.71% (4,743 / 40,478); branches 83.9%; functions 39.3%
src/lib (domain logic)9267.1%
src/app/api (route handlers)870.9%
src/app pages / views1471.8%
src/components3010.3%
src/app/qa (test harness)3615.3%

Business-critical modules (selected from Module 4):

ModuleLine %Branch %Business role
copy-deck-parser.ts85.082.8Turns copywriters' decks into ad units
asset-match.ts98.296.0Filename β†’ unit join
utm-template.ts100100Every tracking link
approval-events.ts100100When an approval is reopened
copy-import.ts100100What a deck may overwrite
retention.ts100100When files may be purged
custody.ts100100Handover custody
final-grid.ts98.985.9Go-live checks
plan-change.ts96.088.9Ad-name construction
discipline.ts90.597.1Who edits which area
branding.ts76.394.7White-label identity
api-auth.ts3.6β€”All route authorisation
activity.ts5.6β€”Audit trail

Note: the 11.7% headline understates protection of UI code, which the journey suite exercises in a browser; V8 coverage from that suite was not collected. [Observed / Inferred]

3. Test pyramid

LayerVolumeRuns againstIn CI?
Unit (pure functions + static "contract" tests)687 testsCode in isolationβœ… Yes
Static contract tests (write-results.test.ts 13 cases, schema-columns.test.ts 10 cases, scripts/harness-contract.js)β€”Source text and baseline.sqlβœ… Yes
Integration β€” database policiessupabase/tests/040_rls.sql (187 lines) + data-migration fixture/assert sets for 062–064Throwaway Postgres built from baseline.sql❌ No (manual)
UI journeys~780 tests in 25 files (483 executed to completion here)Real built Next.js app + in-memory mock store (src/app/qa/store.ts) instead of Supabase❌ No (local; documented as ~30 min, measured > 2.5 h in this container)
End-to-end, live8 specs (~20 tests) + 15-file acceptance walk (28 tests)Real deployment❌ No (manual dispatch)

The shape is a reasonable pyramid at the bottom and top, with a hollow middle: there are no automated tests that execute the real route handlers against a real (even local) Supabase. [Observed] The journey suite substitutes a hand-written emulation of all 125 handlers; scripts/harness-contract.js checks that every route has a twin, not that the twin behaves like the route. [Observed] The QA store re-implements the authorisation helpers (src/app/qa/store.ts:761–822, Module 4), so a bug in the real api-auth.ts is invisible to the browser tests. [Observed]

4. Test quality

  • Behavioural, specification-style assertions. The parser baseline test asserts the agreed parse of an invented 22-unit deck β€” unit count, channel histogram, card labels, Journey states β€” with comments explaining why each assertion exists. [Observed] src/lib/copy-deck-parser.baseline.test.ts:1–40.
  • Tests as guard rails over the codebase itself. write-results.test.ts fails if any awaited write's result is unread or a DELETE does not count rows; schema-columns.test.ts fails if code names a column absent from the schema. These encode past production bugs as permanent checks β€” a mature practice. [Observed]
  • "Plant once" discipline β€” HANDOVER.md requires each fixed rule to get a test that is proven to fail when the rule is broken. [Observed] docs/HANDOVER.md "How a change lands".
  • Fixed calendar for the harness (NEXT_PUBLIC_QA_TODAY=2026-09-04) avoids date-rot flakiness. [Observed] e2e/journeys/qa-server.env.
  • Accessibility is checked in the journey suite with axe (_a11y.spec.ts). [Observed]

5. Are the critical business rules covered?

Critical rule area (Module 4)Unit-tested?Real-route tested?
Permissions (canEditCampaign, canEditArea, canApprove)❌ No (3.6%)❌ No β€” harness re-implements them
Approval reset on copy change (route composition)Helper yes; route No❌ No
Copy-deck parse and import policyParser yes (85%); route No❌ No
Retention / purgeHelper yes (100%); cron and ingest routes No❌ No
Naming and asset matchingβœ… Yes (96–98%)❌ No
RLS policiesSQL test exists for 040 scope; no case for users self-update β€” the Critical finding in Module 9Manual only

29 of the 75 rules catalogued in Module 4 have no unit test and 9 are partially tested. [Observed] Module 4 table.

6. QA process signals

SignalEvidenceAssessment
CI test gateci.yml runs unit tests on every push/PR, each gate reported independentlyGood [Observed]
Local gate scriptnpm run gates β€” six gates by exit codeGood [Observed]
Test environmentsHarness (no DB), live deployment (manual), throwaway Postgres (manual)No shared staging environment evidenced [Inferred]
Environment parityHarness β‰  production by design; per-client deployments migrated by hand, so production schemas may differ from baseline.sql (Module 12)Weak [Observed]
Testing rounds with real usersdocs/TESTING_ROUND.md, feedback widget triaged in Admin β†’ ActivityPresent [Observed]
Coverage thresholdsNone configuredGap [Observed]

🧾 Evidence

  • npx vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.include='src/**' β†’ 70 files, 687 passed; Statements 11.71% (4,743/40,478), Branches 83.94%, Functions 39.25%.
  • Per-area and per-file coverage computed from coverage-summary.json.
  • npm run journeys (scripts/journeys.sh): one build, one server per spec file, one worker; per-file logs /tmp/journeys-3100-*.log.
  • grep -rhoE "^\s*test\(" e2e/journeys β†’ 532; e2e/walk β†’ 28; ls e2e/*.spec.ts β†’ 8.
  • grep -n "users" supabase/tests/040_rls.sql β†’ users rows created as fixtures only; no self-update case.
  • which psql postgres pg_ctl β†’ only psql present.

⚠️ Risks

RiskSeverityEffort
Authorisation and write routes untested against the real implementation; the harness re-implements the rules, so it cannot detect real permission bugs (Module 9's Critical and High findings went undetected)🟠 HighM
Journey, RLS and live suites not enforced in CI β€” they protect only when someone remembers to run them🟠 HighM
No integration environment running real routes against a real Supabase; parity between harness, baseline and each production database unverified🟑 MediumM
No coverage thresholds β€” coverage can fall silentlyβšͺ LowS
Harness store (3,349 lines) is itself untested logic that can drift from the routes it imitates🟑 MediumM

βœ… Recommendations

RecommendationPriorityEffort
Add route-level integration tests that run real handlers against a local Supabase (Supabase CLI in CI), starting with api-auth.ts, members, ad-unit PATCH, brief/campaign PATCH, share-link and cron routesP1M
Add RLS test cases for every table a user client can write β€” users firstP0S
Run the journey suite in CI (sharded, nightly at minimum) and the RLS check on every migration changeP1M
Stand up a shared staging deployment with its own Supabase project for the live e2e and walkP2M
Set coverage floors for src/lib (β‰₯ 65%) and a rising floor for src/app/apiP3S