chore: replace AI skill library with 16 verified mvp-* skills
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Removes the old planner/decision-critic/role-agents/domain-agents system (step-injector scripts, stale scopes, sprint-era workflow) and replaces it with 16 ground-truth-verified skills under .claude/skills/: change control, debugging playbook, failure archaeology, architecture contract, domain reference, OCR/Gemini pipeline, config and secrets, build and env, run and operate, diagnostics (with tested scripts), validation and QA, docs and writing, launch readiness, deploy-safety campaign, proof and analysis toolkit, and research frontier. RULE 0/1/2, the temporal-contamination rule, and the decision stress-test protocol are carried forward into the new skills; the retired content remains in git history. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e729d425fd
commit
c239bb9347
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: mvp-validation-and-qa
|
||||
description: >-
|
||||
Load before claiming any MotoVaultPro change is "done", "tested", or "ready to merge";
|
||||
before running or adding tests; when wondering whether CI ran the tests (it did not);
|
||||
when integration tests could wipe the dev database (DROP TABLE CASCADE); or when you
|
||||
need the mobile+desktop verification procedure (320/768/1920px on staging). Defines the
|
||||
evidence bar and definition of done, maps the real test suites, and gives exact commands
|
||||
to run and add tests. Jest failure mechanics ("jest did not exit", tdd-guard-jest
|
||||
reporter errors, "npm test fails at repo root") are homed in mvp-build-and-env; raw
|
||||
symptom triage starts in mvp-debugging-playbook.
|
||||
---
|
||||
|
||||
# Validation and QA: what counts as evidence
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you are about to run tests, write tests, verify a change, or declare work complete on MotoVaultPro.
|
||||
|
||||
Do NOT use it for:
|
||||
- Issue/branch/PR mechanics, labels, or the RULE 0/1/2 review taxonomy — see `mvp-change-control`.
|
||||
- Diagnosing a live failure (logs, Grafana, container debugging) — see `mvp-debugging-playbook` and `mvp-diagnostics-and-logging`.
|
||||
- Deploy/rollback procedure or staging environment operations — see `mvp-run-and-operate`.
|
||||
- Setting up a machine or understanding why local dev is limited — see `mvp-build-and-env`.
|
||||
- Making CI actually enforce any of this — that is the campaign in `mvp-deploy-safety-campaign`.
|
||||
|
||||
## 1. The evidence bar (read this first)
|
||||
|
||||
**CI gates nothing beyond build + boot.** Verified 2026-07-07 against `.gitea/workflows/staging.yaml`: the only PR-triggered workflow builds 3 Docker images (backend, frontend, ocr — TypeScript compiles because `npm run build` runs inside the Dockerfiles), deploys them over the shared staging environment, and asserts 5 containers pass health checks and `https://staging.motovaultpro.com/api/health` reports `healthy` with 13 required features. **Zero tests, zero lint, zero type-check-as-gate, zero security scan, zero viewport check.** A "green" PR proves only that the code compiles and the app boots.
|
||||
|
||||
Consequence: **validation is entirely the author's job.** If you did not run it, it did not run. Anywhere docs imply CI runs "integration tests" or "viewport validation" (root `CLAUDE.md` does), the code wins: it does not.
|
||||
|
||||
### Definition of done for any change
|
||||
|
||||
Every box, personally executed, before requesting merge:
|
||||
|
||||
- [ ] `npm run lint` green in every touched workspace (`backend/`, `frontend/`; run from that directory — root `package.json` has NO scripts)
|
||||
- [ ] `npm run type-check` green in every touched workspace
|
||||
- [ ] Unit tests no worse than the known-red main baseline in every touched workspace, and green for everything your change touches (section 4 says where each suite can actually run)
|
||||
- [ ] NEW tests written for new behavior — not just existing tests still passing
|
||||
- [ ] Mobile AND desktop verified per the procedure in section 2 (hard project requirement; no CI substitute exists)
|
||||
- [ ] Feature verified end-to-end ON STAGING via the PR deploy, before merge. Every PR push auto-deploys to `https://staging.motovaultpro.com` (last push wins on the shared environment). There is no fully working local dev loop, so staging IS the end-to-end environment.
|
||||
- [ ] Old code deleted — replacement means removal, not accumulation
|
||||
- [ ] PR template test-plan checkboxes filled in truthfully (they are the only artifact of testing; see section 6)
|
||||
|
||||
The human owner's RULE 0/1/2 review (see `mvp-change-control`) substitutes for CI today. Do not make the reviewer discover that a checkbox was aspirational.
|
||||
|
||||
## 2. Mobile + desktop verification procedure
|
||||
|
||||
"ALL features MUST be implemented and tested on BOTH mobile and desktop" is a hard project requirement, and there is no viewport CI of any kind. The procedure, run against staging after your PR deploys:
|
||||
|
||||
**Widths to check** (project convention, from the repo's agent definitions): **320px** and **768px** (mobile), **1920px** (desktop). Use browser devtools responsive mode against `https://staging.motovaultpro.com`.
|
||||
|
||||
**The mobile fork is a separate code path, not CSS.** `frontend/src/App.tsx` sets `mobileMode` when `window.innerWidth <= 768` (or a mobile user agent on resize) and then renders dedicated `*MobileScreen` components driven by a Zustand screen switcher — not the desktop router. So the first mobile question is existence, not layout: **does your feature exist in the mobile screen switcher at all?**
|
||||
|
||||
A new page/screen must run the CANONICAL registration checklist in `mvp-architecture-contract` invariant 2.7 (7 steps covering desktop route, mobile union/maps, lazy import, render block, and navigation entry point) — miss one and the feature is silently absent or unreachable on one platform. The two steps most often missed on mobile are the lazy import and the navigation entry point: a screen can be fully "registered" in the maps yet have no way to reach it.
|
||||
|
||||
**Checklist per viewport:**
|
||||
|
||||
- 320px: feature reachable via bottom nav or hamburger drawer; no horizontal scroll; touch targets usable; forms submit; screen switcher shows the screen.
|
||||
- 768px: still mobile mode (`<= 768` is mobile) — verify the boundary renders correctly.
|
||||
- 1920px: desktop component path renders; feature reachable via desktop navigation; layout uses the space.
|
||||
- Both paths: exercise the actual flow (create/edit/delete), not just "the page loads".
|
||||
|
||||
Only 3 frontend unit tests simulate viewports (by mocking `matchMedia`/`useMediaQuery`); unit tests do not discharge this requirement. Manual verification on staging does.
|
||||
|
||||
## 3. Test-suite reality map (verified 2026-07-07)
|
||||
|
||||
| Workspace | Files | Cases | Runs where | Hazards |
|
||||
|---|---|---|---|---|
|
||||
| `backend/` | 37 `.test.ts` | ~500 | Unit: host or container. Integration: container only | Integration tests are DESTRUCTIVE (below); jest hangs without `--forceExit` |
|
||||
| `frontend/` | 31 in `src/` | 279 | Host only, via `npx jest --reporters=default` (see `mvp-build-and-env`). No container run exists: the shipped image is nginx-only and `.dockerignore` excludes `*.test.*` | `npm test` broken on host (tdd-guard-jest reporter + hardcoded Linux `projectRoot`); known-red baseline on main (`mvp-deploy-safety-campaign` Phase 0.3) |
|
||||
| `ocr/` | 16 `test_*.py` | ~310 | Host with Python env, or container | Never run in CI, ever. All google-genai/Gemini calls are mocked — green OCR tests prove nothing about live Gemini |
|
||||
|
||||
Facts that will surprise you:
|
||||
|
||||
- **Integration tests are destructive.** 2 of the 10 backend integration suites (vehicles, admin) run real migration SQL in `beforeAll` and `DROP TABLE ... CASCADE` in `afterAll` against whatever database the pool points at (e.g. `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts`: `DROP TABLE IF EXISTS vehicles CASCADE` plus a function drop). Most of the others run `DELETE FROM` against live tables (`stations.api.test.ts:35` wipes `station_cache` unscoped); `fuel-logs.integration.test.ts` is an inert stub (no real DB work). Treat the whole set as destructive. Owner non-negotiable: **no destructive database operation without a fresh backup.** Run them only against a disposable database, and `make db-backup` first if there is any chance the pool points at data you care about. Suites can also interfere with each other (shared tables).
|
||||
- **Orphaned tests never run.** `frontend/test/fuel-logs/{FuelLogForm,useFuelGrades}.test.tsx` sit outside jest `roots: ['<rootDir>/src']` and are silently skipped by every run. FuelLogForm — the highest-traffic form in the app — has NO other tests. (The `frontend/test/__mocks__/` files ARE used, via `moduleNameMapper`; only the test files are dead.) Do not add tests under `frontend/test/`.
|
||||
- **8 of 21 backend feature capsules have zero test files** (verified 2026-07-07): `email-ingestion`, `notifications`, `onboarding`, `ownership-costs`, `subscriptions`, `terms-agreement`, `user-preferences`, `user-profile`. None of these have frontend tests either. Touching them means writing the first test.
|
||||
- **Zero skipped tests project-wide.** No `.skip`/`.only`/`xit`/`pytest.mark.skip` anywhere. Keep it that way — a skipped test here is invisible debt with no CI to surface it.
|
||||
- **The lone Cypress spec is unrunnable.** `frontend/cypress/e2e/stations.cy.ts` has no `cypress.config.*` anywhere, no cypress dependency in any `package.json`, uses an undefined `cy.login()` command, and references an `npm run e2e` script that does not exist. Do NOT model new e2e work on it; there is no working e2e framework in this repo today (root `@playwright/test` dependency is equally unused — no config, no specs).
|
||||
- **Root `package.json` has no scripts.** `npm test` at repo root fails. Always `cd backend/`, `cd frontend/`, or `cd ocr/` first. `make lint` and `make type-check` fan out to backend+frontend on the host and work.
|
||||
|
||||
## 4. How to run each suite
|
||||
|
||||
### Backend
|
||||
|
||||
Unit tests work on the host (node_modules present). Jest does NOT exit on its own — the module-level pg pool in `backend/src/core/config/database.ts` and the Redis client hold open handles. Always pass `--forceExit` locally:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm test -- --testPathPattern=src/features/vehicles/tests/unit --forceExit # one feature, unit only
|
||||
npm test -- --testPathPattern=src/features/stations --forceExit # whole feature
|
||||
npm run test:feature --feature=vehicles # same filter via npm config (add -- --forceExit)
|
||||
npm run lint && npm run type-check # both work on host
|
||||
```
|
||||
|
||||
Integration tests need real Postgres+Redis and container config paths (the config loader defaults to `/app/config/production.yml` and `/run/secrets`) — and they are currently runnable NOWHERE as-shipped. The shipped backend image is the production stage (`npm ci --omit=dev`, no jest; only `dist/` plus migration SQL copied), so docs/TESTING.md's `make shell-backend` + `npm test` recipe fails with "jest: not found" — that doc is stale here. The only viable paths are a builder-stage image or a host run against an ephemeral Postgres/Redis: see `mvp-deploy-safety-campaign` 1B. Whatever the vehicle: `make db-backup` first (non-negotiable — see the destructive-tests bullet above).
|
||||
|
||||
### Frontend
|
||||
|
||||
`npm test` is broken on the host (tdd-guard-jest reporter + hardcoded Linux `projectRoot`), and there is NO container alternative: the shipped frontend image is the nginx production stage (no node) and `frontend/.dockerignore` excludes `*.test.*` from every stage, so `docker compose exec mvp-frontend npm test` (still documented in docs/TESTING.md) cannot work. The verified host fallback — and the single home for its mechanics and traps — is `mvp-build-and-env` section 2:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx jest --reporters=default # full suite (path BEFORE flags for single files)
|
||||
npx jest --reporters=default --testPathPattern=src/features/stations
|
||||
npm run lint && npm run type-check # these work normally on the host
|
||||
```
|
||||
|
||||
Expect the known-red baseline on main (17 failing tests as of 2026-07-09; numbers homed in `mvp-deploy-safety-campaign` Phase 0.3) — diff your run against it rather than expecting green.
|
||||
|
||||
### OCR
|
||||
|
||||
```bash
|
||||
cd ocr
|
||||
python -m pytest # needs a Python env with ocr/requirements.txt installed (pytest>=7.4.0 is in it)
|
||||
python -m pytest tests/test_gemini_engine.py -v
|
||||
```
|
||||
|
||||
No `pytest.ini`/`pyproject.toml`; tests import `from app.main import app`, so run from `ocr/`. Remember: CI never runs these — an OCR change with green local pytest has still never been machine-checked anywhere else.
|
||||
|
||||
### Everything cheap at once (host)
|
||||
|
||||
```bash
|
||||
make lint && make type-check # backend + frontend lint and tsc
|
||||
```
|
||||
|
||||
## 5. How to add tests
|
||||
|
||||
### Conventions
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| Backend unit tests | `backend/src/features/{name}/tests/unit/*.test.ts` |
|
||||
| Backend integration tests | `backend/src/features/{name}/tests/integration/*.test.ts` |
|
||||
| Alternate backend convention (audit-log uses it) | `backend/src/features/{name}/__tests__/*.test.ts` |
|
||||
| Backend fixtures | `backend/src/features/{name}/tests/fixtures/` (JSON or TS: `fuel-logs.fixtures.json`, stations' `mock-google-response.ts`) — otherwise inline mock objects; both are established |
|
||||
| Frontend tests | `frontend/src/features/{name}/__tests__/` or co-located `*.test.tsx` — MUST be under `src/` (jest roots) |
|
||||
| Backend core/cross-cutting | `backend/src/core/**` (e.g. `core/config/tests/feature-tiers.test.ts`) |
|
||||
| OCR | `ocr/tests/test_*.py`; images generated inline with PIL, no golden files |
|
||||
|
||||
### Route tests: fastify inject pattern
|
||||
|
||||
Do not spin up a listener. Build the app and inject (established pattern, e.g. `backend/src/features/audit-log/__tests__/audit-log.routes.test.ts`):
|
||||
|
||||
```typescript
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
let app: FastifyInstance;
|
||||
beforeAll(async () => {
|
||||
const { default: buildApp } = await import('../../../app');
|
||||
app = await buildApp();
|
||||
});
|
||||
afterAll(async () => { await app.close(); });
|
||||
|
||||
it('rejects requests without a valid token', async () => {
|
||||
const response = await app.inject({ method: 'GET', url: '/api/admin/audit-logs' });
|
||||
expect(response.statusCode).toBe(401); // JWT plugin rejects before any admin check
|
||||
});
|
||||
```
|
||||
|
||||
To bypass JWT auth, mock the auth plugin before importing the app (see the top of `vehicles.integration.test.ts`: `jest.mock('.../core/plugins/auth.plugin', ...)` decorating `authenticate` to set `request.user`). Caveat on the reference file: its test named "should return 403 for non-admin users" actually sends a bogus bearer token and asserts 401 — the name is misleading; the codebase has no example asserting a real 403 here. Do not copy an `expect(403)` from that name.
|
||||
|
||||
### Frontend mock strategy
|
||||
|
||||
`frontend/jest.config.ts` `moduleNameMapper` **auto-mocks the API client**: any import resolving to `core/api/client` is replaced by `frontend/src/core/api/__mocks__/client.ts`. Components under test never hit the network; configure responses through that mock. CSS and image imports are mapped to `frontend/test/__mocks__/{styleMock,fileMock}.js`.
|
||||
|
||||
### What every new repository test MUST cover: numeric coercion round-trip
|
||||
|
||||
This is the project's recurring bug class: node-postgres returns PostgreSQL `NUMERIC`/`DECIMAL` columns as **strings**, and every repository hand-rolls `parseFloat` coercion in its `mapRow()`. Missed coercion shipped at least three separate bugfix PRs (#241, #244 era). Any new repository method returning a numeric column needs a test proving strings-in, numbers-out. Copy-pasteable, in the established style (mocked `pg.Pool`, matching `vehicles.repository.test.ts`; verified against `FuelLogsRepository` as it exists today):
|
||||
|
||||
```typescript
|
||||
import { Pool } from 'pg';
|
||||
import { FuelLogsRepository } from '../../data/fuel-logs.repository';
|
||||
|
||||
describe('FuelLogsRepository numeric coercion', () => {
|
||||
let pool: Pool;
|
||||
let repository: FuelLogsRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
pool = { query: jest.fn() } as any;
|
||||
repository = new FuelLogsRepository(pool);
|
||||
});
|
||||
|
||||
it('coerces DECIMAL columns (returned as strings by node-postgres) to numbers', async () => {
|
||||
(pool.query as jest.Mock).mockResolvedValue({
|
||||
rows: [{
|
||||
id: 'log-1',
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'veh-1',
|
||||
date: '2026-07-01',
|
||||
odometer: 42000,
|
||||
gallons: '12.345', // pg gives NUMERIC back as string
|
||||
price_per_gallon: '3.599',
|
||||
total_cost: '44.43',
|
||||
station: null, location: null, notes: null,
|
||||
created_at: new Date(), updated_at: new Date(),
|
||||
}],
|
||||
});
|
||||
|
||||
const log = await repository.findById('log-1');
|
||||
|
||||
expect(log!.gallons).toBe(12.345); // number, not '12.345'
|
||||
expect(log!.pricePerGallon).toBe(3.599); // and snake_case -> camelCase
|
||||
expect(log!.totalCost).toBe(44.43);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Assert on the mapped camelCase field names — that simultaneously tests the mandatory `mapRow()` snake_case-to-camelCase conversion. If the method under test returns raw-ish rows (the fuel-logs "enhanced" path does, via `mapEnhancedRow`), still assert `typeof value === 'number'` on every decimal field.
|
||||
|
||||
## 6. Coverage and enforcement reality
|
||||
|
||||
- **No coverage thresholds exist.** Neither `backend/jest.config.js` nor `frontend/jest.config.ts` has `coverageThreshold` (verified 2026-07-07). Backend collects coverage (`text`/`lcov`/`html` reporters) but nothing fails on any number. Do not claim "coverage-gated" anywhere.
|
||||
- **The honor system is the PR template.** `.gitea/PULL_REQUEST_TEMPLATE.md` test-plan checkboxes (`Unit tests` / `Integration tests` / `Manual verification` plus a Commands/steps list) are the only testing artifact a PR carries. Fill in the actual commands you ran; the reviewer has no other signal.
|
||||
- **Want CI to enforce any of this?** That is exactly the scope of `mvp-deploy-safety-campaign` (test/lint gates, SHA-pinned deploys). Until it lands, the RULE 0/1/2 human review in `mvp-change-control` is the only gate — write PRs that make that review easy.
|
||||
- Pre-launch calibration: a paying-user launch is the trajectory. Untested features (section 3's list of 8) touching billing (`subscriptions`) or user data are the highest-risk gap; adding first tests there outranks polishing well-tested capsules.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection. Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| CI runs zero tests/lint (build+boot only) | `grep -nE "npm (test|run lint)|jest|pytest|eslint" .gitea/workflows/*.yaml` (expect no hits) |
|
||||
| Root package.json has no scripts | `python3 -c "import json; print(json.load(open('package.json')).get('scripts'))"` |
|
||||
| Backend 37 test files / ~500 cases / 10 integration | `find backend/src -name "*.test.ts" \| wc -l` ; `find backend/src -path "*tests/integration*" -name "*.test.ts" \| wc -l` |
|
||||
| Frontend 31 files / 279 cases in src; 2 orphaned in frontend/test/ | `find frontend/src \( -name "*.test.ts" -o -name "*.test.tsx" \) \| wc -l` ; `ls frontend/test/fuel-logs/` |
|
||||
| OCR 16 files / ~310 tests | `find ocr/tests -name "test_*.py" \| wc -l` ; `grep -rE "^\s*def test_" ocr/tests \| wc -l` |
|
||||
| 8 backend capsules with zero tests | `for f in backend/src/features/*/; do echo "$(basename $f): $(find $f -name '*.test.ts' \| wc -l)"; done` |
|
||||
| Frontend jest broken on host (tdd-guard-jest reporter, hardcoded projectRoot) | `grep -n "tdd-guard-jest\|projectRoot" frontend/jest.config.ts` then `cd frontend && npm test` |
|
||||
| Integration tests DROP TABLE CASCADE | `grep -rn "DROP TABLE" backend/src --include="*.test.ts"` |
|
||||
| No coverageThreshold anywhere | `grep -rn coverageThreshold backend/jest.config.js frontend/jest.config.ts` (expect no hits) |
|
||||
| Mobile breakpoint <= 768; registration checklist (canonical: `mvp-architecture-contract` 2.7) | `grep -n "innerWidth <= 768" frontend/src/App.tsx` ; `grep -n "MobileScreen\b" frontend/src/core/store/navigation.ts` |
|
||||
| Cypress spec still unrunnable | `find . -name "cypress.config.*"` (expect none) ; `grep -rn cypress frontend/package.json package.json` |
|
||||
| Zero skipped tests | `grep -rnE "\.skip\(|\.only\(|xit\(|xdescribe\(" backend/src frontend/src; grep -rn "pytest.mark.skip" ocr/tests` |
|
||||
| Staging health gate: 13 features | `grep -n REQUIRED_FEATURES .gitea/workflows/staging.yaml` |
|
||||
Reference in New Issue
Block a user