diff --git a/routing/test/polyline.test.mjs b/routing/test/polyline.test.mjs new file mode 100644 index 0000000..ab54db9 --- /dev/null +++ b/routing/test/polyline.test.mjs @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/* + * polyline.test.mjs — polyline6 codec vectors (P1–P8). + * ----------------------------------------------------- + * RED first (plan 05-01 task 2): every case fails while + * routing/src/polyline.ts does not exist — the lazy-import pattern + * keeps each case reporting its own failure. + * + * Vectors are computed ground truth (the plan's block, + * derived with the reference algorithm in the planning session), NOT + * guesses: + * P1 zero vector — decode("??") -> [{lat:0,lng:0}]; encode inverts + * P2 KL point vector — encode([{3.1578,101.7117}]) === "oqv_Egt~~`E" + * (note: the string contains a BACKTICK) + * P3 delta accumulation — the 3-point KL vector (deltas 100/100/100/200) + * P4 negative zigzag — the V4 vector (the ~ inversion branch) + * P5 external OSRM vector — live demo-server geometry (KLCC -> KL Sentral, + * probed 2026-10-24; RESEARCH Finding 1): first + * point decodes to {3.157846, 101.711716} (OSRM + * snapped the origin onto the road graph) and + * encode(decode(s)) === s byte-identically + * P6 empty input — decode("") -> []; encode([]) === "" + * P7 malformed refusal — decode("A") / decode("g") THROW (truncated + * varint — never a partial/NaN point, T-05-02) + * P8 round-trip law — encode(decode(s)) === s for EVERY vector here + * + * Run: npm run routing:test (exit 0 = all green) + */ +import { strict as assert } from "node:assert"; +import { test } from "./harness.mjs"; + +// Lazy codec load — missing module => per-case ERR_MODULE_NOT_FOUND (the RED state). +let codecPromise = null; +function loadCodec() { + if (!codecPromise) { + codecPromise = import("../src/polyline.ts"); + } + return codecPromise; +} + +/* ————— the pinned vectors ————— */ + +const V_ZERO = "??"; +const V_KL_POINT = "oqv_Egt~~`E"; +const V_KL_THREE = "oqv_Egt~~`EgEgEgEoK"; +const V_NEGATIVE = "~tpzA_pihC_pihCnt}oC"; +const V_EXTERNAL = + "ktv_Egu~~`EsAlFaDnGeF`EmGvEmWbSySjOiTfUiOb`@{n@xZ}FhBsGx@cJ?"; + +const KL_POINT = { lat: 3.1578, lng: 101.7117 }; +const KL_THREE_POINTS = [ + { lat: 3.1578, lng: 101.7117 }, + { lat: 3.1579, lng: 101.7118 }, + { lat: 3.158, lng: 101.712 }, +]; +const NEGATIVE_POINTS = [ + { lat: -1.5, lng: 2.25 }, + { lat: 0.75, lng: -0.125 }, +]; + +/* ————— P1–P8 ————— */ + +test("P1 zero vector — decode('??') -> [{lat:0,lng:0}]; encode inverts exactly", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + assert.deepEqual(decodePolyline6(V_ZERO), [{ lat: 0, lng: 0 }]); + assert.equal(encodePolyline6([{ lat: 0, lng: 0 }]), V_ZERO); +}); + +test("P2 KL point vector — encode([{3.1578,101.7117}]) === 'oqv_Egt~~`E' and decode inverts", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + assert.equal(encodePolyline6([KL_POINT]), V_KL_POINT); + assert.deepEqual(decodePolyline6(V_KL_POINT), [KL_POINT]); +}); + +test("P3 delta accumulation — the 3-point KL vector encodes/decodes exactly", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + assert.equal(encodePolyline6(KL_THREE_POINTS), V_KL_THREE); + assert.deepEqual(decodePolyline6(V_KL_THREE), KL_THREE_POINTS); +}); + +test("P4 negative zigzag — the V4 vector exercises the ~ inversion branch", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + assert.equal(encodePolyline6(NEGATIVE_POINTS), V_NEGATIVE); + assert.deepEqual(decodePolyline6(V_NEGATIVE), NEGATIVE_POINTS); +}); + +test("P5 external OSRM vector — live demo geometry decodes and re-encodes byte-identically", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + const first = decodePolyline6(V_EXTERNAL)[0]; + assert.deepEqual( + first, + { lat: 3.157846, lng: 101.711716 }, + "OSRM snapped the origin onto the road graph — the small offset from 3.1578/101.7117 is expected", + ); + assert.equal( + encodePolyline6(decodePolyline6(V_EXTERNAL)), + V_EXTERNAL, + "the external ground truth must round-trip byte-identically (RESEARCH Finding 1)", + ); +}); + +test("P6 empty input — decode('') -> []; encode([]) === ''", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + assert.deepEqual(decodePolyline6(""), []); + assert.equal(encodePolyline6([]), ""); +}); + +test("P7 malformed refusal — truncated varints THROW, never return partial/NaN points (T-05-02)", async () => { + const { decodePolyline6 } = await loadCodec(); + assert.throws(() => decodePolyline6("A"), "a lone 'A' is a truncated coordinate — must throw"); + assert.throws(() => decodePolyline6("g"), "a dangling continuation chunk — must throw"); + for (const bad of ["A", "g", "oqv_E"]) { + let points = null; + try { + points = decodePolyline6(bad); + } catch { + continue; // throwing is the contract + } + assert.fail(`decode(${JSON.stringify(bad)}) returned ${JSON.stringify(points)} instead of throwing`); + } +}); + +test("P8 round-trip law — encode(decode(s)) === s for EVERY vector string in this suite", async () => { + const { decodePolyline6, encodePolyline6 } = await loadCodec(); + const all = [V_ZERO, V_KL_POINT, V_KL_THREE, V_NEGATIVE, V_EXTERNAL]; + for (const s of all) { + assert.equal( + encodePolyline6(decodePolyline6(s)), + s, + `round-trip law violated for ${s.slice(0, 12)}… — the corpus gate (plan 05-02) leans on this`, + ); + } +});