test(08-02): ARG2 codec G-suite — RED gate (G3/G5/G7/G8 re-pins + new G9-G11)
Re-pins: G3 consumes the frozen mini-graph-arg1.bin as the standing ARG1 rejection vector (D-01 one-path law) + v1/v3 tamper; G5's zero-copy flip moves to byte 72 (the ARG2 header end); G7 expects the regenerated ARG2 fixture (landmarkCount >= 1, quantum 0.1, L*N tables, anchor self-cells 0); G8 re-pins the exported constants to 0x41524732/2. New: G9 table round-trip (four landmark properties, sentinel-is-DATA, zero-copy u16 views); G10 encode-validation matrix (9 named-throw cases, T-08-04); G11 decode rejection (landmarkCount=0 refused even when the byte size exactly matches the L=0 layout — D-01/A5 — plus the monster landmarkCount=2e6 arithmetic rejection, T-08-01).
This commit is contained in:
+139
-21
@@ -1,23 +1,31 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/*
|
/*
|
||||||
* graph.test.mjs — the CSR road-graph binary codec gates (G1–G8).
|
* graph.test.mjs — the CSR road-graph binary codec gates (G1–G11).
|
||||||
* ------------------------------------------------------------------
|
* ------------------------------------------------------------------
|
||||||
* RED first (plan 06-01 task 1): every decode-bearing case fails while
|
* RED first (plan 06-01 task 1): every decode-bearing case fails while
|
||||||
* routing/src/graph.ts does not exist (lazy-import pattern — each case
|
* routing/src/graph.ts does not exist (lazy-import pattern — each case
|
||||||
* reports its own ERR_MODULE_NOT_FOUND, the fixture-source.test.mjs style).
|
* reports its own ERR_MODULE_NOT_FOUND, the fixture-source.test.mjs style).
|
||||||
|
* Re-pinned + extended by plan 08-02: the layout version bumped to ARG2 —
|
||||||
|
* a 72-byte header (landmarkCount u32 @60, landmarkQuantumS f32 @64,
|
||||||
|
* reserved u32 @68) plus a u16 landmark-table SUFFIX (to-table, from-table,
|
||||||
|
* u32 anchor ids). One decode path, one search path: anything that is not
|
||||||
|
* exactly an ARG2 graph decodes to null (D-01 fail-honest) — the committed
|
||||||
|
* mini-graph-arg1.bin (Plan 08-01) is the standing ARG1 rejection vector.
|
||||||
*
|
*
|
||||||
* The binary layout is PINNED by 06-01 (plans 04/07 depend on it):
|
* The binary layout is PINNED by 06-01 + 08-02 (plans 04/07 depend on it):
|
||||||
* little-endian CSR — header (magic/version/counts/bbox/maxSpeed) ++
|
* little-endian CSR — header (magic/version/counts/bbox/maxSpeed/landmark
|
||||||
* f64 lat++lng ++ u32 edgeStart[n+1] ++ u32 targets ++ u8 flags ++
|
* block) ++ f64 lat++lng ++ u32 edgeStart[n+1] ++ u32 targets ++ u8 flags
|
||||||
* f32 seconds ++ u32 nameIds ++ u32 nameOffsets[m+1] ++ u8 name pool.
|
* ++ f32 seconds ++ u32 nameIds ++ u32 nameOffsets[m+1] ++ u8 name pool ++
|
||||||
* The header is padded to 64 bytes so the f64 node arrays are 8-byte
|
* u16 toTable[L*N] ++ u16 fromTable[L*N] ++ u32 landmarkNodes[L].
|
||||||
|
* The header is padded to 72 bytes so the f64 node arrays are 8-byte
|
||||||
* aligned — zero-copy TypedArray views REQUIRE aligned offsets (an
|
* aligned — zero-copy TypedArray views REQUIRE aligned offsets (an
|
||||||
* unaligned Float64Array constructor throws RangeError in every engine).
|
* unaligned Float64Array constructor throws RangeError in every engine).
|
||||||
*
|
*
|
||||||
* G1 round-trip — encodeGraph → decodeGraph restores coords,
|
* G1 round-trip — encodeGraph → decodeGraph restores coords,
|
||||||
* targets, flags, weights, names, bbox exactly
|
* targets, flags, weights, names, bbox exactly
|
||||||
* G2 bad magic -> null — T-06-03 fail-honest (0xdeadbeef)
|
* G2 bad magic -> null — T-06-03 fail-honest (0xdeadbeef)
|
||||||
* G3 bad version -> null — version !== 1 refuses
|
* G3 bad version -> null — ARG1 fixture bytes -> null (D-01 one-path
|
||||||
|
* law); version 1 and 3 tamper -> null
|
||||||
* G4 truncated/hostile -> null — byte-length ≠ header-declared size
|
* G4 truncated/hostile -> null — byte-length ≠ header-declared size
|
||||||
* (incl. huge-count header on a tiny buffer,
|
* (incl. huge-count header on a tiny buffer,
|
||||||
* T-06-03b: size check BEFORE view allocation)
|
* T-06-03b: size check BEFORE view allocation)
|
||||||
@@ -25,9 +33,19 @@
|
|||||||
* flipping an input byte flips the decoded view
|
* flipping an input byte flips the decoded view
|
||||||
* G6 name pool — nameAt(0) === "Jalan Mini 1";
|
* G6 name pool — nameAt(0) === "Jalan Mini 1";
|
||||||
* nameAt(UNNAMED_NAME_ID) === undefined
|
* nameAt(UNNAMED_NAME_ID) === undefined
|
||||||
* G7 committed fixture — mini-graph.bin decodes: 25 nodes, 80 edges,
|
* G7 committed fixture — mini-graph.bin (ARG2) decodes: 25 nodes, 80
|
||||||
* 3 names, maxSpeed 60, exactly ONE oneway flag
|
* edges, 3 names, maxSpeed 60, exactly ONE
|
||||||
* G8 exported constants — GRAPH_MAGIC === 0x41524731, GRAPH_VERSION === 1
|
* oneway flag, landmarkCount >= 1, quantum 0.1
|
||||||
|
* G8 exported constants — GRAPH_MAGIC === 0x41524732, GRAPH_VERSION === 2
|
||||||
|
* G9 table round-trip — the four landmark properties restore exactly;
|
||||||
|
* the 0xFFFF sentinel is DATA; u16 tables are
|
||||||
|
* zero-copy views
|
||||||
|
* G10 encode validation — malformed landmark input throws NAMED
|
||||||
|
* encodeGraph: errors (no silent clamp, T-08-04)
|
||||||
|
* G11 decode rejection — landmarkCount = 0 -> null even when the size
|
||||||
|
* matches (D-01/A5: no landmark-less ARG2);
|
||||||
|
* monster landmarkCount on a tiny buffer ->
|
||||||
|
* null by arithmetic BEFORE allocation (T-08-01)
|
||||||
*
|
*
|
||||||
* Run: npm run routing:test (exit 0 = all green)
|
* Run: npm run routing:test (exit 0 = all green)
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +82,16 @@ function syntheticInput() {
|
|||||||
],
|
],
|
||||||
names: ["Jalan Mini 1", "Jalan Mini 2"],
|
names: ["Jalan Mini 1", "Jalan Mini 2"],
|
||||||
maxSpeedKmh: 60,
|
maxSpeedKmh: 60,
|
||||||
|
// ARG2 landmark block (plan 08-02): L=1 anchored at node 0. quantum 0.5
|
||||||
|
// is f32-exact so the round-trip asserts plain equality; the to-row
|
||||||
|
// carries a 0xFFFF sentinel ON PURPOSE — sentinels are DATA (row-major
|
||||||
|
// [l*N + v]: node 2 cannot reach the anchor in this made-up table).
|
||||||
|
landmarks: {
|
||||||
|
quantumS: 0.5,
|
||||||
|
nodes: [0],
|
||||||
|
to: Uint16Array.of(0, 25, 0xffff),
|
||||||
|
from: Uint16Array.of(0, 30, 60),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +131,20 @@ test("G2 bad magic -> null — a tampered header never half-decodes (T-06-03)",
|
|||||||
assert.equal(decodeGraph(tampered), null, "wrong magic -> null, never a throw");
|
assert.equal(decodeGraph(tampered), null, "wrong magic -> null, never a throw");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("G3 bad version -> null — version !== 1 refuses", async () => {
|
test("G3 bad version -> null — ARG1 fixture bytes refuse; version 1 and 3 tamper to null (D-01 one-path law)", async () => {
|
||||||
const { encodeGraph, decodeGraph } = await loadGraph();
|
const { encodeGraph, decodeGraph } = await loadGraph();
|
||||||
|
// The frozen Plan 08-01 vector: REAL ARG1 bytes decode to null — the
|
||||||
|
// Phase 6 router-pending state covers every pre-ARG2 binary (D-01).
|
||||||
|
// There is exactly one decode path; no v1 fallback branch exists.
|
||||||
|
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");
|
||||||
const buf = encodeGraph(syntheticInput());
|
const buf = encodeGraph(syntheticInput());
|
||||||
const future = buf.slice(0);
|
const v1 = buf.slice(0);
|
||||||
new DataView(future).setUint32(4, 2, true);
|
new DataView(v1).setUint32(4, 1, true);
|
||||||
assert.equal(decodeGraph(future), null, "wrong version -> null");
|
assert.equal(decodeGraph(v1), null, "version 1 -> null");
|
||||||
|
const v3 = buf.slice(0);
|
||||||
|
new DataView(v3).setUint32(4, 3, true);
|
||||||
|
assert.equal(decodeGraph(v3), null, "version 3 -> null");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("G4 truncated + hostile-size buffer -> null — exact byte-size check BEFORE any view allocation (T-06-03b)", async () => {
|
test("G4 truncated + hostile-size buffer -> null — exact byte-size check BEFORE any view allocation (T-06-03b)", async () => {
|
||||||
@@ -123,7 +159,7 @@ test("G4 truncated + hostile-size buffer -> null — exact byte-size check BEFOR
|
|||||||
assert.equal(decodeGraph(new ArrayBuffer(16)), null, "sub-header buffer -> null");
|
assert.equal(decodeGraph(new ArrayBuffer(16)), null, "sub-header buffer -> null");
|
||||||
// Hostile header: valid magic/version but a huge nodeCount on a tiny buffer —
|
// Hostile header: valid magic/version but a huge nodeCount on a tiny buffer —
|
||||||
// the size mismatch rejects it before any allocation (T-06-03b).
|
// the size mismatch rejects it before any allocation (T-06-03b).
|
||||||
const hostile = new ArrayBuffer(64);
|
const hostile = new ArrayBuffer(72);
|
||||||
const dv = new DataView(hostile);
|
const dv = new DataView(hostile);
|
||||||
const { GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
|
const { GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
|
||||||
dv.setUint32(0, GRAPH_MAGIC, true);
|
dv.setUint32(0, GRAPH_MAGIC, true);
|
||||||
@@ -139,10 +175,10 @@ test("G5 zero-copy views — the views' .buffer IS the input buffer; flipping an
|
|||||||
assert.ok(g, "decode succeeds");
|
assert.ok(g, "decode succeeds");
|
||||||
assert.equal(g.lat.buffer, buf, "lat is a VIEW over the input buffer (zero-copy)");
|
assert.equal(g.lat.buffer, buf, "lat is a VIEW over the input buffer (zero-copy)");
|
||||||
assert.equal(g.edgeSeconds.buffer, buf, "edgeSeconds is a VIEW over the input buffer");
|
assert.equal(g.edgeSeconds.buffer, buf, "edgeSeconds is a VIEW over the input buffer");
|
||||||
// The padded header ends at byte 64 — the first lat byte. Flip it.
|
// The padded header ends at byte 72 — the first lat byte. Flip it.
|
||||||
const dv = new DataView(buf);
|
const dv = new DataView(buf);
|
||||||
const before = g.lat[0];
|
const before = g.lat[0];
|
||||||
dv.setUint8(64, dv.getUint8(64) ^ 0xff);
|
dv.setUint8(72, dv.getUint8(72) ^ 0xff);
|
||||||
assert.notEqual(g.lat[0], before, "mutating the input buffer flips the decoded view");
|
assert.notEqual(g.lat[0], before, "mutating the input buffer flips the decoded view");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,7 +192,7 @@ test("G6 name pool — nameAt(0) resolves the first pool name; the unnamed senti
|
|||||||
assert.equal(g.nameAt(999), undefined, "out-of-range id -> undefined");
|
assert.equal(g.nameAt(999), undefined, "out-of-range id -> undefined");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("G7 committed mini-graph fixture — decodes to 25 nodes / 80 edges / 3 names, maxSpeed 60, exactly ONE oneway flag", async () => {
|
test("G7 committed ARG2 fixture — decodes to 25 nodes / 80 edges / 3 names / L>=1 landmarks, maxSpeed 60, ONE oneway", async () => {
|
||||||
const { decodeGraph } = await loadGraph();
|
const { decodeGraph } = await loadGraph();
|
||||||
const bin = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph.bin")));
|
const bin = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph.bin")));
|
||||||
assert.ok(bin.byteLength > 1000, `the committed fixture is substantial (${bin.byteLength} B)`);
|
assert.ok(bin.byteLength > 1000, `the committed fixture is substantial (${bin.byteLength} B)`);
|
||||||
@@ -181,11 +217,93 @@ test("G7 committed mini-graph fixture — decodes to 25 nodes / 80 edges / 3 nam
|
|||||||
if (g.edgeSeconds[e] > 5) sawSlow = true;
|
if (g.edgeSeconds[e] > 5) sawSlow = true;
|
||||||
}
|
}
|
||||||
assert.ok(sawFast && sawSlow, "both speed classes (60 and 30 km/h) exist in the weights");
|
assert.ok(sawFast && sawSlow, "both speed classes (60 and 30 km/h) exist in the weights");
|
||||||
|
// ARG2 landmark laws (plan 08-02): the fixture regenerates THROUGH the
|
||||||
|
// encode authority with a tiny real landmark stage (L=2).
|
||||||
|
assert.equal(g.landmarkCount, 2, "the build's tiny L");
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(g.landmarkQuantumS - 0.1) < 1e-6,
|
||||||
|
`quantum is the 0.1 s c-unit (f32 readback ${g.landmarkQuantumS})`,
|
||||||
|
);
|
||||||
|
assert.equal(g.landmarkTo.length, g.landmarkCount * g.nodeCount, "to-table is L*N row-major");
|
||||||
|
assert.equal(g.landmarkFrom.length, g.landmarkCount * g.nodeCount, "from-table is L*N row-major");
|
||||||
|
for (let l = 0; l < g.landmarkCount; l++) {
|
||||||
|
const a = g.landmarkNodes[l];
|
||||||
|
assert.ok(Number.isInteger(a) && a >= 0 && a < g.nodeCount, `anchor ${l} in range`);
|
||||||
|
assert.equal(g.landmarkTo[l * g.nodeCount + a], 0, `anchor ${l}'s own to-cell is 0`);
|
||||||
|
assert.equal(g.landmarkFrom[l * g.nodeCount + a], 0, `anchor ${l}'s own from-cell is 0`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("G8 exported constants — GRAPH_MAGIC === 0x41524731 ('ARG1'), GRAPH_VERSION === 1", async () => {
|
test("G8 exported constants — GRAPH_MAGIC === 0x41524732 ('ARG2'), GRAPH_VERSION === 2", async () => {
|
||||||
const mod = await loadGraph();
|
const mod = await loadGraph();
|
||||||
assert.equal(mod.GRAPH_MAGIC, 0x41524731);
|
assert.equal(mod.GRAPH_MAGIC, 0x41524732);
|
||||||
assert.equal(mod.GRAPH_VERSION, 1);
|
assert.equal(mod.GRAPH_VERSION, 2);
|
||||||
assert.equal(mod.UNNAMED_NAME_ID, 0xffffffff);
|
assert.equal(mod.UNNAMED_NAME_ID, 0xffffffff);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("G9 table round-trip — the four landmark properties restore exactly; sentinels are DATA; tables are zero-copy", async () => {
|
||||||
|
const { encodeGraph, decodeGraph } = await loadGraph();
|
||||||
|
const buf = encodeGraph(syntheticInput()).slice(0);
|
||||||
|
const g = decodeGraph(buf);
|
||||||
|
assert.ok(g, "decode succeeds");
|
||||||
|
assert.equal(g.landmarkCount, 1, "landmarkCount === L");
|
||||||
|
assert.equal(g.landmarkTo.length, 1 * 3, "to-table row-major L*N");
|
||||||
|
assert.equal(g.landmarkFrom.length, 1 * 3, "from-table row-major L*N");
|
||||||
|
assert.deepEqual([...g.landmarkNodes], [0], "landmarkNodes[i] matches the input anchors");
|
||||||
|
assert.equal(g.landmarkQuantumS, 0.5, "quantum restores exactly (0.5 is f32-exact)");
|
||||||
|
assert.deepEqual([...g.landmarkTo], [0, 25, 0xffff], "to-table bytes restore exactly — 0xFFFF is DATA");
|
||||||
|
assert.deepEqual([...g.landmarkFrom], [0, 30, 60], "from-table bytes restore exactly");
|
||||||
|
assert.equal(g.landmarkTo.buffer, buf, "the u16 to-table is a zero-copy view too");
|
||||||
|
assert.equal(g.landmarkFrom.buffer, buf, "the u16 from-table is a zero-copy view too");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("G10 encode validation — malformed landmark input throws NAMED encodeGraph: errors (T-08-04, no silent clamp)", async () => {
|
||||||
|
const { encodeGraph } = await loadGraph();
|
||||||
|
const base = syntheticInput();
|
||||||
|
const mangle = (over) => ({ ...base, landmarks: { ...base.landmarks, ...over } });
|
||||||
|
const cases = [
|
||||||
|
["zero anchors", mangle({ nodes: [] })],
|
||||||
|
["to-row length != L*N", mangle({ to: Uint16Array.of(0, 25) })],
|
||||||
|
["from-row length != L*N", mangle({ from: Uint16Array.of(0) })],
|
||||||
|
["anchor id >= nodeCount", mangle({ nodes: [7] })],
|
||||||
|
["anchor id negative", mangle({ nodes: [-1] })],
|
||||||
|
["quantum 0", mangle({ quantumS: 0 })],
|
||||||
|
["quantum negative", mangle({ quantumS: -1 })],
|
||||||
|
["quantum NaN", mangle({ quantumS: Number.NaN })],
|
||||||
|
["landmarks block missing", { ...base, landmarks: undefined }],
|
||||||
|
];
|
||||||
|
for (const [label, input] of cases) {
|
||||||
|
assert.throws(() => encodeGraph(input), /^Error: encodeGraph: /, `${label} -> named encodeGraph: error`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("G11 decode rejection — landmarkCount 0 -> null even when the size matches; monster count -> null by arithmetic (T-08-01)", async () => {
|
||||||
|
const { encodeGraph, decodeGraph, GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
|
||||||
|
const buf = encodeGraph(syntheticInput());
|
||||||
|
const zeroed = buf.slice(0);
|
||||||
|
new DataView(zeroed).setUint32(60, 0, true);
|
||||||
|
assert.equal(decodeGraph(zeroed), null, "landmarkCount = 0 -> null (D-01/A5: no landmark-less ARG2)");
|
||||||
|
// A CONSISTENT landmark-less buffer (byte size exactly matches the L=0
|
||||||
|
// layout: header + sections + align4(namePoolEnd), no suffix) still
|
||||||
|
// refuses — the law is the count itself, not a size mismatch. Quantum
|
||||||
|
// is valid here so the L=0 law is isolated.
|
||||||
|
const n = 1;
|
||||||
|
const lay = 72 + 8 * n + 8 * n + 4 * (n + 1) + 4 * 1; // = 100: header + lat + lng + edgeStart[2] + nameOffset[1]; no suffix
|
||||||
|
const bare = new ArrayBuffer(lay); // byteLength === the exact L=0 layout total
|
||||||
|
const dv = new DataView(bare);
|
||||||
|
dv.setUint32(0, GRAPH_MAGIC, true);
|
||||||
|
dv.setUint32(4, GRAPH_VERSION, true);
|
||||||
|
dv.setUint32(8, n, true);
|
||||||
|
dv.setUint32(12, 0, true); // edgeCount
|
||||||
|
dv.setUint32(16, 0, true); // nameCount
|
||||||
|
dv.setUint32(20, 0, true); // namePoolLen
|
||||||
|
dv.setFloat32(64, 0.5, true); // a valid quantum…
|
||||||
|
dv.setUint32(60, 0, true); // …but landmarkCount = 0
|
||||||
|
assert.equal(decodeGraph(bare), null, "an exactly-sized landmark-less ARG2 -> null");
|
||||||
|
// Monster header: L = 2,000,000 on a tiny buffer — the exact-size
|
||||||
|
// arithmetic rejects BEFORE any Uint16Array allocation (a null return,
|
||||||
|
// never a RangeError).
|
||||||
|
const monster = buf.slice(0);
|
||||||
|
new DataView(monster).setUint32(60, 2_000_000, true);
|
||||||
|
assert.equal(decodeGraph(monster), null, "monster landmarkCount on a tiny buffer -> null, no allocation");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user