#!/usr/bin/env node /* * contract.test.mjs — the RoutingSource contract gates (C1–C6). * ------------------------------------------------------------- * RED first (plan 05-01 task 1): every import-bearing case fails while * routing/src/types.ts does not exist — the lazy-import pattern (the * bot/test/initdata.test.mjs precedent) keeps each case reporting its * own failure instead of losing the suite load. * * C1 interface shape — the seam module loads; a stub * { name, route } behaves as the contract * C2 day-one model fields — cumulativeDistanceM + steps: RouteStep[] * + the GUID-05/06 obligation, grep-pinned * by test, not by hope (D-07/D-11) * C3 null contract — the RoutingSource doc comment names the * "router pending" semantics (D-03) * C4 no logging/storage/network — ZERO banned tokens in routing/src * code (comments stripped first — the * web/test/no-storage.test.mjs pattern) * C5 zero runtime deps — no "dependencies" in routing/package.json * (D-05: nothing in the Worker/browser path) * C6 ODbL header present — the committed seed carries license + * attribution + notice (T-05-06) * * Run: npm run routing:test (exit 0 = all green) */ import { strict as assert } from "node:assert"; import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "./harness.mjs"; const here = path.dirname(fileURLToPath(import.meta.url)); // routing/test const routingDir = path.resolve(here, ".."); // routing/ const srcDir = path.join(routingDir, "src"); // routing/src // Lazy types load — missing module => per-case ERR_MODULE_NOT_FOUND (the RED state). let typesPromise = null; function loadTypes() { if (!typesPromise) { typesPromise = import("../src/types.ts"); } return typesPromise; } /* path-scrape + comment strip (the no-storage gate's contract) */ function stripComments(src) { return src .replace(/\/\*[\s\S]*?\*\//g, " ") // block comments .replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (benign over-stripping) } const BANNED = [ "console.", "localStorage", "sessionStorage", "indexedDB", "document.cookie", ".put(", "fetch(", "XMLHttpRequest", "process.env", ]; /* ————— C1 — the seam exists and a stub behaves as the contract ————— */ test("C1 interface shape — the seam module loads; a stub {name, route} behaves as the contract", async () => { await loadTypes(); // types.ts parses under Node 24 type-stripping // Structural: a minimal backend satisfying RoutingSource (D-01) — // route() is a function and resolves a Promise (null = router pending). const stub = { name: "x", route: async () => null, }; assert.equal(typeof stub.route, "function", "route() must be a function"); const pending = stub.route({ origin: { lat: 0, lng: 0 }, destinationId: "x" }); assert.ok(pending instanceof Promise, "route() must return a Promise"); assert.equal(await pending, null, "the honest pending state resolves null"); }); /* ————— C2 — the day-one route model fields are grep-pinned ————— */ test("C2 day-one model fields — D-07/D-11 type gate pins cumulativeDistanceM + steps + GUID-05/06", () => { const text = readFileSync(path.join(srcDir, "types.ts"), "utf8"); assert.ok( text.includes("cumulativeDistanceM"), "RouteGeometry.cumulativeDistanceM (snapped-progress support) is missing — D-07 violated", ); assert.ok( text.includes("steps: RouteStep[]"), "RouteOption.steps (day-one step geometry) is missing — D-07 violated", ); assert.ok( text.includes("GUID-05/06 upstream obligation"), "the GUID-05/06 upstream-obligation note is missing from the day-one fields", ); }); /* ————— C3 — the null contract is contract documentation ————— */ test("C3 null contract — the RoutingSource doc comment names the router-pending semantics (D-03)", () => { const text = readFileSync(path.join(srcDir, "types.ts"), "utf8"); const at = text.indexOf("export interface RoutingSource"); assert.ok(at > 0, "export interface RoutingSource is missing"); const before = text.slice(Math.max(0, at - 2000), at); const doc = before.match(/\/\*\*[\s\S]*$/); // the last doc block before the interface assert.ok(doc, "RoutingSource must carry a doc comment"); assert.ok( doc[0].includes("router pending"), "the D-03 null contract ('router pending') must be documented on the interface", ); }); /* ————— C4 — the three zero-laws, mechanically ————— */ test("C4 no-logging/no-storage/no-network — zero banned tokens in routing/src code (comments stripped)", () => { const files = readdirSync(srcDir).filter((f) => f.endsWith(".ts")); assert.ok(files.length >= 1, "scanned a real src tree"); for (const file of files) { const code = stripComments(readFileSync(path.join(srcDir, file), "utf8")); for (const banned of BANNED) { assert.ok( !code.includes(banned), `${file} references ${banned} — the D-05/D-12/D-15 zero-laws are violated`, ); } } }); /* ————— C5 — zero runtime dependencies ————— */ test("C5 zero runtime dependencies — routing/package.json has no dependencies field (D-05)", () => { const pkg = JSON.parse(readFileSync(path.join(routingDir, "package.json"), "utf8")); assert.equal(pkg.name, "@araharah/routing"); assert.equal(pkg.type, "module"); assert.equal( pkg.dependencies, undefined, "a runtime dependency would ride the Worker/browser bundle path — D-05 violated", ); }); /* ————— C6 — the committed dataset's ODbL header (T-05-06) ————— */ test("C6 ODbL header + populated corpus — the committed dataset carries license, attribution, notice, and the golden pairs", () => { const seed = JSON.parse( readFileSync(path.join(srcDir, "data", "route-fixtures.json"), "utf8"), ); assert.equal(seed._license, "ODbL-1.0", "the ODbL license field is missing"); assert.ok( String(seed._attribution).includes("OpenStreetMap"), "the attribution must credit OpenStreetMap contributors", ); assert.ok(String(seed._notice).includes("ODbL"), "the notice must name ODbL"); assert.equal(seed.version, 1); assert.ok(Array.isArray(seed.pairs), "pairs must be an array"); assert.ok( seed.pairs.length >= 10 && seed.pairs.length <= 20, `the committed corpus carries the golden pairs (10..20) — 05-01 shipped an EMPTY seed ` + `explicitly so plan 05-02's build-time script could populate it; found ${seed.pairs.length}`, ); });