diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e891eae..ff41169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,14 @@ jobs: CHANGED=$(git diff --name-only "$BASE" HEAD || true) echo "changed files:"; echo "$CHANGED" - # Shared foundations → run everything. (Root config, shared types/util/json, the execution - # spine, and the root orchestrators GameManager/City/Clock are imported across all modules.) - SHARED_RE='^(src/types/|src/util/|src/json/|src/app/game/execution/|src/app/game/GameManager\.ts|src/app/game/City\.ts|src/app/game/Clock\.ts|package\.json|package-lock\.json|tsconfig\.json|jest\.config\.js|\.babelrc|babel\.config\.js|scripts/|\.github/workflows/)' + # Shared foundations → run EVERY module's tests. This is a correctness net, not laziness: these + # are imported by / affect the compilation of every module, so a change here can break any + # module's tests. It is deliberately limited to things that change TEST OUTCOMES — shared source + # (types/util/json, the execution spine, the root orchestrators GameManager/City/Clock) and the + # test toolchain config (package(-lock), tsconfig, jest.config, babel). It does NOT include CI + # workflow files, scripts/, lint config, or docs — changing those can't change a module test's + # result, so a PR touching only those (like a docs/CI-config change) runs NO module tests. + SHARED_RE='^(src/types/|src/util/|src/json/|src/app/game/execution/|src/app/game/GameManager\.ts|src/app/game/City\.ts|src/app/game/Clock\.ts|package\.json|package-lock\.json|tsconfig\.json|jest\.config\.js|\.babelrc|babel\.config\.js)' SELECTED="" if echo "$CHANGED" | grep -qE "$SHARED_RE"; then @@ -71,8 +76,9 @@ jobs: fi # Emit a JSON array (empty array when nothing test-relevant changed → the test job is skipped). - JSON=$(printf '%s\n' $SELECTED | grep -v '^$' | sort -u \ - | jq -R . | jq -cs .) + # Use jq to drop blanks + dedupe — NOT `grep -v '^$'`, which exits 1 when SELECTED is empty + # (e.g. a docs-only PR) and, under `set -o pipefail`, would fail the whole step. + JSON=$(printf '%s\n' $SELECTED | jq -R 'select(length > 0)' | jq -cs 'unique') echo "selected modules: $JSON" echo "modules=$JSON" >> "$GITHUB_OUTPUT" @@ -144,35 +150,77 @@ jobs: - name: Generation perf-regression gates run: npx jest --selectProjects perf --runInBand - # One concurrent job per affected test module (dynamic matrix) — fast, granular pass/fail. Each runs - # with --coverage and uploads its own per-module coverage report (measured by its own tests) for the - # `coverage` job to gate. - test: + # One concurrent job per test module — fast, granular pass/fail; each runs its module's jest project + # with --coverage and uploads its own per-module report for the `coverage` job to gate. Shared steps + # live in the reusable ./.github/workflows/test-module.yml; here we have one caller per module, each + # gated by a per-module `if` so modules this PR did NOT affect show up as SKIPPED in the checks list + # (not merely absent). `changes` decides which are affected (see that job). A static matrix can't do + # this — `matrix` isn't available in a job-level `if` — so keep this list in sync with `changes`. + test-world: needs: changes - if: needs.changes.outputs.modules != '[]' - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - module: ${{ fromJSON(needs.changes.outputs.modules) }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - run: npm ci - - name: Run ${{ matrix.module }} tests with coverage - run: npx jest --selectProjects ${{ matrix.module }} --coverage --coverageReporters=json --coverageReporters=lcovonly --coverageDirectory coverage - - name: Upload ${{ matrix.module }} coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.module }} - path: | - coverage/coverage-final.json - coverage/lcov.info - if-no-files-found: ignore + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'world') }} + uses: ./.github/workflows/test-module.yml + with: { module: world } + test-agents: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'agents') }} + uses: ./.github/workflows/test-module.yml + with: { module: agents } + test-population: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'population') }} + uses: ./.github/workflows/test-module.yml + with: { module: population } + test-events: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'events') }} + uses: ./.github/workflows/test-module.yml + with: { module: events } + test-actions: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'actions') }} + uses: ./.github/workflows/test-module.yml + with: { module: actions } + test-execution: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'execution') }} + uses: ./.github/workflows/test-module.yml + with: { module: execution } + test-economy: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'economy') }} + uses: ./.github/workflows/test-module.yml + with: { module: economy } + test-skills: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'skills') }} + uses: ./.github/workflows/test-module.yml + with: { module: skills } + test-objects: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'objects') }} + uses: ./.github/workflows/test-module.yml + with: { module: objects } + test-history: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'history') }} + uses: ./.github/workflows/test-module.yml + with: { module: history } + test-save: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'save') }} + uses: ./.github/workflows/test-module.yml + with: { module: save } + test-data: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'data') }} + uses: ./.github/workflows/test-module.yml + with: { module: data } + test-util: + needs: changes + if: ${{ contains(fromJSON(needs.changes.outputs.modules), 'util') }} + uses: ./.github/workflows/test-module.yml + with: { module: util } # Coverage gate: does NOT run tests — it downloads every module's report from the `test` jobs and # fails if ANY module's OWNED-file statement coverage is below COVERAGE_THRESHOLD (jest.config.js, @@ -180,7 +228,7 @@ jobs: # Jest's collectCoverageFrom is additive, so an unfiltered report is diluted by transitively-required # files from other modules. All modules currently clear 80%, so this is BLOCKING (in ci-success's needs). coverage: - needs: [changes, test] + needs: [changes, test-world, test-agents, test-population, test-events, test-actions, test-execution, test-economy, test-skills, test-objects, test-history, test-save, test-data, test-util] if: always() && needs.changes.outputs.modules != '[]' runs-on: ubuntu-latest timeout-minutes: 10 @@ -220,22 +268,18 @@ jobs: # any required upstream job failed or was cancelled (skipped is fine). `audit` is intentionally NOT # here (advisory — dev-tooling advisories shouldn't block). ci-success: - needs: [changes, typecheck, build, lint, perf, test, coverage] + needs: [changes, typecheck, build, lint, perf, coverage, test-world, test-agents, test-population, test-events, test-actions, test-execution, test-economy, test-skills, test-objects, test-history, test-save, test-data, test-util] if: always() runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Verify no required job failed + # join(needs.*.result) = the results of every job listed in `needs` (audit is excluded — advisory). + # skipped is fine (an unaffected module / a skipped coverage gate); only failure/cancelled block. run: | - set -euo pipefail - for r in \ - "${{ needs.changes.result }}" \ - "${{ needs.typecheck.result }}" \ - "${{ needs.build.result }}" \ - "${{ needs.lint.result }}" \ - "${{ needs.perf.result }}" \ - "${{ needs.test.result }}" \ - "${{ needs.coverage.result }}"; do + set -eu + echo "results: ${{ join(needs.*.result, ' ') }}" + for r in ${{ join(needs.*.result, ' ') }}; do if [ "$r" = "failure" ] || [ "$r" = "cancelled" ]; then echo "A required job did not succeed (result: $r)"; exit 1 fi diff --git a/.github/workflows/test-module.yml b/.github/workflows/test-module.yml new file mode 100644 index 0000000..324e25f --- /dev/null +++ b/.github/workflows/test-module.yml @@ -0,0 +1,37 @@ +name: test-module + +# Reusable workflow: run ONE module's Jest project with coverage and upload its per-module report for +# the `coverage` gate. ci.yml calls this once per module, each with a per-module `if`, so a module the +# PR didn't affect shows as SKIPPED in the checks list. (A static matrix can't do that — `matrix` is not +# available in a job-level `if` — and a dynamic matrix omits unaffected modules entirely rather than +# showing them skipped. Hence one small caller job per module, all sharing these steps.) + +on: + workflow_call: + inputs: + module: + description: The test module (jest project) to run — e.g. economy, events, util. + required: true + type: string + +jobs: + run: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - name: Run ${{ inputs.module }} tests with coverage + run: npx jest --selectProjects ${{ inputs.module }} --coverage --coverageReporters=json --coverageReporters=lcovonly --coverageDirectory coverage + - name: Upload ${{ inputs.module }} coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ inputs.module }} + path: | + coverage/coverage-final.json + coverage/lcov.info + if-no-files-found: ignore diff --git a/docs/tasks/008-test-suites-unit-integration.md b/docs/tasks/008-test-suites-unit-integration.md index aaa1278..742fa12 100644 --- a/docs/tasks/008-test-suites-unit-integration.md +++ b/docs/tasks/008-test-suites-unit-integration.md @@ -1,66 +1,277 @@ -# [Test] Integration (Playwright) suite +# [Test] Integration (Playwright) suite — the browser-level test layer - **Type:** Test infrastructure -- **Labels:** `test`, `tooling`, `coverage`, `playwright` -- **Status:** 🚧 **Re-scoped (updated 2026-06-29).** The original task asked for a unit suite + a coverage - script + a Playwright integration suite. The **unit suite** and **coverage** are now **done** and unrelated to - their original "only `personTravel.test.ts` exists" baseline: - - The repo has a mature Jest + ts-jest unit suite — **41 test files / 220 tests** covering the simulation core - (pathfinding, footprint/depth, save/load, curves/predicates, business gen + economics + demand, the event - compiler/engine, households/rehousing/cohabitation/move-out/eviction, life events, city stats, teardown, …). - - **Coverage** is wired (task 009): `npm run test:coverage` (`jest --coverage`) with `collectCoverageFrom` - over `src/app/game/**` + `src/util/**` (Phaser-only glue excluded), an `lcov` report, and a - `coverageThreshold` gate (~72% floor, currently ~78% statements). CI consumes it. - - **The remaining work — and the active scope of this task — is the Playwright integration suite below.** - -## Summary - -Add an **integration test suite** that boots the real app (React HUD + Phaser) with a **debug save auto-loaded** -and uses **Playwright** to drive and assert in-app behaviour the headless unit suite can't (rendering, input, -window interactions). Keep it isolated from the fast Jest unit run (`npm test`); it runs under its own script. - -## Background / current state (verified) - -- **Unit + coverage:** done (see Status). `npm test` is the fast unit run; `npm run test:coverage` gates - coverage. Do **not** fold Playwright into `npm test`. -- **Debug auto-load exists** (`003`): `json/config.json` → `debug.autoLoad.{enabled,save}` boots straight into - `MainScene` from an embedded save, bypassing the splash (`GameManager` applies it on `hudReady`). The - integration harness should use this for a deterministic start state. -- **No Playwright, no integration harness, no committed fixture save** exist yet. -- HUD elements mostly lack stable selectors; add `data-testid` attributes where needed for robust assertions. - -## Goals / Requirements - -1. **Add Playwright** as a dev dependency and a dedicated script (e.g. `test:integration`) plus a - `playwright.config.ts` targeting a single Chromium project. Document how to run it. -2. **Boot from a committed fixture save** via the `003` debug auto-load, so tests start from a known, - deterministic world (no splash). Commit the fixture. -3. **Implement high-value cases**, at minimum: - - App boots from the debug save into `MainScene` (no splash) and the HUD mounts. - - Select tool → clicking a house opens `HouseDetails` and renders a family tree; clicking a workplace opens - `WorkplaceDetails`; clicking a person opens `PersonDetails` with its life-event log. - - Placing a road/house/work building with a tool updates the world (a tile/sprite appears). - - Tool hotkeys (`F1`–`F6`, `Esc`) switch the active tool/cursor. - - Save flow: toolbar save / `Ctrl+S` triggers a save and the success toast appears. - - The clock/date-time widget renders and advances; clicking it opens the **city overview** (031). - - The city event **feed** (029) shows entries as the sim runs. -4. **Wire it into CI (009).** Add the reserved Playwright job to `.github/workflows/ci.yml` (install Chromium - with `npx playwright install --with-deps chromium`, run `test:integration`, upload the report/trace on - failure). Keep it a separate job so the unit gate stays fast. -5. **Determinism:** fixed fixture save, controlled timing, stable `data-testid` selectors. - -## Out of scope - -- A specific coverage threshold number (configured in `009`, done). -- Visual-regression / screenshot-diff testing; cross-browser matrices (single Chromium initially). - -## Acceptance criteria - -- A Playwright integration suite runs via its own script, boots from a committed debug save, and asserts the - defined use cases; it is wired into CI as its own job. -- Running instructions are documented. - -## Notes - -- The unit-suite/coverage portions of the original task are complete; this file now tracks only the integration - suite. Sequence after 009 (CI) — which reserves a Playwright job — and reuse the `003` auto-load. +- **Labels:** `test`, `tooling`, `playwright`, `integration`, `ci` +- **Status:** 📋 **Ready.** The unit suite + coverage from the original 008/009 scope are long done and have + since been rebuilt into a modular, per-module architecture (below). This task is now **only** the + **Playwright integration suite** — the browser-level layer that exercises what the headless Jest suite + structurally cannot: the React HUD, the Phaser canvas, real input, windows, save/load round-trips through the + actual UI, and emergent on-map behaviour (people/cars moving, commuting) over time. + +--- + +## 0. Read this first — the current test architecture (so you build *with* it, not around it) + +The repo already has a mature, opinionated test setup. Study it before adding anything; your integration suite +should feel like a first-class citizen of it, not a bolt-on. Sources of truth: **`CLAUDE.md` §2 (Scripts), §4 +(Architecture), §5.3 (Testing & quality gates)**, `jest.config.js`, `.github/workflows/ci.yml`, +`scripts/coverage-gate.mjs`. + +**Unit suite (Jest, done — do NOT fold Playwright into it):** + +- One Jest **project per module**: `test//` mirrors `src/app/game//`, with `test/util/` for pure + utilities and `test/perf/` for the offline-generator perf gates. Run all with `npm test`; run one with + `npx jest --selectProjects `. There are **~111 suites / ~1500+ tests** now (not the "41/220" an older + version of this file claimed). +- **Per-module coverage, BLOCKING at 80%.** Each `test ()` CI job uploads its own coverage report; the + `coverage` job runs `scripts/coverage-gate.mjs`, which **filters each report to the files that module OWNS** + (`jest.config.js` `MODULE_COVERAGE`) before scoring — because Jest's `collectCoverageFrom` is *additive* (it + forces owned files in but doesn't filter transitively-required files out). `COVERAGE_THRESHOLD` is one number + in `jest.config.js`. +- **What the unit suite deliberately does NOT cover:** the Phaser-only glue and the React HUD — i.e. + `src/app/game/scene/**` (MainScene, TitleScene, DebugTools), `src/app/game/GameManager.ts` (excluded from + coverage), and everything in `src/app/hud/**` (no jest tests at all). These "can't be meaningfully unit-covered + without a browser harness" — **that browser harness is THIS task.** So the integration suite is also where the + scene/HUD coverage gap gets filled (see §7). + +**CI (`.github/workflows/ci.yml`)** is split into concurrent, independently-reported checks aggregated by a +single stable `ci-success` job (the required status check): `changes` (path-filters which modules a PR touched), +a dynamic `test` matrix (one job per affected module), `typecheck`, `build`, `lint` (ESLint + markdownlint), +`perf`, and `coverage` — all **blocking**. `audit` is advisory. **Your integration job must NOT be in +`ci-success`'s needs** (see §8 — it's slow and runs async/non-blocking). + +**Working agreements that apply to your tests too (CLAUDE.md §5):** deterministic (seed any RNG — the whole sim +is deterministic per world seed; no reliance on wall-clock), keep it green, don't weaken gates, one task → one +branch → one PR. Every task ships with tests; this task's *deliverable* is tests, so the "tests" are the suite +itself + proof it runs. + +--- + +## 1. The app under test — how it boots, serves, and can be driven + +- **Stack:** Phaser 4 canvas + React 18 HUD, bundled by Parcel, entry `src/html/index.html` → `src/app/main.tsx`. + `main.tsx` builds a `GameManager` (the event bus + orchestrator + `Clock`), which creates the Phaser `Game` + with `TitleScene` and `MainScene`; React `` mounts on the `gameInitialized` event into `#hud-container`. +- **Boot flow:** `TitleScene` splash with **Start Game** (new world → `MainScene`) and **Load Game** (restores + the most recent save via the pluggable `SaveProvider` → `LocalStorageProvider`). The HUD emits `hudReady`; + `GameManager` applies any queued load (title-screen load or debug auto-load) only then. +- **Debug auto-load (the key hook for deterministic starts):** `src/json/config.json` → + `debug.autoLoad.{enabled, save}`. When `enabled: true`, `GameManager` boots **straight into `MainScene` from + the embedded `save` string, bypassing the splash**, on `hudReady`. The `save` is a compressed+base64 save + payload (see §3). This is how a build "auto-loads a scenario". Other debug flags live under `debug` + (`masterSwitch` gates overlays, `spawnKeys` enables the `P`/`V` debug spawns, `drawCurbs`/`drawLanes`/etc.). +- **Tools & input (`json/input.json`, `MainScene`):** `F1`–`F6` = soil / road / house / work / **select** / + bulldoze; `Esc` = select. `G` toggles the grid overlay. `W/A/S/D` pan, `Q/E` zoom. `Ctrl+S` saves (handled in + the HUD, which suppresses the browser dialog). The **Select tool** is the universal inspector: clicking a + person/house/workplace/the clock opens `PersonDetails`/`HouseDetails`/`WorkplaceDetails`/`CityDetails`. +- **Building/placement:** roads snap to a 3×3 supertile grid and auto-tile from neighbours; buildings soft-snap + flush against a road side (invalid spots preview red). Each structure occupies a 3×3 footprint. Bulldozing an + occupied building tears it down coherently (evict residents / close business). +- **Time:** the `Clock` advances from the frame loop (1 in-game day = 1 real hour; the canonical tick is the + in-game hour). `timeChanged`/`newTick`/`newDay` fan out on the bus; the HUD clock widget shows the live date. + **This real-time advance is your biggest determinism challenge** — solve it with a test hook (§6), don't + `sleep`. +- **Serving for tests:** `npm run build-prod` bundles to `./bin` (Parcel), and `postbuild-prod` copies the + history asset + sprites there. Serve `./bin` as static files (any static server — the dev setup uses + `browser-sync`; `npx serve ./bin` or `http-server ./bin` work too). Point Playwright's `webServer.command` at + a build+serve and `baseURL` at it. (The dev path is `npm run dev` → Parcel watch + `browser-sync` on `./dist`; + fine for local iteration, but CI should test the **production build**.) + +--- + +## 2. Deliverables (what to build) + +1. **Playwright harness.** Add `@playwright/test` as a devDependency, a `playwright.config.ts` (single Chromium + project to start; `webServer` that builds+serves `./bin`; `baseURL`; trace/screenshot/video on failure; + sensible timeouts since the sim runs in real time), a `test:integration` npm script, and a `test/integration/` + directory. Keep it **entirely separate** from the Jest projects (do not add it to `jest.config.js` + `projects`, do not let `npm test` pick it up). +2. **A deterministic test hook** to make the opaque canvas + real-time sim assertable (§6) — the single most + important enabler. Plus `data-testid` attributes on HUD elements (§4) for robust React selectors. +3. **Scenario save fixtures + a generator script** (§3). +4. **The baseline suites** (§4 HUD, §5 canvas) + **scenario-specific tests** (§4.3). +5. **Coverage collection** for the scene/HUD gap (§7). +6. **CI wiring as an async, non-blocking job** (§8). +7. **Docs:** a short README/section on how to run it locally and what the hook exposes. + +--- + +## 3. Scenario saves — fixtures + a generator script + +Tests need known, deterministic starting worlds. The save format is an **id-based `WorldSnapshot`** +(`types/Save.ts`) → JSON → deflate (`pako`, `util/compress.ts`) → base64, produced/consumed by +`game/save/SaveManager.ts` through a `SaveProvider` (`LocalStorageProvider` today). Save version is `SAVE_VERSION`. + +**Create a generator script** (e.g. `scripts/generateScenario.ts`, run via `tsx` like the existing +`scripts/generateHistoryAsset.ts`) that produces committed scenario save strings under +`test/integration/fixtures/`. Two viable approaches — pick per scenario: + +- **(a) Headless snapshot builder (preferred for small, precise scenarios).** Construct a `WorldSnapshot` + object directly (roads/buildings by anchor key, people/vehicles with stable ids, households, businesses, + economy, clock) and serialize it with the same `compress`/base64 path `SaveManager` uses. Fully deterministic, + no browser. Study `SaveManager`'s serialize/deserialize and `types/Save.ts` for the exact shape; reuse the real + `Population`/`BusinessGen`/`Economy` generators to fill realistic data from a fixed seed. +- **(b) Record-a-scenario via Playwright.** Drive the real app to build a scenario (place roads/buildings, let + it run N ticks), trigger a save, then read the payload out of `localStorage` (LocalStorageProvider's key) and + commit it as a fixture. Good for complex emergent scenarios that are painful to hand-build. + +**Wiring a fixture into the debug auto-load.** The `debug.autoLoad.save` in the committed `config.json` must +stay empty (don't ship a scenario in the real config). To boot a *specific* fixture per test without rebuilding +per scenario, add a **small, test-only parametrization hook** (consistent with the existing `debug` flags), +choosing one: + +- Seed `localStorage` before load (`page.addInitScript`) with the fixture under the `LocalStorageProvider` key, + then drive the title-screen **Load Game** (or set `autoLoad` to consume localStorage). Cleanest — no rebuild. +- OR have `GameManager` also honour a `?load=` URL param / a `window.__TOWNBOX_AUTOLOAD` global in + addition to `config.autoLoad`, gated so it only activates for the test build. Document whichever you add. + +A single shared "default" scenario (a small town with a few houses, a workplace, roads, a couple of +residents/commuters) covers most HUD + canvas baseline cases; add a handful of purpose-built fixtures for the +scenario-specific tests. + +--- + +## 4. HUD (React) baseline suite — `test/integration/hud/` + +Add stable `data-testid` attributes to the HUD components as you go (`hud/Hud.tsx`, `hud/Toolbar.tsx`, +`hud/Clock.tsx`, `hud/Feed.tsx`, `hud/Window.tsx`, `hud/windows/*`). Cover, at minimum, **every** operation +below with real Playwright interactions + assertions: + +- **Start a game.** From the splash: **Start Game** boots a new world into `MainScene` and the HUD mounts (clock + + feed + toolbar visible). Also **Load Game** restores a seeded save. Also the **debug auto-load** path boots + straight in with no splash. +- **Save.** Toolbar save button **and** `Ctrl+S` each trigger a save and surface the success **toast** + (`Toasts.tsx`, from `gameSaved`); a failure surfaces the error toast. +- **Load.** Round-trip: save a modified world, reload the page, load it back, assert the world matches (via the + test hook — e.g. same building/resident counts). +- **Toolbar buttons.** Click **each** tool button (soil/road/house/work/select/bulldoze) and confirm the action: + the active tool highlights, the emitted `toolSelected` takes effect, and the cursor/placement mode changes; + confirm `F1`–`F6` / `Esc` keys do the same and stay in sync with the buttons. +- **Windows (via the Select tool):** open a window (click a house → `HouseDetails` with a family tree; a person → + `PersonDetails` with its life-event log; a workplace → `WorkplaceDetails`; the clock → `CityDetails` overview); + **move** a window (drag the title bar — `react-rnd` — assert position changes); **resize** a window (drag a + handle — assert size changes); **close** a window (assert it unmounts). Cover the singleton vs. per-identity + window rules (house/city are singletons; person windows dedupe by identity). +- **Event feed updates.** With the sim running (advance time via the hook), assert the city event **feed** + (`Feed.tsx`, from `cityEvent`) gains entries (births/deaths/hires/etc.), that clicking a feed entry opens the + subject's inspector, and that the feed collapses/expands. +- **Clock widget** renders and **advances** (the date/time changes as the sim ticks). + +### 4.3 Scenario-specific tests — `test/integration/scenarios/` + +Auto-load purpose-built fixtures and assert emergent outcomes over controlled sim time (advance via the hook, +§6). Examples (pick a meaningful handful): a **commuter scenario** → the resident leaves home, reaches the +workplace, and returns; a **household draw** → placing a house materialises a coherent family whose tree renders; +an **economy scenario** (oversupplied category / understaffed business) → the business trends toward +shrink/bankruptcy over months; a **death/rehousing** scenario → a resident dies, is despawned, and an orphaned +minor is re-housed. These are the payoff — they assert the *simulation*, end to end, through the real UI. + +--- + +## 5. Visual canvas operations suite — `test/integration/canvas/` + +The Phaser canvas is opaque to DOM queries, so assert via the **test hook** (§6), not pixel diffs. Cover: + +- **Place a road tile:** select the road tool, click a grid cell, assert a `Road` now exists at that anchor + (and that adjacent roads auto-tiled — the sprite/neighbour code updated). +- **Place a building:** house and work — click a valid road-side cell, assert a `House`/`Workplace` exists with + its 3×3 footprint, and (house) a household materialised / (work) a business generated. +- **Bulldoze a built tile:** select the bulldozer, click a placed building, assert it's removed and the teardown + was coherent (residents evicted / business closed; the lot desaturated). +- **People move:** with the sim advancing, assert a person travels from one building to another (their + position/`currentBuilding` changes over ticks) — leverage a commuter fixture so a trip is guaranteed. +- **People enter/exit cars:** during a commute, assert the travel state machine progresses + (`ExitingBuilding → WalkingToCar → EnteringCar → Driving → … → Arrived`), i.e. a car is spawned/assigned and + the person boards then disembarks. +- **Cars move:** assert a `Vehicle` drives along lanes between buildings (position changes; it despawns on + arrival). + +Because movement is real-time, drive these with the hook's **step-N-ticks** control (deterministic) rather than +waiting on wall-clock, and assert on the resulting simulation state. + +--- + +## 6. The determinism hook — expose a test-only window API + +This is the linchpin. Add a **test-only** global (e.g. `window.__townbox`) that the app installs **only** when a +debug/test flag is set (reuse the `config.json` `debug` mechanism, a `?test=1` param, or a Parcel env var — never +in normal production). It should expose read + control access to the live sim: + +- **Read:** the `Field` (query a tile at `(row,col)` → is it a `Road`/`House`/`Workplace`?; footprint anchors), + the `City`/`Population` (resident/household/business counts, a person's `currentBuilding`, travel state, + vehicle count/positions), the `Clock` (current tick/date), and the event history/feed. +- **Control:** **advance the sim deterministically** — e.g. `stepTicks(n)` that drives the same + `newTick`/`newDay`/economy cadence the frame loop does, without depending on real elapsed time. This lets a + canvas test do `place → stepTicks(24) → assert person arrived` with zero flakiness. Optionally pause/resume the + RAF loop so time only advances when the test asks. + +Expose it through `GameManager` (which already owns the `Clock`, `Field`, `City`) so it stays inside the +game→HUD boundary (no reaching into internals from tests except through this documented seam). Keep it strictly +gated so it never ships enabled. + +--- + +## 7. Coverage — fill the scene/HUD gap (don't fight the per-module gate) + +The Jest per-module gate intentionally **excludes** `scene/**`, `GameManager.ts`, and all of `hud/**`. The +integration suite is the right place to cover them. Wire it **separately** from the jest coverage gate: + +- **Collect browser coverage.** Either instrument the test build with `babel-plugin-istanbul` (then read + `window.__coverage__` after each test and merge with `istanbul-lib-coverage` — already a devDependency — into + an lcov/`coverage-final.json`), or use Playwright's Chromium **V8 coverage** API and convert to istanbul. +- **Scope + report.** Produce a coverage report scoped to the browser-only surface (`src/app/game/scene/**`, + `src/app/game/GameManager.ts`, `src/app/hud/**`). Upload it as a CI artifact. Start it **informational** + (report + trend), and only later — once the suite is broad enough — consider a separate integration-coverage + threshold (a distinct gate, NOT folded into the per-module `coverage-gate.mjs`, whose owned-file model assumes + the jest scoping). Reuse the spirit of `scripts/coverage-gate.mjs` if you add a threshold. + +--- + +## 8. CI wiring — a slow, async, NON-BLOCKING job + +The suite boots a real browser and runs the sim over many ticks — it will take minutes, not seconds. **It must +not gate merges.** Add an `integration` job to `.github/workflows/ci.yml`: + +- Steps: checkout → setup-node (20, npm cache) → `npm ci` → `npx playwright install --with-deps chromium` → + `npm run build-prod` (or let `playwright.config.ts` `webServer` build+serve) → `npm run test:integration`. +- `if: always()` on report upload; **upload the Playwright HTML report + traces/videos on failure**, and the + coverage artifact. +- **Do NOT add `integration` to `ci-success`'s `needs`** (keep it advisory/non-blocking, exactly like `audit` + and how `coverage`/`perf` were staged before they were trusted). Optionally gate it to `pull_request` + + `push` to main, or behind a label / `workflow_dispatch`, so it doesn't run on every trivial push. Give it a + generous `timeout-minutes`. It runs concurrently with everything else; its result is visible but never blocks. +- Follow the file's existing conventions (the `changes` job, comment style, pinned action versions). + +When it's mature and stable, a follow-up can promote it (its own required check, or into `ci-success`) — call +that out in the PR but don't do it here. + +--- + +## 9. Out of scope + +- Visual-regression / screenshot-diff testing, and cross-browser matrices (single Chromium initially; the hook + + state assertions are more robust than pixel diffs for this canvas app). +- Any change to the Jest per-module gate, `COVERAGE_THRESHOLD`, or `ci-success`'s required checks. +- Touching `docs/tasks/**` history. + +## 10. Acceptance criteria + +- `@playwright/test` + `playwright.config.ts` + a `test:integration` script exist; the suite boots the real + production build (React HUD + Phaser) from a committed scenario fixture via the debug auto-load / seeded load, + with a documented, test-gated determinism hook (`window.__townbox` or equivalent) and `data-testid`s on the HUD. +- A **scenario generator script** produces committed fixtures under `test/integration/fixtures/`. +- The **HUD suite** (§4) covers start/save/load, every toolbar button + hotkey, window open/move/resize/close, + the event feed updating, and the clock advancing; the **canvas suite** (§5) covers road/building placement, + bulldozing, and people/cars moving + commute enter/exit; plus a handful of **scenario-specific** tests (§4.3). +- Tests are deterministic (fixtures + the step-ticks hook, no wall-clock waits) and green locally. +- Browser coverage of the scene/HUD gap is collected and uploaded (§7). +- CI runs the suite as a **separate, async, non-blocking** job (§8) — visible, never in `ci-success`'s needs. +- Running instructions are documented (README/section). + +## 11. Notes + +- Reuse the debug auto-load (`003`) for a deterministic, splash-free start; add `data-testid`s where the HUD + lacks stable selectors. +- Do **not** fold Playwright into `npm test` / the Jest projects — it's a separate suite with its own script and + its own (non-blocking) CI job, the browser analogue of how `test/perf/` is isolated. +- Never commit a scenario in the real `config.json`, and never ship the test hook enabled. diff --git a/docs/tasks/055-history-asset-pipeline.md b/docs/tasks/055-history-asset-pipeline_DONE.md similarity index 100% rename from docs/tasks/055-history-asset-pipeline.md rename to docs/tasks/055-history-asset-pipeline_DONE.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md new file mode 100644 index 0000000..be058f5 --- /dev/null +++ b/docs/tasks/README.md @@ -0,0 +1,117 @@ +# Task backlog + +This folder is TownBox's JIRA-style backlog. **Every file here is a well-defined, self-contained piece of work +that is safe to merge to `main` on its own** — clear goals and requirements with accurate references to real +code (see `CLAUDE.md` §5.1). This README is the index. + +## Conventions + +- **One task → one file → one branch → one PR.** Files are numbered in roughly the order they were created. +- **Done is marked in the filename.** A completed task's file is renamed to append `_DONE` before `.md` (e.g. + `005-clock-and-calendar-system_DONE.md`), and its row here flips to ✅. A task replaced by a later one gets + `_SUPERSEEDED`. Keep this index in sync when a task's status changes. +- **Status legend:** ✅ Done · 🚧 Open · ⛔ Superseded. + +## Roughly, the arcs + +- **001–013** — foundations: Phaser 4, the 3×3 tile grid, save/load, clock, households/genealogy, the daily + commute, and the procedural-simulation-framework plan. +- **014–037** — employment, the money economy end-to-end (wages → cost of living → business P&L → bankruptcy → + eviction/homelessness → recovery, B2B materials), household-lifecycle dynamics, the UI/inspector layer, content + expansion, and CI. +- **038–054** — the simulation-enrichment arc: hourly ticks + the execution boundary, objects/Possessions, the + Action system, event triggers/causation, the Brain + Job Orchestrator, and the content backfills. +- **055, 076–079** — the offline history-asset pipeline + logical-economy world + generator perf, and the + pre-055 audit-remediation hardening. +- **056–075** — the progression & context arc: calendar/weekends, school, skill proficiency + a 335-skill DAG, + job rank ladders + promotions, placement tags + object generation, and interaction contracts/consent. + +## Index + +| Task | Status | Title | +| --- | --- | --- | +| [001](001-upgrade-phaser-4_DONE.md) | ✅ | [Maintenance] Upgrade Phaser 3 → Phaser 4 | +| [002](002-tile-placement-granularity-3x3_DONE.md) | ✅ | [Feature] Subdivide each tile into a 3×3 sub-tile grid | +| [003](003-save-load-system_DONE.md) | ✅ | [Feature] Save & load system | +| [004](004-household-generation-redesign_DONE.md) | ✅ | [Planning] Redesign family generation → household + cross-household genealogy | +| [005](005-clock-and-calendar-system_DONE.md) | ✅ | [Feature] Clock & calendar system | +| [006](006-job-commute-pathfinding_DONE.md) | ✅ | [Feature] Wire jobs to pathfinding — daily work commute loop | +| [007](007-business-generation_SUPERSEEDED.md) | ⛔ | [Feature] Business generation for work buildings + job/skill data | +| [008](008-test-suites-unit-integration.md) | 🚧 | [Test] Integration (Playwright) suite — the browser-level test layer | +| [009](009-github-actions-ci_DONE.md) | ✅ | [Test] GitHub Actions CI pipeline | +| [010](010-marriage-formation-over-time_DONE.md) | ✅ | [Feature] Marriage / partnership formation over time | +| [011](011-emergent-rehousing_DONE.md) | ✅ | [Feature] Emergent re-housing of household survivors | +| [012](012-live-app-verification-clock-population.md) | 🚧 | [Task] Live-app verification of the clock & population simulation | +| [013](013-procedural-simulation-framework_DONE.md) | ✅ | [Planning] File-based procedural simulation framework — blueprints + life events | +| [014](014-people-skills-model_DONE.md) | ✅ | [Feature] People skills model & assignment | +| [015](015-skill-matched-hiring_DONE.md) | ✅ | [Feature] Skill-matched hiring as resource-slot events | +| [016](016-retire-debug-spawning_DONE.md) | ✅ | [Feature] Retire debug/random spawning; source all spawning from the simulation | +| [017](017-money-model_DONE.md) | ✅ | [Feature] Money model: wallets & ledger | +| [018](018-wages-and-payroll_DONE.md) | ✅ | [Feature] Wages & payroll | +| [019](019-cost-of-living_DONE.md) | ✅ | [Feature] Cost of living & household spending | +| [020](020-business-economics_DONE.md) | ✅ | [Feature] Business economics: revenue, materials, P&L & size dynamics | +| [021](021-business-bankruptcy_DONE.md) | ✅ | [Feature] Business bankruptcy & closure | +| [022](022-eviction-and-homelessness_DONE.md) | ✅ | [Feature] Household insolvency: eviction & homelessness | +| [023](023-newlywed-cohabitation_DONE.md) | ✅ | [Feature] Newlywed cohabitation & household merging | +| [024](024-adult-children-move-out_DONE.md) | ✅ | [Feature] Adult children move out / new-household formation | +| [025](025-structure-teardown_DONE.md) | ✅ | [Feature] Structure teardown on bulldoze (residents & businesses) | +| [026](026-entity-selection-model_DONE.md) | ✅ | [Feature] Entity selection model (people & buildings) | +| [027](027-person-inspector-window_DONE.md) | ✅ | [Feature] Person inspector window (with event log) | +| [028](028-workplace-inspector-window_DONE.md) | ✅ | [Feature] Workplace / business inspector window | +| [029](029-city-event-feed_DONE.md) | ✅ | [Feature] City event feed / notifications | +| [030](030-toolbar-and-tools_DONE.md) | ✅ | [Feature] Toolbar wiring & tool selection | +| [031](031-city-overview-window_DONE.md) | ✅ | [Feature] City overview / dashboard window | +| [032](032-expand-life-events_DONE.md) | ✅ | [Feature] Expand the life-event manifest | +| [033](033-expand-business-blueprints_DONE.md) | ✅ | [Feature] Demand-driven business revenue + expanded blueprints | +| [034](034-expand-jobs-and-skills_DONE.md) | ✅ | [Feature] Expand jobs & skills reference tables | +| [035](035-materials-and-products_DONE.md) | ✅ | [Feature] Materials & products production/consumption chain | +| [036](036-pregame-history-bootstrap_DONE.md) | ✅ | [Feature] Pre-game history bootstrap (detailed fast-forward simulation) | +| [037](037-bankrupt-lot-reoccupancy_DONE.md) | ✅ | [Feature] Bankrupt-lot re-occupancy (vacant buildings attract new businesses) | +| [038](038-simulation-enrichment-architecture_DONE.md) | ✅ | [Planning] Simulation enrichment & the execution boundary — architecture proposal + discovery baseline | +| [039](039-data-schema-registry-and-validators_DONE.md) | ✅ | [Foundation] Data-schema registry, validators & CI gate | +| [040](040-hourly-ticks-and-execution-boundary_DONE.md) | ✅ | [Foundation] Hourly ticks, shared tick lifecycle & the simulation execution boundary | +| [041](041-objects-and-possessions_DONE.md) | ✅ | [Core] Objects & Person Possessions | +| [042](042-event-triggers-and-causation_DONE.md) | ✅ | [Core] Event triggers (`manual` / `probabilistic` / `automated`) & causation logging | +| [043](043-actions-core_DONE.md) | ✅ | [Core] Actions: definitions, parameters, shared requirements, lifecycle, pools & sequences | +| [044](044-action-consequences-and-object-action-relationships_DONE.md) | ✅ | [Core] Action Consequences (bounded DSL) & `object-action-relationships.json` | +| [045](045-job-shifts-and-work-actions_DONE.md) | ✅ | [Integration] Job shift schedules & work-Action declarations | +| [046](046-brain-and-hooks_DONE.md) | ✅ | [Integration] Brain & the Hooks pattern | +| [047](047-job-orchestrator_DONE.md) | ✅ | [Integration] The Job Orchestrator | +| [048](048-events-revision-hourly-migration_DONE.md) | ✅ | [Migration] Revise & backfill all existing Events for triggers, hourly ticks & Action links | +| [049](049-content-planning-lists_DONE.md) | ✅ | [Content prep] Pre-initiative content planning lists | +| [050](050-objects-data-backfill_DONE.md) | ✅ | [Content] Objects data backfill (1,200+ archetypes) | +| [051](051-actions-data-backfill_DONE.md) | ✅ | [Content] Actions data backfill (general-purpose + per-job) | +| [052](052-events-data-backfill_DONE.md) | ✅ | [Content] Events data backfill (500 probabilistic + 500 manual) | +| [053](053-object-action-relationships-backfill_DONE.md) | ✅ | [Content] `object-action-relationships.json` backfill | +| [054](054-action-event-relationship-docs_DONE.md) | ✅ | [Docs] Document the Action ↔ Event relationships & lifecycle flows | +| [055](055-history-asset-pipeline_DONE.md) | ✅ | [Feature] Offline history-asset pipeline + asset-fed new game | +| [056](056-progression-arc-discovery-baseline_DONE.md) | ✅ | [Planning] Progression & context arc — discovery and migration baseline | +| [057](057-calendar-weekdays-and-weekends_DONE.md) | ✅ | [Framework] Calendar weekday & weekend support | +| [058](058-school-assignments-and-scheduling_DONE.md) | ✅ | [Feature] School assignments, scheduling & weekend behavior | +| [059](059-skill-proficiency-schema-and-store_DONE.md) | ✅ | [Framework] Skill rework — proficiency schema, dependency graph & central store | +| [060](060-basic-skills-backfill_DONE.md) | ✅ | [Content] Basic skills — definition & backfill | +| [061](061-specific-skills-backfill-and-migration_DONE.md) | ✅ | [Content] Specific skills — replace the generic skill families & migrate all references | +| [062](062-skill-initialization-and-early-childhood_DONE.md) | ✅ | [Feature] Person skill initialization & early-childhood seeding | +| [063](063-school-day-skill-progression_DONE.md) | ✅ | [Feature] School-day skill progression | +| [064](064-job-ranks-and-training-grants_DONE.md) | ✅ | [Framework] Job ranks & entry-level training grants | +| [065](065-job-skill-progression-and-promotion_DONE.md) | ✅ | [Feature] Job skill progression & rank promotion | +| [066](066-jobs-ranks-data-backfill_DONE.md) | ✅ | [Content] Jobs backfill — ranks, skill requirements & progression declarations | +| [067](067-parameterized-requirements-and-event-payloads_DONE.md) | ✅ | [Framework] Parameterized requirements, object refs & event payloads | +| [068](068-generalize-actions-and-events_DONE.md) | ✅ | [Migration] Generalize Actions & Events | +| [069](069-object-placement-tags_DONE.md) | ✅ | [Framework] Contextual placement tags — objects, buildings & businesses | +| [070](070-contextual-object-generation_DONE.md) | ✅ | [Feature] Deterministic contextual object generation | +| [071](071-building-context-action-requirements_DONE.md) | ✅ | [Content] Backfill Action requirements from building context | +| [072](072-person-targeted-action-contracts_DONE.md) | ✅ | [Framework] Person-targeted Action interaction contracts | +| [073](073-consent-and-action-failure_DONE.md) | ✅ | [Feature] Consent evaluation & Action failure handling | +| [074](074-person-targeted-actions-backfill_DONE.md) | ✅ | [Content] Person-targeted Actions backfill — contracts, consent flags & decline events | +| [075](075-progression-arc-validation-and-docs_DONE.md) | ✅ | [Test/Docs] Progression & context arc — end-to-end validation and documentation | +| [076](076-audit-remediation_DONE.md) | ✅ | [Audit] Consumption & closed-loop remediation (pre-055 hardening) | +| [077](077-offline-logical-economy-world_DONE.md) | ✅ | [Feature] Offline logical-economy world — off-map jobs/schools/objects during history generation | +| [078](078-offline-generator-perf-optimization_DONE.md) | ✅ | [Perf] Offline history-generator — per-agent step-cost optimization | +| [079](079-offline-generator-perf-brain-actions_DONE.md) | ✅ | [Perf] Offline history-generator — the brain/actions per-agent pass | + +## Open work + +- **[008](008-test-suites-unit-integration.md)** — the Playwright browser-level integration suite (HUD + canvas + + scenario tests). The Jest unit suite and per-module coverage gate are done; this is the remaining browser layer. +- **[012](012-live-app-verification-clock-population.md)** — live-app verification of the clock & population sim.