--- name: mvp-build-and-env description: Load when starting from a fresh MotoVaultPro checkout or when any build/test/install command fails locally. Triggers - "how do I run this", "npm test fails", "Could not resolve a module for a custom reporter", "tdd-guard-jest", "Jest did not exit one second after the test run", jest hangs, "Configuration file not found at /app/config/production.yml", "make setup fails", secrets are directories, "mobile-setup nothing to be done", pytest/paddleocr install, Dockerfile build questions, "why is my migration not on staging". Covers what works on the dev machine vs container-only, the exact local test loop per workspace, Docker build anatomy, and known environment traps. --- # Build and Environment Reality All commands below were verified by running them or reading the exact source on 2026-07-07. Where docs contradict code, code wins; this file states code behavior. ## When to use / When NOT to use Use this skill when you have a fresh checkout and need to build, lint, type-check, or run tests, or when a local command fails in a confusing way. Do NOT use this skill for: - Deploying, rollback, staging/prod operations, backups: `mvp-run-and-operate` - What counts as test evidence, adding tests, mobile+desktop validation: `mvp-validation-and-qa` - Issue/branch/PR workflow and review rules: `mvp-change-control` - Runtime failures of a deployed stack: `mvp-debugging-playbook` - Config keys, secrets catalog, feature tiers: `mvp-config-and-secrets` ## 1. The development model (read this first) Development on this project is done by AI sessions working in this repo; the human owner reviews PRs. There is NO fully working local dev loop and none is expected. The loop is: 1. Edit code locally. Run unit tests + lint + type-check + build locally (per workspace, below). 2. Open a PR. CI builds 3 Docker images and deploys them to staging. That is the entire gate — no tests, no lint (canonical statement: `mvp-validation-and-qa` section 1). The only compile check CI performs is `tsc` inside the Dockerfiles. 3. End-to-end verification happens ON STAGING (https://staging.motovaultpro.com) after the PR pipeline deploys. See `mvp-validation-and-qa` for the evidence bar. Consequence: anything your local commands do not catch, nothing catches before staging. Run the local gates every time; they are the only gates. ## 2. What works on the dev machine (verified 2026-07-07) Root `package.json` has NO scripts and no workspaces. `npm test` / `npm run lint` at repo root fail with "Missing script". Always cd into `backend/`, `frontend/`, or `ocr/`. ### Bootstrap ```bash make install # runs npm install in frontend/ AND backend/ (Makefile:220) # or individually: cd backend && npm install cd frontend && npm install ``` ### Dev-safe make targets (Makefile:218-254; these never touch Docker) | Target | Does | |---|---| | `make install` | `npm install` in frontend + backend | | `make type-check` | `npm run type-check` in frontend + backend | | `make lint` | `npm run lint` in frontend + backend | | `make build-local` | `npm run build` in frontend + backend (outputs `frontend/dist`, `backend/dist`) | Every other make target (`setup`, `start`, `rebuild`, `migrate`, `clean`, ...) drives Docker. Per root CLAUDE.md, `make setup`/`make rebuild` are for staging/prod-style builds, NOT development — and on this machine `make setup` does not produce a working stack anyway (section 3). ### backend/ — everything works ```bash cd backend npm run lint # eslint src (flat config eslint.config.js) npm run type-check # tsc --noEmit npm run build # tsc --project tsconfig.build.json -> dist/ npm test -- --forceExit # all unit tests npm test -- --forceExit --testPathPattern=src/features/fuel-logs # one feature npm run test:feature --feature=fuel-logs # same, needs --forceExit caveat too ``` `--forceExit` is MANDATORY locally. Without it jest prints "Jest did not exit one second after the test run" and hangs indefinitely (verified: killed after 8+ min; with `--forceExit` the same suite finishes in ~2 s). Cause: `backend/src/core/config/database.ts` creates a pg Pool eagerly and unit tests leave open handles. Do not "fix" a hang by waiting. Expect a RED baseline on main: 15 of 25 unit suites fail pre-existing (6 die loading real config, 7 fail ts-jest compilation, 2 contain the 2 genuinely failing tests; `Tests: 2 failed, 147 passed, 149 total`, re-verified 2026-07-09 — baseline detail homed in `mvp-deploy-safety-campaign` Phase 0.3). Judge your change against that baseline, not against zero. Why unit tests work at all locally: they mock `core/config/config-loader` and `core/config/redis` at the top of the test file (see `src/features/stations/tests/unit/station-matching.test.ts:5-15`). The real config-loader is an eager singleton (`config-loader.ts:281`: `export const appConfig = configLoader.load()`) that throws `Configuration file not found at /app/config/production.yml` if imported unmocked outside a container. If you see that error in a test, the test (or something it imports) is loading real config — mock it like the existing unit tests do. Integration tests (`src/features/*/tests/integration/`) are container-only AND destructive — see section 3. Broken script: `npm run migrate:feature` references `src/_system/migrations/run-feature.ts`, which does not exist (only `run-all.ts` does). It fails on invocation. ### frontend/ — build tools work, `npm test` is broken outside the container ```bash cd frontend npm run lint # works npm run type-check # works npm run build # works: tsc --project tsconfig.build.json && vite build npm run dev # vite dev server on :3000 (UI only; API calls need a backend) ``` `npm test` fails outside the container with: ``` Error: Could not resolve a module for a custom reporter. Module name: tdd-guard-jest ``` Two independent causes in `frontend/jest.config.ts:28-36`: 1. The `tdd-guard-jest` reporter is declared only in the ROOT `package.json` devDependencies, and no root `node_modules/` exists on a fresh checkout (root has no scripts, nobody runs `npm install` there), so the module cannot resolve from `frontend/`. 2. The reporter config hardcodes `projectRoot: '/home/egullickson/motovaultpro'` — a Linux path from the container/CI host, wrong on this macOS checkout even if the module resolved. WORKING FALLBACK (verified 2026-07-07, executes and reports results): ```bash cd frontend npx jest src/path/to/File.test.tsx --reporters=default # single file npx jest --reporters=default --testPathPattern=fuel-logs # by pattern npx jest --reporters=default # full suite ``` Trap: `--reporters` is greedy. `npx jest --reporters=default src/Foo.test.tsx` (positional AFTER the flag) swallows the path as a second reporter name and fails with "Could not resolve a module for a custom reporter. Module name: src/Foo...". Put the positional path FIRST, or use `--testPathPattern`. Expect the fallback run to be RED at baseline on main: `Tests: 17 failed, 196 passed, 213 total` across 14 failing suites (re-verified 2026-07-09; the baseline numbers are homed in `mvp-deploy-safety-campaign` Phase 0.3). Diff your run against that baseline — do not attribute the pre-existing failures to your change, and do not report "only 1 known failure". There is NO in-container alternative: the shipped frontend image is the nginx production stage (no node/npm) and `frontend/.dockerignore` excludes `*.test.*` from every stage, so `docker compose exec mvp-frontend npm test` (still documented in docs/TESTING.md) has never been able to work. This fallback plus staging verification is the whole frontend test story today. Also note: `frontend/test/` contains test files outside jest `roots: ['/src']` — they never run anywhere. See `mvp-validation-and-qa`. ### ocr/ — tests exist; running them locally is heavyweight and UNVERIFIED ```bash cd ocr python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt # includes paddleocr, google-cloud-vision, google-genai — multi-GB, slow python -m pytest tests/ ``` Verified facts only: `ocr/requirements.txt` lists `pytest>=7.4.0` and `pytest-asyncio>=0.21.0`; 16 test files exist under `ocr/tests/`; tests import `from app.main import app`; there is no venv, no `pytest.ini`, and no `conftest.py` in the repo. The pip install and pytest run above were NOT executed during authoring (paddleocr download is too heavy) — UNVERIFIED as a complete recipe. CI never runs OCR tests either (`ocr/Dockerfile` has no test step), so OCR test results always come from a manual run. ## 3. Container-only, and why ### Backend integration tests — DESTRUCTIVE, backup first (non-negotiable) `backend/src/features/*/tests/integration/*.test.ts` need a real Postgres + Redis and real config (`CONFIG_PATH`, `SECRETS_DIR` — container paths). docs/TESTING.md:24-32 documents `make shell-backend` then `npm test`, but that recipe is STALE-DANGEROUS: the shipped backend image is the production stage (`npm ci --omit=dev` — no jest; only `dist/` plus migration SQL copied), so `npm test` inside it fails with "jest: not found". As-shipped, the integration tests are runnable nowhere; the viable paths are a builder-stage image (`docker build --target builder ...`) or a host run against an ephemeral Postgres/Redis — see `mvp-deploy-safety-campaign` 1B. They are DESTRUCTIVE by design: `beforeAll` executes the feature's real migration SQL and `afterAll` drops the tables, e.g. `vehicles.integration.test.ts:38` runs `DROP TABLE IF EXISTS vehicles CASCADE` and drops `update_updated_at_column()` CASCADE (which other tables' triggers depend on); `admin.integration.test.ts:72-73` drops `admin_audit_logs` and `admin_users`. They run against whatever database the container config points at — the shared dev DB `motovaultpro`. Owner non-negotiable: never run integration tests against a database you care about without a fresh backup first (`make db-backup` or `./scripts/export-database.sh`). Restore path: `./scripts/import-database.sh`. See `mvp-run-and-operate`. Note: `config/app/ci.yml` ("CI-specific configuration for backend tests") exists but nothing in the repo references it — it also points at `mvp-postgres`/db `motovaultpro`, so it does not make the tests safe. ### Full local stack — currently cannot work on this machine `make setup` (compose up --build + migrations) is labeled staging/prod-build only in root CLAUDE.md, and on this checkout it will not produce a healthy stack because four "secret files" exist only as Docker-created EMPTY DIRECTORIES (Docker auto-creates a directory when a bind-mount source file is missing; canonical description of this trap: `mvp-config-and-secrets` section 2): ``` secrets/app/stripe-secret-key.txt <- directory secrets/app/stripe-webhook-secret.txt <- directory secrets/app/auth0-ocr-client-id.txt <- directory secrets/app/auth0-ocr-client-secret.txt <- directory ``` The backend's Zod secrets schema (`config-loader.ts:114-125`) requires `stripe_secret_key` and `stripe_webhook_secret` as non-optional strings; reading a directory fails, validation throws, and the backend crash-loops at startup. The OCR container bind-mounts the two auth0-ocr paths (`docker-compose.yml:213-214`). To ever make the local stack work: `rm -rf` the four directories, create real `.txt` files (see the `.txt.example` siblings), then `make setup`. Until then, local full-stack is off the table — which is consistent with the dev model in section 1. Verify current state: `ls -la secrets/app/ | grep '^d'` (any `.txt` entry that is a directory is a broken mount point). ## 4. Docker build anatomy Three Dockerfiles; CI (`.gitea/workflows/staging.yaml`) builds all three on every push to main and every PR sync, tags `:<7-char-sha>` AND `:latest`, and pushes to the registry. | Image | Dockerfile | Compile gate | Registry | |---|---|---|---| | backend | `backend/Dockerfile` (2-stage) | `RUN npm run build` (tsc) at line 31 | `git.motovaultpro.com/egullickson/backend` | | frontend | `frontend/Dockerfile` (4-stage) | `RUN npm run build` (tsc + vite) at line 35 | `git.motovaultpro.com/egullickson/frontend` | | ocr | `ocr/Dockerfile` (1-stage) | none (pip install only, no tests) | `git.motovaultpro.com/egullickson/ocr` | These tsc runs are the ONLY compile gate CI has. A PR is "green" when the 3 images build and staging boots healthy — nothing else. Migrations ship inside the backend image: `backend/Dockerfile:58-61` sets `ENV MIGRATIONS_DIR=/app/migrations` and copies `src/features` and `src/core` there; the container CMD (`Dockerfile:84`) runs `node dist/_system/migrations/run-all.js && npm start` on every start. Therefore a NEW MIGRATION REACHES STAGING/PROD ONLY VIA AN IMAGE REBUILD — merging SQL into the repo does nothing until CI builds and deploys a new backend image. A new feature's migrations also require an entry in `MIGRATION_ORDER` in `backend/src/_system/migrations/run-all.ts` or they silently never run. Frontend build-time vs runtime config: all `VITE_*` values are baked at image build via compose/CI build args (`docker-compose.yml:60-64`, ARGs at `frontend/Dockerfile:21-25`). Exception: the Google Maps key/map-id are injected at container RUNTIME by `frontend/scripts/load-config.sh` from `/run/secrets`, not at build. PR builds clobber `:latest` — deploy implications and blue-green mechanics are in `mvp-run-and-operate`. ## 5. Environment traps table | Trap | Reality | Do instead | |---|---|---| | Root `npm test`/`npm run lint` | Root `package.json` has NO scripts (root CLAUDE.md is wrong here; code wins) | cd into `backend/` or `frontend/` | | Root dependencies | `test@^3.3.0` (stray/accidental package, supply-chain smell), plus `jest`, `@playwright/test`, `tdd-guard-jest` with no root node_modules — all dead weight | Do not `npm install` at root expecting anything; do not add root deps | | `make mobile-setup` | Advertised in help and `.PHONY` but HAS NO RULE — "Nothing to be done" | Mobile testing procedure: `mvp-validation-and-qa` | | `make clean` | `docker compose down -v --rmi all` — DESTROYS DB VOLUMES wherever run | Non-negotiable: fresh backup first (`make db-backup`) | | Backend jest hang | pg pool open handles; jest never exits | Always `npm test -- --forceExit` locally | | Frontend `npm test` | Broken outside container (tdd-guard-jest reporter + hardcoded Linux projectRoot) | `npx jest --reporters=default`; positional path BEFORE the flag | | Integration tests | `DROP TABLE ... CASCADE` on the shared dev DB | Container-only, backup first, never against a DB you care about | | `make setup` locally | 4 secrets are empty directories; backend Zod validation crash-loops | Treat local full-stack as unavailable; verify on staging | | `frontend/.env.local` | Affects `npm run dev` only; deployed images use build args baked by CI | Change Gitea CI variables / compose build args for deployed values | | New migration "not on staging" | Migrations live inside the backend image at `/app/migrations` | Merge -> CI image build -> deploy; check `_migrations` table | | `npm run migrate:feature` (backend) | Points at nonexistent `run-feature.ts` | `npm run migrate` (runs all, idempotent per file) | ## Provenance and maintenance Authored 2026-07-07 against commit e729d42 (main). All commands in sections 2 and the frontend fallback were executed on the dev machine that day, except the OCR venv/pytest recipe (labeled UNVERIFIED). Volatile facts and how to re-check them: | Fact | Re-verify with | |---|---| | Root package.json still has no scripts + stray `test` dep | `cat package.json` | | Backend scripts unchanged (test/lint/type-check/build, broken migrate:feature) | `cat backend/package.json` and `ls backend/src/_system/migrations/` | | Frontend jest still broken (reporter + projectRoot) | `grep -n -A6 reporters frontend/jest.config.ts` | | Frontend fallback still works | `cd frontend && npx jest --reporters=default --listTests` | | Backend jest still hangs without forceExit | `cd backend && npx jest src/features/stations/tests/unit/station-matching.test.ts` (should finish; if it hangs, trap still live) | | 4 secrets still empty directories | `ls -la secrets/app/ \| grep '^d'` | | `make mobile-setup` still ruleless | `grep -n "mobile-setup:" Makefile \|\| echo missing` | | Integration tests still DROP TABLE | `grep -rn "DROP TABLE" backend/src --include="*.test.ts"` | | Migrations still image-packaged | `grep -n "MIGRATIONS_DIR\|migrations" backend/Dockerfile` | | CI still runs zero tests/lint | `grep -rn "npm test\|npm run lint" .gitea/workflows/ \|\| echo none` | | Registry image names | `grep -n "egullickson/" .gitea/workflows/staging.yaml \| head` | | OCR pytest deps present | `grep -n "pytest" ocr/requirements.txt` |