test(05-01): add failing fixture-source + loader-env suites (F1-F8, L1-L3)
- synthetic injected datasets only — never the network, never the committed file - F3 pins the 200 m haversine tolerance boundary; F6 the defensive pair skip - F7 the lazy-index source gate (LAW 1); L2 the esbuild inline marker
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* fixture-source.test.mjs — the fixture RoutingSource backend gates (F1–F8).
|
||||
* --------------------------------------------------------------------------
|
||||
* RED first (plan 05-01 task 3): every case fails while
|
||||
* routing/src/fixture-source.ts does not exist.
|
||||
*
|
||||
* Datasets are SYNTHETIC and injected via createFixtureRoutingSource(data) —
|
||||
* never the network, never the committed file (that one is exercised by the
|
||||
* loader-env suite's L3 against the default source).
|
||||
*
|
||||
* F1 empty corpus -> null — the committed seed's state until 05-02
|
||||
* F2 match serves best-first — backend tag, order, decoded geometry,
|
||||
* cumulative field, EMPTY steps (T-05-07)
|
||||
* F3 tolerance boundary — ~111 m serves; ~222 m is honest null
|
||||
* (the 200 m law, haversine-verified)
|
||||
* F4 slug guard — non-slug / over-long destinationId ->
|
||||
* null WITHOUT consulting the index (T-05-03)
|
||||
* F5 unknown destination — valid slug with no pair -> null (D-03,
|
||||
* never a nearest-match)
|
||||
* F6 malformed pair skipped — NaN distance / empty routes / throwing
|
||||
* geometry are skipped at index build; a
|
||||
* well-formed sibling still serves
|
||||
* F7 lazy index — source gate: no `new Map(` and no
|
||||
* `decodePolyline6(` call at TOP LEVEL
|
||||
* (gazetteer LAW 1 — Workers 1 s startup)
|
||||
* F8 getPairOrigin — slug-guarded lookup by pairId; positive
|
||||
* half is deferred while the committed
|
||||
* seed is empty (visible SKIP, the
|
||||
* loader-env L2/L3 precedent)
|
||||
*
|
||||
* Run: npm run routing:test (exit 0 = all green)
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { 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
|
||||
|
||||
// Lazy module loads — missing modules => per-case ERR_MODULE_NOT_FOUND (RED).
|
||||
let polylinePromise = null;
|
||||
function loadPolyline() {
|
||||
if (!polylinePromise) polylinePromise = import("../src/polyline.ts");
|
||||
return polylinePromise;
|
||||
}
|
||||
let sourcePromise = null;
|
||||
function loadSource() {
|
||||
if (!sourcePromise) sourcePromise = import("../src/fixture-source.ts");
|
||||
return sourcePromise;
|
||||
}
|
||||
|
||||
/* ————— synthetic dataset builder (public KL coordinates, 1e-6 grid) ————— */
|
||||
|
||||
const KL_TOWER = { lat: 3.1528, lng: 101.7038, label: "Kuala Lumpur Tower" };
|
||||
const KL_SENTRAL = { lat: 3.1343, lng: 101.6869 };
|
||||
const PATH_A = [
|
||||
{ lat: 3.1528, lng: 101.7038 },
|
||||
{ lat: 3.15, lng: 101.7 },
|
||||
{ lat: 3.14, lng: 101.69 },
|
||||
{ lat: 3.1343, lng: 101.6869 },
|
||||
];
|
||||
const PATH_B = [
|
||||
{ lat: 3.1528, lng: 101.7038 },
|
||||
{ lat: 3.148, lng: 101.699 },
|
||||
{ lat: 3.1343, lng: 101.6869 },
|
||||
];
|
||||
|
||||
async function buildSyntheticDataset() {
|
||||
const { encodePolyline6 } = await loadPolyline();
|
||||
return {
|
||||
version: 1,
|
||||
originToleranceM: 200,
|
||||
pairs: [
|
||||
{
|
||||
pairId: "kl-tower-to-kl-sentral",
|
||||
origin: KL_TOWER,
|
||||
destinationId: "kl-sentral",
|
||||
routes: [
|
||||
{ geometry: encodePolyline6(PATH_A), distanceM: 4800, durationS: 600 },
|
||||
{ geometry: encodePolyline6(PATH_B), distanceM: 5200, durationS: 640 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/* ————— F1–F8 ————— */
|
||||
|
||||
test("F1 empty corpus -> null — every route() resolves null (the seed's state until 05-02)", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const source = createFixtureRoutingSource({ version: 1, originToleranceM: 200, pairs: [] });
|
||||
assert.equal(source.name, "fixture", "the backend tag is 'fixture'");
|
||||
const result = await source.route({
|
||||
origin: { lat: 3.1528, lng: 101.7038 },
|
||||
destinationId: "kl-sentral",
|
||||
});
|
||||
assert.equal(result, null, "an empty corpus is honest-null, never a fabricated route");
|
||||
});
|
||||
|
||||
test("F2 match serves best-first — backend tag, order, decoded geometry, cumulative field, EMPTY steps", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const data = await buildSyntheticDataset();
|
||||
const source = createFixtureRoutingSource(data);
|
||||
const origin = { lat: 3.1528, lng: 101.7038 };
|
||||
const result = await source.route({ origin, destinationId: "kl-sentral" });
|
||||
assert.ok(result, "the pinned origin + matching destinationId must serve");
|
||||
assert.equal(result.backend, "fixture");
|
||||
assert.equal(result.destinationId, "kl-sentral");
|
||||
assert.deepEqual(result.origin, origin);
|
||||
assert.equal(result.options.length, 2, "both synthetic routes serve, order preserved");
|
||||
assert.deepEqual(
|
||||
result.options[0].geometry.points,
|
||||
PATH_A,
|
||||
"options[0] is fixture route 1 (best-first order preserved)",
|
||||
);
|
||||
assert.equal(result.options[0].summary.distanceM, 4800);
|
||||
assert.equal(result.options[0].summary.durationS, 600);
|
||||
assert.equal(result.options[1].summary.distanceM, 5200);
|
||||
const cum = result.options[0].geometry.cumulativeDistanceM;
|
||||
assert.equal(cum.length, PATH_A.length, "cumulative is the same length as points");
|
||||
assert.equal(cum[0], 0, "cumulative starts at 0");
|
||||
for (let i = 1; i < cum.length; i++) {
|
||||
assert.ok(cum[i] >= cum[i - 1], "cumulative is non-decreasing");
|
||||
assert.ok(Number.isFinite(cum[i]), "cumulative entries are finite meters");
|
||||
}
|
||||
for (const option of result.options) {
|
||||
assert.deepEqual(option.steps, [], "fixture-world steps are ALWAYS empty (T-05-07, D-10)");
|
||||
}
|
||||
});
|
||||
|
||||
test("F3 tolerance boundary — ~111 m from the pinned origin serves; ~222 m is honest null", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const data = await buildSyntheticDataset();
|
||||
const source = createFixtureRoutingSource(data);
|
||||
// 0.001 deg lat ~= 111 m (haversine) — inside the 200 m tolerance.
|
||||
const near = await source.route({
|
||||
origin: { lat: 3.1538, lng: 101.7038 },
|
||||
destinationId: "kl-sentral",
|
||||
});
|
||||
assert.ok(near, "GPS jitter within ~111 m of the pinned origin must still serve");
|
||||
// 0.002 deg lat ~= 222 m — outside the 200 m tolerance.
|
||||
const far = await source.route({
|
||||
origin: { lat: 3.1548, lng: 101.7038 },
|
||||
destinationId: "kl-sentral",
|
||||
});
|
||||
assert.equal(far, null, "beyond the tolerance the backend must NOT fabricate a route (D-03)");
|
||||
});
|
||||
|
||||
test("F4 slug guard — non-slug and over-long destinationIds are refused before the index is touched", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const data = await buildSyntheticDataset();
|
||||
const source = createFixtureRoutingSource(data);
|
||||
const origin = { lat: 3.1528, lng: 101.7038 };
|
||||
assert.equal(
|
||||
await source.route({ origin, destinationId: "not a slug!" }),
|
||||
null,
|
||||
"spaces/punctuation are refused by the slug guard (T-05-03)",
|
||||
);
|
||||
assert.equal(
|
||||
await source.route({ origin, destinationId: "a".repeat(65) }),
|
||||
null,
|
||||
"a 65-char id exceeds the 1..64 slug charset — refused (hostile-shape DoS bound)",
|
||||
);
|
||||
});
|
||||
|
||||
test("F5 unknown destination -> null — a valid slug with no pair is the honest pending state (D-03)", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const data = await buildSyntheticDataset();
|
||||
const source = createFixtureRoutingSource(data);
|
||||
const result = await source.route({
|
||||
origin: { lat: 3.1528, lng: 101.7038 },
|
||||
destinationId: "petronas-twin-towers",
|
||||
});
|
||||
assert.equal(result, null, "never a nearest-match geometry for an unknown destination");
|
||||
});
|
||||
|
||||
test("F6 malformed pair skipped — NaN distance / empty routes / throwing geometry never throw to the caller", async () => {
|
||||
const { createFixtureRoutingSource } = await loadSource();
|
||||
const { encodePolyline6 } = await loadPolyline();
|
||||
const broken = {
|
||||
version: 1,
|
||||
originToleranceM: 200,
|
||||
pairs: [
|
||||
{
|
||||
pairId: "broken-nan",
|
||||
origin: { lat: 3.1528, lng: 101.7038, label: "broken distance" },
|
||||
destinationId: "kl-sentral",
|
||||
routes: [
|
||||
{
|
||||
geometry: encodePolyline6([{ lat: 3.1528, lng: 101.7038 }]),
|
||||
distanceM: Number.NaN,
|
||||
durationS: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pairId: "broken-empty-routes",
|
||||
origin: { lat: 3.1528, lng: 101.7038, label: "empty routes" },
|
||||
destinationId: "kl-sentral",
|
||||
routes: [],
|
||||
},
|
||||
{
|
||||
pairId: "broken-throwing-geometry",
|
||||
origin: { lat: 3.1528, lng: 101.7038, label: "truncated polyline" },
|
||||
destinationId: "kl-sentral",
|
||||
routes: [{ geometry: "A", distanceM: 100, durationS: 10 }],
|
||||
},
|
||||
{
|
||||
pairId: "kl-tower-to-kl-sentral",
|
||||
origin: KL_TOWER,
|
||||
destinationId: "kl-sentral",
|
||||
routes: [
|
||||
{
|
||||
geometry: encodePolyline6(PATH_A),
|
||||
distanceM: 4800,
|
||||
durationS: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const source = createFixtureRoutingSource(broken);
|
||||
const served = await source.route({
|
||||
origin: { lat: 3.1528, lng: 101.7038 },
|
||||
destinationId: "kl-sentral",
|
||||
});
|
||||
assert.ok(served, "the well-formed sibling pair still serves");
|
||||
assert.equal(served.options.length, 1, "only the well-formed pair is admitted");
|
||||
assert.equal(served.options[0].summary.distanceM, 4800);
|
||||
});
|
||||
|
||||
test("F7 lazy index — no `new Map(` and no `decodePolyline6(` call at TOP LEVEL (gazetteer LAW 1)", () => {
|
||||
const srcPath = path.join(here, "..", "src", "fixture-source.ts");
|
||||
const src = readFileSync(srcPath, "utf8");
|
||||
// Strip comments first (the no-storage gate pattern) — prose is not a call site.
|
||||
const code = src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||||
for (const line of code.split("\n")) {
|
||||
if (line.trim() === "") continue;
|
||||
if (/^\s/.test(line)) continue; // indented => inside a function — allowed
|
||||
assert.ok(
|
||||
!line.includes("new Map("),
|
||||
`top-level index construction — LAW 1 violated: ${line.trim().slice(0, 60)}`,
|
||||
);
|
||||
assert.ok(
|
||||
!line.includes("decodePolyline6("),
|
||||
`top-level geometry decode — LAW 1 violated: ${line.trim().slice(0, 60)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("F8 getPairOrigin — slug-guarded lookup; positive half deferred while the seed is empty", async () => {
|
||||
const mod = await loadSource();
|
||||
assert.equal(mod.getPairOrigin("not a slug!"), null, "garbage pairId -> null (slug guard)");
|
||||
assert.equal(mod.getPairOrigin("a".repeat(65)), null, "over-long pairId -> null");
|
||||
assert.equal(mod.getPairOrigin("unknown-pair"), null, "unknown pair -> honest null");
|
||||
const pairs = mod.getPairs();
|
||||
assert.ok(Array.isArray(pairs), "getPairs returns an array");
|
||||
if (pairs.length === 0) {
|
||||
console.log(
|
||||
"SKIP F8 (positive half) — the committed seed is empty until plan 05-02 populates it; the lookup machinery is proven by F2's injected dataset",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const first = pairs[0];
|
||||
const origin = mod.getPairOrigin(first.pairId);
|
||||
assert.deepEqual(
|
||||
origin,
|
||||
{ lat: first.origin.lat, lng: first.origin.lng },
|
||||
"getPairOrigin resolves the pinned origin for a known pairId",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user