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:
araharah-executor
2026-10-24 04:59:17 +08:00
parent e5b6c06b12
commit 69b6a20350
7 changed files with 267 additions and 2 deletions
+163
View File
@@ -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",
);
});
+10
View File
@@ -0,0 +1,10 @@
/*
* harness.mjs — zero-dependency test registry.
* Same hand-rolled shape as bot/test/harness.mjs (the Phase 2 precedent):
* suites import { test } and register async cases; runner.mjs drains the registry.
*/
const tests = [];
export function test(name, fn) {
tests.push({ name, fn });
}
export const registry = tests;
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
/*
* runner.mjs — hand-rolled zero-dependency test runner.
*
* Same shape as bot/test/runner.mjs (the Phase 2 precedent):
* - Auto-discovers *.test.mjs in this directory (sorted) and dynamic-imports them.
* - A suite that fails to LOAD is recorded as one failing case
* ("suite load failed: <file>") — the run never crashes mid-way.
* - Per case: PASS <name> / FAIL <name> (+ error detail).
* - Summary line + exit codes: exit 0 only when every case passed.
*/
import { readdirSync } from "node:fs";
import { registry } from "./harness.mjs";
const dir = import.meta.dirname;
const suites = readdirSync(dir)
.filter((f) => f.endsWith(".test.mjs"))
.sort();
for (const file of suites) {
try {
await import(`./${file}`);
} catch (loadErr) {
registry.push({
name: `suite load failed: ${file}`,
fn: () => {
throw loadErr;
},
});
}
}
let passedCount = 0;
let failedCount = 0;
for (const { name, fn } of registry) {
try {
await fn();
passedCount++;
console.log(`PASS ${name}`);
} catch (err) {
failedCount++;
console.log(`FAIL ${name}`);
console.error(` ${err && err.stack ? err.stack : String(err)}`);
}
}
console.log(`[runner] ${passedCount} passed, ${failedCount} failed`);
process.exitCode = failedCount > 0 ? 1 : 0;