- fifth npm workspace @araharah/routing (buildless TS, search/ precedent) - root delegates routing:test / routing:typecheck / routing:corpus - C1-C6 RED: contract gates fail while src/types.ts + seed are absent
164 lines
6.6 KiB
JavaScript
164 lines
6.6 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 seed's ODbL header (T-05-06) ————— */
|
||
|
||
test("C6 ODbL header present — the committed seed carries license + attribution + notice", () => {
|
||
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.equal(
|
||
seed.pairs.length,
|
||
0,
|
||
"the seed ships EMPTY — plan 05-02's build-time script populates it",
|
||
);
|
||
});
|