/** * Everything that reads a pack must read the pack the World is drawing. * * ## The bug this is downstream of * * `World`'s constructor is `this.city = reconciledCity(city)` (`world.ts:116`), * so the moment any reconciliation rule is on, there are **two** Californias in * the process: the object `cities/california.ts` exported, and the rewritten one * the engine is actually projecting, exaggerating and drawing roads from. * * Two consumers were reading the first one while the camera stood in the second: * * - `buildLadder` (`main.ts`) derives every rung's stand-off from `focus`, so * a raw-pack ladder measures a world nobody is looking at — the Places rail * silently disagreeing with the camera about where it is. * - `createMinimap` was handed `entry.city` beside a reconciled `handle.world`, * so the plan view drew a different California than the frame beside it. * * Neither is visible with the flag off, because `reconciledCity` returns the * pack **by identity** when no rule is on — which is exactly what made the bug * survive: it is latent until the day somebody turns a rule on to take a * measurement, and then it corrupts the measurement rather than announcing * itself. * * ## What is asserted * * Not "main.ts calls the right function" — that is a source-text assertion and * `integration/sceneWiring.test.ts` already owns that genre. This asserts the * *property*: for every pack and every rule, the reconciled object differs from * the raw one in the columns that rule owns, so a consumer holding the wrong one * is holding provably different numbers. A future consumer wired to the raw pack * is a bug this file describes even if it cannot name the call site. */ import assert from "node:assert/strict"; import { after, describe, it } from "node:test"; import CALIFORNIA from "../cities/california.ts"; import SAN_FRANCISCO from "../cities/sf.ts"; import SOCAL from "../cities/socal.ts"; import { RULES, reconciledCity, setReconcile, setReconcileWarn } from "../cities/reconcile.ts"; import type { Rule } from "../cities/reconcile.ts"; import type { City } from "../engine/types.ts"; const PACKS: readonly (readonly [string, City])[] = [ ["california", CALIFORNIA], ["sf", SAN_FRANCISCO], ["socal", SOCAL], ]; after(() => { setReconcile(null); setReconcileWarn(null); }); /** The columns each rule owns, as the reader of a pack would see them. */ const OWNED: Readonly unknown>> = { projection: (city) => city.lngScale ?? null, exaggeration: (city) => city.verticalExaggeration, roads: (city) => city.roads.map((road) => road.width).join(","), ground: (city) => `${city.coastFalloff}|${JSON.stringify(city.palette ?? null)}`, }; describe("a reconciled pack is not the pack the module exported", () => { it("returns the very same object when nothing is on", () => { setReconcile(false); for (const [id, pack] of PACKS) { assert.equal(reconciledCity(pack), pack, `${id} was copied with the flag off`); } }); /** * The load-bearing one. If a rule changes nothing for every pack then a * consumer reading the raw pack is harmless and this whole file is theatre — * so the file has to prove it is not. */ it("changes the column its rule owns, for at least one pack, for every rule", () => { for (const rule of RULES) { setReconcile([rule]); const moved = PACKS.filter(([, pack]) => { const read = OWNED[rule]; return read(reconciledCity(pack)) !== read(pack); }); assert.ok( moved.length > 0, `rule "${rule}" changed nothing on any pack — either the rule is dead ` + "or OWNED is reading the wrong column", ); } }); it("never mutates the pack the module exported", () => { const before = PACKS.map(([, pack]) => JSON.stringify(pack)); setReconcile(true); for (const [, pack] of PACKS) reconciledCity(pack); setReconcile(false); for (const [index, [id]] of PACKS.entries()) { assert.equal(JSON.stringify(PACKS[index]![1]), before[index], `${id} was mutated in place`); } }); /** * The worker's half of the contract. `World` posts `this.city` to the terrain * worker as a structured clone and the worker builds a second `World` from it; * `reconciled: true` is what stops that second pass applying every rule again * on top of itself. Without it the exaggeration rule would square. */ it("is idempotent, so the worker's second pass is a no-op", () => { setReconcile(true); for (const [id, pack] of PACKS) { const once = reconciledCity(pack); assert.equal(once.reconciled, true, `${id} did not mark itself reconciled`); assert.equal(reconciledCity(once), once, `${id} was reconciled twice`); } }); }); describe("the rule parser", () => { it("takes exact rule names", () => { setReconcile(null); for (const rule of RULES) { // Through the public surface: setReconcile with an explicit list is what // a node script uses, and the query-string path shares `parse`. setReconcile([rule]); const read = OWNED[rule]; const moved = PACKS.some(([, pack]) => read(reconciledCity(pack)) !== read(pack)); assert.ok(moved, `"${rule}" did not select itself`); } }); /** * The defect: `parse` prefix-matched, so `?reconcile=palette` selected nothing * and produced an empty set — **identical to the flag being absent**. A * photograph taken to judge a rule was a photograph of the raw board, and * nothing in the picture could tell you which. */ it("warns loudly rather than silently reading an unknown rule as off", async () => { const said: string[] = []; setReconcileWarn((message) => said.push(message)); setReconcile(null); const { activeRules } = await import("../cities/reconcile.ts"); const search = { search: "?reconcile=palette" }; const globals = globalThis as { location?: unknown }; const had = "location" in globals; const previous = globals.location; globals.location = search; try { const on = activeRules(); assert.equal(on.size, 0, "an unknown rule must not select a real one"); assert.equal(said.length, 1, "an unknown rule must warn exactly once"); assert.match(said[0] ?? "", /unknown rule "palette"/); assert.match(said[0] ?? "", /the flag is NOT off/); } finally { if (had) globals.location = previous; else delete globals.location; setReconcileWarn(null); } }); /** * The other half of prefix-matching: one letter used to select a whole rule, * which is fine until two rules share it. `p` must now select nothing. */ it("does not accept an abbreviation", async () => { const said: string[] = []; setReconcileWarn((message) => said.push(message)); setReconcile(null); const { activeRules } = await import("../cities/reconcile.ts"); const globals = globalThis as { location?: unknown }; const had = "location" in globals; const previous = globals.location; globals.location = { search: "?reconcile=p" }; try { assert.equal(activeRules().size, 0, '"p" selected a rule by prefix'); assert.equal(said.length, 1); } finally { if (had) globals.location = previous; else delete globals.location; setReconcileWarn(null); } }); });