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
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
#!/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");
|
||||
});
|
||||
Reference in New Issue
Block a user