#!/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)"); });