Files
arah2/routing/test/astar.test.mjs
T
araharah-executor d3f0b4577f test(08.1-01): re-pin codec + search suites for ARG3 and the CH query (RED)
- graph.test.mjs: G1-G11 re-pinned for the ARG3 layout (chShortcutCount
  @60, CH suffix); G7 suspended until task 3 regenerates the fixture
- astar.test.mjs: committed-fixture cases A1/A2/A4/A5/A6 suspended;
  A3 re-pinned on an in-memory ch block; A8 extended to ch-query.ts;
  A11 becomes the D-04 one-search-path pin; ALT-era A9/A10/A12/A13 fold
- ch-query.test.mjs: Q1-Q5 (625-pair equivalence, directed oneway law,
  original-edge seconds, island nulls, structural deletion) + G1-G3
  in-memory ARG3 pins; all fixtures through contractGraph + encodeGraph
2026-10-26 06:48:24 +08:00

217 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
/*
* astar.test.mjs — the search surface + turn classifier + step derivation.
* ----------------------------------------------------------------------------
* RED first (plan 06-01 task 3): behavior cases fail while
* routing/src/astar.ts does not exist (lazy-import pattern).
*
* Phase 08.1 re-pin (plan 01 task 2): routeAstar is CH-backed — the ALT
* machinery is DELETED (D-04/D-05: one search path, no dormant engine).
* The exhaustive equivalence law (A1's 625-pair sweep) and the oneway /
* steps / classifier laws carry over UNCHANGED in intent; the
* committed-fixture cases (A1/A2/A4/A5/A6) are SUSPENDED here because
* mini-graph.bin is still ARG2 at this point in the plan — task 3
* regenerates the fixture through the ARG3 encode path and un-suspends
* them in place. The in-memory and structural cases are re-pinned HERE:
*
* A3 unreachable -> null — honest miss (synthetic disconnected
* graph, ch block through the ONE authority)
* A7 public surface — index.ts exposes the engine-A exports;
* createHttpRoutingSource is LIVE
* A8 structural — no `new Map(` in astar.ts AND ch-query.ts
* (allocation-free hot loop: typed arrays only)
* A11 structural (CH law) — the uphill rank filter + shortcut
* unpacking exist in ch-query.ts CODE;
* astar.ts contains NO landmark token
* (D-04's one-search-path pin)
* A1 exhaustive equivalence — SUSPENDED until task 3 (ARG3 fixture)
* A2 oneway respected — SUSPENDED until task 3 (ARG3 fixture)
* A4 deriveSteps L-path — SUSPENDED until task 3 (ARG3 fixture)
* A5 depart + arrive always — SUSPENDED until task 3 (ARG3 fixture)
* A6 classifyTurn pins — SUSPENDED until task 3 (ARG3 fixture)
*
* The A9/A10/A12/A13 ALT-era cases (island / search-scoped skip / oneway
* ring / mixed columns) folded into ch-query.test.mjs: their surviving
* content is CH-law Dijkstra equivalence over directed fixtures — the
* ALT-specific table assertions are gone with the tables (D-05).
*
* 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
let astarPromise = null;
function loadAstar() {
if (!astarPromise) astarPromise = import("../src/astar.ts");
return astarPromise;
}
let graphPromise = null;
function loadGraph() {
if (!graphPromise) graphPromise = import("../src/graph.ts");
return graphPromise;
}
function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
// loadMiniGraph() returns in task 3, when the committed fixture is ARG3
// and the suspended cases un-suspend.
/** Brute-force O(n^2) Dijkstra — plain arrays, SAME traversal law as CH. */
function dijkstra(g, origin, goal) {
const dist = new Array(g.nodeCount).fill(Infinity);
const done = new Array(g.nodeCount).fill(false);
dist[origin] = 0;
for (;;) {
let u = -1;
let best = Infinity;
for (let i = 0; i < g.nodeCount; i++) {
if (!done[i] && dist[i] < best) {
best = dist[i];
u = i;
}
}
if (u === -1 || u === goal) return dist[goal];
done[u] = true;
for (let e = g.edgeStart[u]; e < g.edgeStart[u + 1]; e++) {
if ((g.edgeFlags[e] & 1) !== 0) continue; // oneway-reverse: never relaxed
const v = g.edgeTarget[e];
const nd = dist[u] + g.edgeSeconds[e];
if (nd < dist[v]) dist[v] = nd;
}
}
}
/* ————— A3 — the in-memory unreachable-pair law ————— */
test("A3 unreachable -> null — a disconnected pair resolves the honest miss", async () => {
const { routeAstar } = await loadAstar();
const { encodeGraph, decodeGraph } = await loadGraph();
// Two disconnected components: 0<->1 and 2<->3. The ch block is
// hand-built (the plan's option B): contractGraph over two disjoint
// mirrors emits no shortcut, and an ARG3 with chShortcutCount = 0 is
// not a road graph (decode refuses — G11), so the block carries one
// harmless shortcut (0->1, via 3, weight 4 — equal to the direct edge,
// rank-upward: rank(from)=1 > rank(via)=0, rank(to)=2 > 0).
const g = decodeGraph(
encodeGraph({
lat: [3.15, 3.1505, 3.16, 3.1605],
lng: [101.69, 101.6905, 101.69, 101.6905],
edges: [
{ from: 0, to: 1, seconds: 4, nameId: 0 },
{ from: 1, to: 0, seconds: 4, nameId: 0 },
{ from: 2, to: 3, seconds: 4, nameId: 0 },
{ from: 3, to: 2, seconds: 4, nameId: 0 },
],
names: ["Isolated Road"],
maxSpeedKmh: 60,
ch: {
rank: Uint32Array.of(1, 2, 3, 0),
shortcutStart: Uint32Array.of(0, 1, 1, 1, 1),
shortcutTarget: Uint32Array.of(1),
shortcutSeconds: Float32Array.of(4),
shortcutVia: Uint32Array.of(3),
},
}),
);
assert.ok(g, "synthetic graph decodes");
assert.equal(dijkstra(g, 0, 2), Infinity, "Dijkstra agrees: unreachable");
assert.equal(routeAstar(g, 0, 2), null, "unreachable -> null (never a fabricated path)");
const within = routeAstar(g, 0, 1);
assert.ok(within, "the connected pair routes");
assert.deepEqual(within.nodes, [0, 1]);
assert.ok(Math.abs(within.seconds - 4) < 1e-6, "the hop costs its edge weight");
});
/* ————— A7/A8/A11 — the public surface + structural CH laws ————— */
test("A7 public surface — index.ts exposes engine A + the engine-a HTTP source (re-pinned 06-04)", async () => {
const mod = await import("../src/index.ts");
for (const name of [
"decodeGraph",
"encodeGraph",
"GRAPH_MAGIC",
"GRAPH_VERSION",
"snapToGraph",
"SNAP_BBOX_MARGIN_DEG",
"routeAstar",
"deriveSteps",
"classifyTurn",
"TURN_STRAIGHT_MAX_DEG",
"TURN_SLIGHT_MAX_DEG",
"TURN_UTURN_MIN_DEG",
"createHttpRoutingSource",
"contractGraph",
]) {
assert.notEqual(mod[name], undefined, `index.ts must export ${name}`);
}
assert.equal(typeof mod.createHttpRoutingSource, "function", "backend #2's client seam is live (D-01, 06-04)");
});
test("A8 structural — no `new Map(` in astar.ts AND ch-query.ts (typed arrays only in the hot loop)", () => {
for (const file of ["astar.ts", "ch-query.ts"]) {
const src = readFileSync(path.join(here, "..", "src", file), "utf8");
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
assert.ok(!code.includes("new Map("), `${file}: no Map allocation — the hot loop is typed arrays`);
}
});
test("A11 structural (CH law) — the uphill rank filter + unpacking live in ch-query.ts; astar.ts has NO landmark token (D-04)", () => {
// astar.ts is a thin CH delegation + turn/step derivation: the
// comment-stripped code must contain NONE of the deleted ALT
// machinery — a dormant second engine would violate D-04's one-path law.
const astarSrc = readFileSync(path.join(here, "..", "src", "astar.ts"), "utf8");
const astarCode = astarSrc
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
for (const token of ["landmarkTo", "landmarkFrom", "cleanColumnsFor", "LANDMARK_SENTINEL"]) {
assert.ok(!astarCode.includes(token), `comment-stripped astar.ts must NOT contain ${token}`);
}
assert.ok(astarCode.includes("routeCH"), "routeAstar delegates to the CH query in code");
// The CH machinery itself lives in ch-query.ts — the uphill rank filter
// (chRank reads), the shortcut CSR, and the via-unpacking are CODE, not
// comments (a comment-only mention would strip away and fail this pin).
const chSrc = readFileSync(path.join(here, "..", "src", "ch-query.ts"), "utf8");
const chCode = chSrc
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
for (const token of ["chRank", "shortcutVia", "shortcutStart", "edgeFlags"]) {
assert.ok(chCode.includes(token), `comment-stripped ch-query.ts must reference ${token} in code`);
}
assert.ok(chCode.includes("peekKey"), "the bidirectional stop rule reads the heap minima (peekKey)");
assert.ok(chCode.includes("landmark") === false, "ch-query.ts carries no landmark machinery either");
});
/* ————— suspended committed-fixture cases (task 3 un-suspends) ————— */
test("A1 exhaustive equivalence — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// The committed fixture is still ARG2 here; the CH query's exhaustive
// 625-pair Dijkstra-equivalence law runs in ch-query.test.mjs (Q1) on
// an in-memory ARG3 grid until task 3 re-pins this case in place.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
});
test("A2 oneway respected — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
});
test("A4 deriveSteps L-path — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
});
test("A5 depart + arrive always — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
});
test("A6 classifyTurn pins — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
});