- routing/test/fixtures/golden-pairs.json: the hand-authored 15-pair pinned
corpus (D-02/D-12) — origins AND destinations are gazetteer landmarks,
klBbox + 100 KB gz budget + probe id + alternatives floor pinned
- routing/scripts/fetch-route-fixtures.mjs: the maintainer build tool
(overpass-topup.mjs pattern) — D-12 boundary in the header, pre-flight
public-coordinate proof against the gazetteer (1e-4 deg), mandatory
User-Agent, 1200 ms inter-request delay, one retry on network/5xx,
collect-then-write, --only=<pairId> single-pair repair merge
- routing/src/data/route-fixtures.json: 15 real OSRM-demo pairs committed
(1-3 verbatim polyline6 routes each; 8 pairs carry >= 2 alternates),
ODbL header fields preserved (T-05-06)
- loader-env L3 armed (RED commit 01b0ed6) + C6 re-pinned from the empty-seed
state to the populated corpus (05-01 documented both as 05-02 handoffs)
- npm delegates: root routing:fixtures; routing fixtures + corpus scripts
164 lines
6.8 KiB
JavaScript
164 lines
6.8 KiB
JavaScript
#!/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}`,
|
||
);
|
||
});
|