# TESTING.md Runner: built-in `node:test` + `node:assert/strict`. No Jest/Mocha. Script: `"test": "NODE_ENV=test node --test --test-concurrency=1"`. **Do NOT pass a directory or an unquoted glob** (`node --test tests/` finds nothing on Node 21+; `tests/**/*.test.js` is expanded by `sh` and skips top-level files). Plain `node --test` auto-discovers every `*.test.js`. Test files MUST be named `*.test.js`. ## Layout - `tests/unit/` — pure functions in `src/game` and `src/utils`. No DB, no HTTP. - `tests/integration/` — boot `app` with a **temporary SQLite file**, run migrations + seeds, call the real HTTP API. ## Test DB setup helper (`tests/helpers.js`) - Create temp path via `os.tmpdir()`; set `process.env.DB_PATH` BEFORE requiring `src/db`. - Run migrate + seed programmatically. - Start the app on port 0 (`app.listen(0)`), return base URL and a `close()` fn. - Provide `registerAndLogin(base)` which returns a cookie string to reuse in `fetch` headers. - Delete the temp file in `after()`. ## What MUST be tested | Area | Cases | |---|---| | energy | no time passed; partial tick; full cap; clock went backwards; large gap | | leveling | exact XP threshold; multi-level jump; max level cap | | stats | each class; equipment bonuses applied | | combat | fixed seed → identical log; hero win; hero loss; round cap; min damage 1 | | rewards | level-diff multiplier clamps; loot chance 0% / 100% | | auth | register ok; duplicate (case-insensitive); weak password; login ok/fail; session persists; logout | | hunt | spends energy; insufficient energy rejected; HP too low rejected; locked zone rejected; rewards persisted | | inventory | equip swaps slot; can't equip others' items; use potion consumes 1 | | shop | insufficient gold; level gate; sell equipped rejected; quantities validated | | expedition | claim before end rejected; claim once only; second start while active rejected | | leaderboard | sort allowlist; ordering correct | ## Determinism Inject RNG: `createRng(seed)` (mulberry32) in `src/utils/rng.js` exposing `.float()`, `.int(min,max)`, `.range(min,max)`. In integration tests, allow overriding the RNG via an `app` factory option or env seed. Inject time: tests set `time.setNow(fn)` to fast-forward energy/expeditions instead of sleeping. ## Naming `describe('energy.computeEnergy', () => { it('caps at max', ...) })`. ## Rules - Run `npm test` and check the TOTAL against the sum of tests in all `*.test.js` files. A bad glob once silently skipped 17 tests. - Integration tests use `setup()` from `tests/helpers.js`, which clears every `src/` module from the require cache so each test gets a fresh app and DB. Never hand-pick modules to clear. - Force game outcomes with test-only zones/monsters and the `{ rng, now }` options of services — don't rely on luck, and don't weaken assertions to "winner is hero or monster". - A failing test is never deleted to make the suite green. Fix the code or log a Known Issue. - New bug → write the failing test first, then fix. - Update the Test Summary table in STATUS.md after each phase.