test(05-01): scaffold routing workspace with failing contract suite
- 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
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
#!/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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user