- 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
518 lines
21 KiB
JavaScript
518 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
||
/*
|
||
* ch-query.test.mjs — the uphill bidirectional CH query gates (Q1–Q5)
|
||
* plus the in-memory ARG3 codec pins (G1–G3).
|
||
* ----------------------------------------------------------------------------
|
||
* RED first (plan 08.1-01 task 2): every case fails while
|
||
* routing/src/ch-query.ts does not exist (lazy-import pattern).
|
||
*
|
||
* This suite is where the deleted ALT cases (astar.test.mjs A9/A10/A12/A13)
|
||
* FOLDED per D-04/D-05: their surviving content — Dijkstra equivalence over
|
||
* directed fixtures, the unreachable miss, the oneway law — is re-pinned
|
||
* against the CH query; the ALT table assertions are gone with the tables.
|
||
* ALL fixtures here are IN-MEMORY (encodeGraph → decodeGraph through the
|
||
* ONE authority); the committed mini-graph.bin is still ARG2 in this task
|
||
* and is NOT read (task 3 regenerates it and re-pins the committed-fixture
|
||
* cases in graph/astar.test.mjs).
|
||
*
|
||
* Q1 exhaustive equivalence — the CH query equals brute-force Dijkstra
|
||
* for EVERY pair of the 25-node in-memory
|
||
* grid; path endpoints are origin/goal
|
||
* Q2 directed oneway law — ring + chord + one-way-in spur: every pair
|
||
* matches the oracle in BOTH directions and
|
||
* every returned hop is a traversable
|
||
* original edge (bit0 never relaxed)
|
||
* Q3 original-edge seconds — reported seconds equal a fresh sum over
|
||
* the returned ORIGINAL-edge path (shortcut
|
||
* weights steer only — Pitfall 3)
|
||
* Q4 unreachable -> null — the island fold (A9/A10): cross-component
|
||
* pairs resolve null; internal pairs stay
|
||
* Dijkstra-equal
|
||
* Q5 structural deletion — no ALT tokens in astar.ts; landmark.ts and
|
||
* landmark.test.mjs ABSENT; index.ts exports
|
||
* no landmark symbols (D-04/D-05)
|
||
* G1 ARG3 round-trip — contractGraph output round-trips rank +
|
||
* shortcut CSR exactly through zero-copy views
|
||
* G2 rejection vectors — ARG1 bytes -> null; a version-2 tamper -> null
|
||
* G3 fail-honest decode — truncated / hostile-count / chShortcutCount=0
|
||
* buffers -> null BEFORE any typed-array
|
||
* allocation (T-08.1-01/T-08.1-02)
|
||
*
|
||
* Run: npm run routing:test (exit 0 = all green)
|
||
*/
|
||
import { strict as assert } from "node:assert";
|
||
import { readFileSync, existsSync } 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 chQueryPromise = null;
|
||
function loadChQuery() {
|
||
if (!chQueryPromise) chQueryPromise = import("../src/ch-query.ts");
|
||
return chQueryPromise;
|
||
}
|
||
let graphPromise = null;
|
||
function loadGraph() {
|
||
if (!graphPromise) graphPromise = import("../src/graph.ts");
|
||
return graphPromise;
|
||
}
|
||
let contractionPromise = null;
|
||
function loadContraction() {
|
||
if (!contractionPromise) contractionPromise = import("../src/contraction.ts");
|
||
return contractionPromise;
|
||
}
|
||
|
||
function toArrayBuffer(buf) {
|
||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||
}
|
||
|
||
let geoPromise = null;
|
||
async function haversineM(a, b) {
|
||
if (!geoPromise) geoPromise = import("../src/geo.ts");
|
||
const { haversineM: h } = await geoPromise;
|
||
return h(a, b);
|
||
}
|
||
|
||
/** Brute-force O(n^2) Dijkstra — plain arrays, SAME traversal law. */
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** The traversable (bit0 clear) original edge u -> v, or -1. */
|
||
function findTraversableEdge(g, u, v) {
|
||
for (let e = g.edgeStart[u]; e < g.edgeStart[u + 1]; e++) {
|
||
if (g.edgeTarget[e] === v && (g.edgeFlags[e] & 1) === 0) return e;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/** Fresh sum over the returned ORIGINAL-edge path (the Pitfall 3 oracle). */
|
||
function resumSeconds(g, nodes) {
|
||
let seconds = 0;
|
||
for (let i = 1; i < nodes.length; i++) {
|
||
const e = findTraversableEdge(g, nodes[i - 1], nodes[i]);
|
||
if (e < 0) return -1; // a non-traversable hop: the caller asserts loud
|
||
seconds += g.edgeSeconds[e];
|
||
}
|
||
return seconds;
|
||
}
|
||
|
||
/* ————— in-memory fixtures (encodeGraph → decodeGraph, the ONE authority) ————— */
|
||
|
||
/**
|
||
* The 25-node grid, the committed fixture's shape rebuilt in memory:
|
||
* 5x5, 80 directed edge entries, one oneway pair (2->3), perimeter 60
|
||
* km/h + interior 30 km/h, three street names. Contracted through the
|
||
* ONE CH core, encoded through the ONE codec.
|
||
*/
|
||
async function buildGridGraph() {
|
||
const { encodeGraph, decodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const lat = [];
|
||
const lng = [];
|
||
for (let r = 0; r < 5; r++) {
|
||
for (let c = 0; c < 5; c++) {
|
||
lat.push((3150000 + r * 500) / 1e6);
|
||
lng.push((101690000 + c * 500) / 1e6);
|
||
}
|
||
}
|
||
const id = (r, c) => r * 5 + c;
|
||
const edges = [];
|
||
const addEdge = async (u, v, perimeter) => {
|
||
const kmh = perimeter ? 60 : 30;
|
||
const meters = await haversineM({ lat: lat[u], lng: lng[u] }, { lat: lat[v], lng: lng[v] });
|
||
edges.push({ from: u, to: v, seconds: meters / (kmh / 3.6) });
|
||
};
|
||
for (let r = 0; r < 5; r++) {
|
||
for (let c = 0; c < 4; c++) {
|
||
await addEdge(id(r, c), id(r, c + 1), r === 0 || r === 4);
|
||
await addEdge(id(r, c + 1), id(r, c), r === 0 || r === 4);
|
||
}
|
||
}
|
||
for (let c = 0; c < 5; c++) {
|
||
for (let r = 0; r < 4; r++) {
|
||
await addEdge(id(r, c), id(r + 1, c), c === 0 || c === 4);
|
||
await addEdge(id(r + 1, c), id(r, c), c === 0 || c === 4);
|
||
}
|
||
}
|
||
const reverse = edges.find((e) => e.from === 3 && e.to === 2);
|
||
if (!reverse) throw new Error("oneway reverse edge not found — grid invariant broken");
|
||
reverse.flags = 1;
|
||
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
|
||
const ch = contractGraph({ nodeCount: 25, edges });
|
||
const g = decodeGraph(
|
||
encodeGraph({
|
||
lat,
|
||
lng,
|
||
edges,
|
||
names: ["Jalan Mini 1", "Jalan Mini 2", "Jalan Persiaran Mini"],
|
||
maxSpeedKmh: 60,
|
||
ch,
|
||
}),
|
||
);
|
||
assert.ok(g, "the in-memory ARG3 grid decodes");
|
||
assert.ok(g.chShortcutCount > 0, "the contracted grid carries a hierarchy");
|
||
return g;
|
||
}
|
||
|
||
/**
|
||
* The directed oneway fixture (the A12 fold): a directed 8-node ring
|
||
* (10 s one-way hops; every reverse mirror stored with bit0 — never
|
||
* relaxed), a bidirectional chord (0<->4, 15 s), and a one-way-in spur
|
||
* head (node 8 -> 3: nothing reaches 8). Directed reach asymmetry is
|
||
* exactly what broke the ALT four-family potential; under CH the
|
||
* shortcuts inherit direction structurally.
|
||
*/
|
||
async function buildRingGraph() {
|
||
const { encodeGraph, decodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const lat = [];
|
||
const lng = [];
|
||
for (let i = 0; i < 8; i++) {
|
||
lat.push((3100000 + Math.round(400 * Math.sin((i / 8) * 2 * Math.PI))) / 1e6);
|
||
lng.push((101700000 + Math.round(400 * Math.cos((i / 8) * 2 * Math.PI))) / 1e6);
|
||
}
|
||
lat.push(3095000 / 1e6); // node 8: the spur head
|
||
lng.push(101705000 / 1e6);
|
||
const edges = [];
|
||
for (let i = 0; i < 8; i++) {
|
||
const next = (i + 1) % 8;
|
||
edges.push({ from: i, to: next, seconds: 10, flags: 0 }); // the one-way hop
|
||
edges.push({ from: next, to: i, seconds: 10, flags: 1 }); // bit0: stored, never relaxed
|
||
}
|
||
edges.push({ from: 0, to: 4, seconds: 15, flags: 0 }); // bidirectional chord
|
||
edges.push({ from: 4, to: 0, seconds: 15, flags: 0 });
|
||
edges.push({ from: 8, to: 3, seconds: 10, flags: 0 }); // one-way-in spur
|
||
const ch = contractGraph({ nodeCount: 9, edges });
|
||
const g = decodeGraph(
|
||
encodeGraph({
|
||
lat,
|
||
lng,
|
||
edges,
|
||
names: ["Ring Road"],
|
||
maxSpeedKmh: 60,
|
||
ch,
|
||
}),
|
||
);
|
||
assert.ok(g, "the oneway-ring synthetic decodes");
|
||
assert.equal(g.nodeCount, 9);
|
||
return g;
|
||
}
|
||
|
||
/**
|
||
* The island fixture (the A9/A10 fold): component A is a 3x3 grid
|
||
* (nodes 0-8), component B a separate 4-node path (nodes 9-12) far
|
||
* apart. Cross-component pairs are unreachable — the honest miss.
|
||
*/
|
||
async function buildIslandGraph() {
|
||
const { encodeGraph, decodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const lat = [];
|
||
const lng = [];
|
||
for (let r = 0; r < 3; r++) {
|
||
for (let c = 0; c < 3; c++) {
|
||
lat.push((3150000 + r * 500) / 1e6);
|
||
lng.push((101690000 + c * 500) / 1e6);
|
||
}
|
||
}
|
||
for (let i = 0; i < 4; i++) {
|
||
lat.push((3200000 + i * 500) / 1e6);
|
||
lng.push((101750000 + i * 500) / 1e6);
|
||
}
|
||
const edges = [];
|
||
const link = (u, v) => {
|
||
edges.push({ from: u, to: v, seconds: 10, flags: 0 });
|
||
edges.push({ from: v, to: u, seconds: 10, flags: 0 });
|
||
};
|
||
for (let r = 0; r < 3; r++) {
|
||
for (let c = 0; c < 3; c++) {
|
||
if (c < 2) link(r * 3 + c, r * 3 + c + 1);
|
||
if (r < 2) link(r * 3 + c, (r + 1) * 3 + c);
|
||
}
|
||
}
|
||
for (let i = 0; i < 3; i++) link(9 + i, 10 + i);
|
||
const ch = contractGraph({ nodeCount: 13, edges });
|
||
const g = decodeGraph(
|
||
encodeGraph({
|
||
lat,
|
||
lng,
|
||
edges,
|
||
names: ["Island Road"],
|
||
maxSpeedKmh: 60,
|
||
ch,
|
||
}),
|
||
);
|
||
assert.ok(g, "the island synthetic decodes");
|
||
assert.equal(g.nodeCount, 13);
|
||
return g;
|
||
}
|
||
|
||
/* ————— Q1–Q5 ————— */
|
||
|
||
test("Q1 exhaustive equivalence — the CH query matches brute-force Dijkstra on EVERY 25x25 node pair", async () => {
|
||
const { routeCH } = await loadChQuery();
|
||
const g = await buildGridGraph();
|
||
let pairs = 0;
|
||
let reachable = 0;
|
||
for (let o = 0; o < g.nodeCount; o++) {
|
||
for (let t = 0; t < g.nodeCount; t++) {
|
||
pairs++;
|
||
const optimal = dijkstra(g, o, t);
|
||
const result = routeCH(g, o, t);
|
||
if (optimal === Infinity) {
|
||
assert.equal(result, null, `pair ${o}->${t}: unreachable must resolve null, never a path`);
|
||
continue;
|
||
}
|
||
reachable++;
|
||
assert.ok(result, `pair ${o}->${t}: reachable must route`);
|
||
assert.ok(
|
||
Math.abs(result.seconds - optimal) < 1e-6,
|
||
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
|
||
);
|
||
assert.equal(result.nodes[0], o, `pair ${o}->${t}: path starts at origin`);
|
||
assert.equal(result.nodes[result.nodes.length - 1], t, `pair ${o}->${t}: path ends at goal`);
|
||
}
|
||
}
|
||
assert.equal(pairs, 625, "the full 25x25 sweep ran");
|
||
assert.ok(reachable > 600, `the grid is densely connected (${reachable} reachable pairs)`);
|
||
});
|
||
|
||
test("Q2 directed oneway law — ring + chord + spur matches the oracle in BOTH directions; every hop is a traversable original edge", async () => {
|
||
const { routeCH } = await loadChQuery();
|
||
const g = await buildRingGraph();
|
||
let pairs = 0;
|
||
let reachable = 0;
|
||
let crossNulls = 0;
|
||
for (let o = 0; o < g.nodeCount; o++) {
|
||
for (let t = 0; t < g.nodeCount; t++) {
|
||
pairs++;
|
||
const optimal = dijkstra(g, o, t);
|
||
const result = routeCH(g, o, t);
|
||
if (optimal === Infinity) {
|
||
assert.equal(result, null, `pair ${o}->${t}: unreachable must resolve null`);
|
||
crossNulls++;
|
||
continue;
|
||
}
|
||
reachable++;
|
||
assert.ok(result, `pair ${o}->${t}: reachable must route`);
|
||
assert.ok(
|
||
Math.abs(result.seconds - optimal) < 1e-6,
|
||
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
|
||
);
|
||
// The unpacking law: every returned hop is a TRAVERSABLE original
|
||
// edge — no bit0 mirror and no unpacked shortcut survives.
|
||
for (let i = 1; i < result.nodes.length; i++) {
|
||
assert.ok(
|
||
findTraversableEdge(g, result.nodes[i - 1], result.nodes[i]) >= 0,
|
||
`pair ${o}->${t}: hop ${result.nodes[i - 1]}->${result.nodes[i]} is a traversable original edge`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
assert.equal(pairs, 81, "the full 9x9 sweep ran");
|
||
assert.ok(reachable > 40, `the ring is reachable (${reachable} pairs)`);
|
||
assert.ok(crossNulls >= 8, `the spur head is one-way-in (${crossNulls} pairs can never reach it)`);
|
||
});
|
||
|
||
test("Q3 original-edge seconds — reported seconds equal a fresh sum over the unpacked ORIGINAL-edge path", async () => {
|
||
const { routeCH } = await loadChQuery();
|
||
const g = await buildGridGraph();
|
||
// A long cross-grid pair (the route rides shortcuts) and the oneway
|
||
// detour pair: both must re-sum exactly over original edges.
|
||
for (const [o, t] of [[0, 24], [3, 2], [6, 19], [12, 12]]) {
|
||
const result = routeCH(g, o, t);
|
||
if (o === t) {
|
||
assert.deepEqual(result.nodes, [o], "the trivial pair is the single node");
|
||
assert.equal(result.seconds, 0, "the trivial pair costs 0");
|
||
continue;
|
||
}
|
||
assert.ok(result, `${o}->${t} routes`);
|
||
const fresh = resumSeconds(g, result.nodes);
|
||
assert.ok(fresh >= 0, `${o}->${t}: every returned hop is a traversable original edge`);
|
||
assert.ok(
|
||
Math.abs(result.seconds - fresh) < 1e-6,
|
||
`${o}->${t}: reported ${result.seconds} === original-edge re-sum ${fresh} (shortcut weights never enter seconds)`,
|
||
);
|
||
const optimal = dijkstra(g, o, t);
|
||
assert.ok(Math.abs(result.seconds - optimal) < 1e-6, `${o}->${t}: and it is the Dijkstra optimum`);
|
||
// distanceM is the haversine sum over the SAME unpacked path.
|
||
let meters = 0;
|
||
for (let i = 1; i < result.nodes.length; i++) {
|
||
const a = { lat: g.lat[result.nodes[i - 1]], lng: g.lng[result.nodes[i - 1]] };
|
||
const b = { lat: g.lat[result.nodes[i]], lng: g.lng[result.nodes[i]] };
|
||
meters += await haversineM(a, b);
|
||
}
|
||
assert.ok(
|
||
Math.abs(result.distanceM - meters) < 1e-6,
|
||
`${o}->${t}: distanceM is the haversine sum over the unpacked path`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("Q4 unreachable -> null — the island fold: cross pairs resolve null, internal pairs stay Dijkstra-equal", async () => {
|
||
const { routeCH } = await loadChQuery();
|
||
const g = await buildIslandGraph();
|
||
let pairs = 0;
|
||
let crossNulls = 0;
|
||
for (let o = 0; o < g.nodeCount; o++) {
|
||
for (let t = 0; t < g.nodeCount; t++) {
|
||
pairs++;
|
||
const optimal = dijkstra(g, o, t);
|
||
const result = routeCH(g, o, t);
|
||
const inA = o < 9 && t < 9;
|
||
const inB = o >= 9 && t >= 9;
|
||
if (!inA && !inB) {
|
||
assert.equal(optimal, Infinity, `cross pair ${o}->${t}: Dijkstra agrees unreachable`);
|
||
assert.equal(result, null, `cross pair ${o}->${t}: resolves null, never a fabricated path`);
|
||
crossNulls++;
|
||
continue;
|
||
}
|
||
assert.ok(result, `pair ${o}->${t} (${inA ? "A" : "B"}-internal) routes`);
|
||
assert.ok(
|
||
Math.abs(result.seconds - optimal) < 1e-6,
|
||
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
|
||
);
|
||
}
|
||
}
|
||
assert.equal(pairs, 169, "the full 13x13 sweep ran");
|
||
assert.equal(crossNulls, 72, "every cross-component pair is a null");
|
||
});
|
||
|
||
test("Q5 structural deletion — no ALT tokens in astar.ts; landmark.ts + landmark.test.mjs ABSENT; index exports none (D-04/D-05)", () => {
|
||
const src = readFileSync(path.join(here, "..", "src", "astar.ts"), "utf8");
|
||
const code = src
|
||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||
for (const token of ["landmarkTo", "landmarkFrom", "cleanColumnsFor", "LANDMARK_SENTINEL"]) {
|
||
assert.ok(!code.includes(token), `comment-stripped astar.ts must not contain ${token}`);
|
||
}
|
||
assert.equal(existsSync(path.join(here, "..", "src", "landmark.ts")), false, "landmark.ts is deleted (D-05)");
|
||
assert.equal(
|
||
existsSync(path.join(here, "landmark.test.mjs")),
|
||
false,
|
||
"landmark.test.mjs is deleted (D-05)",
|
||
);
|
||
const indexSrc = readFileSync(path.join(here, "..", "src", "index.ts"), "utf8");
|
||
const indexCode = indexSrc
|
||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||
assert.ok(!indexCode.includes("landmark"), "index.ts exports no landmark symbols");
|
||
});
|
||
|
||
/* ————— G1–G3 — the in-memory ARG3 codec pins ————— */
|
||
|
||
test("G1 ARG3 round-trip — contractGraph output round-trips rank + shortcut CSR exactly through zero-copy views", async () => {
|
||
const { encodeGraph, decodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const g0 = await buildGridGraph(); // builds through encode/decode already
|
||
assert.ok(g0, "the grid decoded (G1's round-trip ran inside the builder)");
|
||
// Explicit round-trip on a fresh contraction output.
|
||
const { contractGraph: contract2 } = await loadContraction();
|
||
const edges = [
|
||
{ from: 0, to: 1, seconds: 1, flags: 0 },
|
||
{ from: 1, to: 0, seconds: 1, flags: 0 },
|
||
{ from: 0, to: 2, seconds: 1, flags: 0 },
|
||
{ from: 2, to: 0, seconds: 1, flags: 0 },
|
||
];
|
||
const ch = contract2({ nodeCount: 3, edges });
|
||
const { encodeGraph: enc, decodeGraph: dec } = await loadGraph();
|
||
const buf = enc({
|
||
lat: [3.15, 3.1505, 3.151],
|
||
lng: [101.69, 101.6905, 101.691],
|
||
edges,
|
||
names: [],
|
||
maxSpeedKmh: 60,
|
||
ch,
|
||
}).slice(0);
|
||
const g = dec(buf);
|
||
assert.ok(g, "decode succeeds");
|
||
assert.equal(g.chShortcutCount, ch.shortcutTarget.length, "the header count matches the contraction");
|
||
assert.deepEqual([...g.chRank], [...ch.rank], "rank restores exactly");
|
||
assert.deepEqual([...g.shortcutStart], [...ch.shortcutStart], "shortcutStart restores exactly");
|
||
assert.deepEqual([...g.shortcutTarget], [...ch.shortcutTarget], "shortcutTarget restores exactly");
|
||
assert.deepEqual([...g.shortcutSeconds], [...ch.shortcutSeconds], "shortcutSeconds restores exactly");
|
||
assert.deepEqual([...g.shortcutVia], [...ch.shortcutVia], "shortcutVia restores exactly");
|
||
assert.equal(g.chRank.buffer, buf, "chRank is a zero-copy view");
|
||
assert.equal(g.shortcutVia.buffer, buf, "shortcutVia is a zero-copy view");
|
||
});
|
||
|
||
test("G2 rejection vectors — ARG1 bytes -> null; a version-2 tamper -> null (D-07 one decode path)", async () => {
|
||
const { decodeGraph } = await loadGraph();
|
||
// The frozen Plan 08-01 vector (the ARG1 rejection fixture).
|
||
const arg1 = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph-arg1.bin")));
|
||
assert.equal(decodeGraph(arg1), null, "ARG1 bytes -> null, never a throw or a fallback decode");
|
||
// A version-2 tamper of an in-memory ARG3 buffer refuses too.
|
||
const { encodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const edges = [
|
||
{ from: 0, to: 1, seconds: 1, flags: 0 },
|
||
{ from: 1, to: 0, seconds: 1, flags: 0 },
|
||
{ from: 0, to: 2, seconds: 1, flags: 0 },
|
||
{ from: 2, to: 0, seconds: 1, flags: 0 },
|
||
];
|
||
const buf = encodeGraph({
|
||
lat: [3.15, 3.1505, 3.151],
|
||
lng: [101.69, 101.6905, 101.691],
|
||
edges,
|
||
names: [],
|
||
maxSpeedKmh: 60,
|
||
ch: contractGraph({ nodeCount: 3, edges }),
|
||
});
|
||
const v2 = buf.slice(0);
|
||
new DataView(v2).setUint32(4, 2, true);
|
||
assert.equal(decodeGraph(v2), null, "version 2 -> null (ARG2 bytes decode to nothing — D-07)");
|
||
});
|
||
|
||
test("G3 fail-honest decode — truncated / hostile-count / chShortcutCount=0 buffers -> null BEFORE any allocation", async () => {
|
||
const { decodeGraph, GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
|
||
const { encodeGraph } = await loadGraph();
|
||
const { contractGraph } = await loadContraction();
|
||
const edges = [
|
||
{ from: 0, to: 1, seconds: 1, flags: 0 },
|
||
{ from: 1, to: 0, seconds: 1, flags: 0 },
|
||
{ from: 0, to: 2, seconds: 1, flags: 0 },
|
||
{ from: 2, to: 0, seconds: 1, flags: 0 },
|
||
];
|
||
const ch = contractGraph({ nodeCount: 3, edges });
|
||
const buf = encodeGraph({
|
||
lat: [3.15, 3.1505, 3.151],
|
||
lng: [101.69, 101.6905, 101.691],
|
||
edges,
|
||
names: [],
|
||
maxSpeedKmh: 60,
|
||
ch,
|
||
});
|
||
assert.equal(decodeGraph(buf.slice(0, buf.byteLength - 1)), null, "truncated -> null");
|
||
const hostile = new ArrayBuffer(72);
|
||
const dv = new DataView(hostile);
|
||
dv.setUint32(0, GRAPH_MAGIC, true);
|
||
dv.setUint32(4, GRAPH_VERSION, true);
|
||
dv.setUint32(8, 1_000_000, true); // a million nodes on a 72-byte buffer
|
||
assert.equal(decodeGraph(hostile), null, "hostile count -> null on arithmetic, no allocation");
|
||
const zeroed = buf.slice(0);
|
||
new DataView(zeroed).setUint32(60, 0, true);
|
||
assert.equal(decodeGraph(zeroed), null, "chShortcutCount = 0 -> null even when the size matches");
|
||
});
|