From 6e5f6b11921118e67b649dc1eb1922843d94bdc8 Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:31:34 -0300 Subject: [PATCH 1/6] docs(tasks): mark 055 done; rewrite 008 as the Playwright integration-suite brief - Rename 055-history-asset-pipeline.md -> _DONE (the offline history-asset pipeline shipped; matches the filename-suffix done convention). - Rewrite the badly-stale 008 (its "current state" still claimed 41 files / 220 tests / ~72% coverage from the project's infancy). The unit suite + coverage are long done and rebuilt into the modular per-module architecture; 008 is now ONLY the Playwright integration suite, written for a next session with full context: the current test/CI architecture, the app's boot/serve/auto-load mechanics, a determinism test-hook design (step-ticks over wall-clock), scenario-save fixtures + a generator script, the HUD + canvas baseline suites and scenario tests the maintainer specified, browser-coverage wiring for the scene/HUD gap the jest gate leaves, and an async NON-BLOCKING CI job (never in ci-success's needs). Co-Authored-By: Claude Opus 4.8 --- .../tasks/008-test-suites-unit-integration.md | 339 ++++++++++++++---- ....md => 055-history-asset-pipeline_DONE.md} | 0 2 files changed, 275 insertions(+), 64 deletions(-) rename docs/tasks/{055-history-asset-pipeline.md => 055-history-asset-pipeline_DONE.md} (100%) 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 From 280ddaaf4ddccbeaf7039abd9b260562c4ef0f60 Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:37:37 -0300 Subject: [PATCH 2/6] docs(tasks): add a backlog index (README) for the tasks folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docs/tasks/README.md β€” the index CLAUDE.md Β§5.1 refers to but which never existed: a status table of all 79 tasks (76 done, 007 superseded, 008 + 012 open), a short arc overview, and the done/superseded filename conventions. Bundled with marking 055 done and the 008 integration-suite rewrite. --- docs/tasks/README.md | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/tasks/README.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. From 6467cd6cef7206b5661583556bbc4b8c21e1baea Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:41:56 -0300 Subject: [PATCH 3/6] ci: fix the `changes` job failing on a no-module (docs-only) PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSON-array emission piped SELECTED through `grep -v '^$'`, which exits 1 when SELECTED is empty (a PR that touches no module and isn't "shared" β€” e.g. this docs/tasks change). Under `set -o pipefail` that failed the whole `changes` step, cascading into ci-success. Replace the grep with `jq 'select(length > 0)'` + `jq -cs 'unique'`, which yields `[]` on empty input without a nonzero exit. (Surfaced by PR #96; the bug shipped to main with the reorg merge.) --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e891eae..52b2a98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,8 +71,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" From 6d412f8c7797bfa36c09c2f234564d40a7215f93 Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:48:16 -0300 Subject: [PATCH 4/6] ci: don't fan module tests out on CI-config/scripts/docs changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `changes` job's "shared β†’ run every module" net included `scripts/` and `.github/workflows/`, so a PR touching only CI config or docs (like this one) ran the full 13-module test matrix for no reason β€” those files can't change a module test's OUTCOME. Narrow SHARED_RE to things that actually affect test results: shared source (types/util/json, the execution spine, GameManager/City/Clock) and the test toolchain config (package(-lock)/tsconfig/jest.config/babel). Now a module runs iff its own src/app/game// or test// changed, or genuinely-shared code did. A docs/CI-config PR runs NO module tests. --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52b2a98..ce9c2fb 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 From b582562c7465fab06c6700f074683c425c7d5d74 Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:52:06 -0300 Subject: [PATCH 5/6] ci: show unaffected module tests as SKIPPED, not absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the `test` job from a dynamic matrix (fromJSON(modules) β€” only creates jobs for affected modules, so unaffected ones vanish from the checks list) to a STATIC matrix of all 13 modules, each gated by `if: contains(fromJSON(needs.changes.outputs.modules), matrix.module)`. Affected modules run; the rest show as skipped. A docs/CI-config PR now shows all 13 test () checks as skipped rather than none at all. --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce9c2fb..7d216df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,18 +150,19 @@ 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. + # One concurrent job per test module β€” fast, granular pass/fail; each runs with --coverage and uploads + # its own per-module report for the `coverage` job to gate. The matrix is the STATIC list of all + # modules, and each entry is gated by a per-module `if` so the ones this PR didn't affect show up as + # SKIPPED in the checks list (not merely absent). `changes` decides which are affected (see that job). test: needs: changes - if: needs.changes.outputs.modules != '[]' + if: ${{ contains(fromJSON(needs.changes.outputs.modules), matrix.module) }} runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: - module: ${{ fromJSON(needs.changes.outputs.modules) }} + module: [world, agents, population, events, actions, execution, economy, skills, objects, history, save, data, util] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 500d84bf066b8957c354df710ba912fc09b86de8 Mon Sep 17 00:00:00 2001 From: Mauricio Fernandes Date: Sat, 11 Jul 2026 23:57:20 -0300 Subject: [PATCH 6/6] =?UTF-8?q?ci:=20show=20unaffected=20module=20tests=20?= =?UTF-8?q?as=20skipped=20=E2=80=94=20one=20job=20per=20module=20(reusable?= =?UTF-8?q?=20workflow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt (static matrix + `if: contains(..., matrix.module)`) is an INVALID workflow β€” `matrix` isn't available in a job-level `if`. To get 13 individually-skippable checks, use one caller job per module whose `if` uses a LITERAL module name (only needs/fromJSON/contains β€” valid in a job `if`), delegating the shared steps to a new reusable workflow (.github/workflows/test-module.yml). Affected modules run; the rest show SKIPPED. Rewire `coverage` + `ci-success` needs to the 13 test- jobs; ci-success now checks `join(needs.*.result, ' ')` so it scales without listing each. --- .github/workflows/ci.yml | 117 ++++++++++++++++++++---------- .github/workflows/test-module.yml | 37 ++++++++++ 2 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/test-module.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d216df..ff41169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,36 +150,77 @@ jobs: - name: Generation perf-regression gates run: npx jest --selectProjects perf --runInBand - # One concurrent job per test module β€” fast, granular pass/fail; each runs with --coverage and uploads - # its own per-module report for the `coverage` job to gate. The matrix is the STATIC list of all - # modules, and each entry is gated by a per-module `if` so the ones this PR didn't affect show up as - # SKIPPED in the checks list (not merely absent). `changes` decides which are affected (see that job). - 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: ${{ contains(fromJSON(needs.changes.outputs.modules), matrix.module) }} - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - module: [world, agents, population, events, actions, execution, economy, skills, objects, history, save, data, util] - 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, @@ -187,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 @@ -227,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