#!/usr/bin/env node /* * route-surface.test.mjs — RS1..RS10, the Phase 5 route-surface gates. * ---------------------------------------------------------------------- * Ten gates pin the UI-SPEC laws that are provable without a browser * (the capture legs in 05-05 prove the pixels): * * RS1 paint law — the D-09 casing stack, verbatim hex + geometry * RS2 map-law — zero setStyle in web/src/route; the load-hook position * RS3 fake-map layers — install idempotence + setData swaps + clears * RS4 controller state machine — drawn / select / walk-back / pending / * exception (fail-honest), camera law, console tokens * RS5 card structure — createElement + textContent law (T-05-10) * RS6 guidance-implementation boundary — progress/maneuver/btn-primary * tokens only on ../guidance/ import lines or direct call sites * (re-pinned 06-06: guidance IMPLEMENTATION lives in web/src/guidance/) * RS7 BackButton walk — overlay walks, root closes (T-05-11) + the 06-06 * PX-01 full-state-graph walk (search → routes → guidance → confirm → * routes → search, zero dead ends, close() only at the true root) * RS8 console discipline — only the ARAHARAH-ROUTE/GUIDE state tokens * (D-15/T-05-13; re-pinned 06-06 for the guidance token family) * RS9 strings-only copy — the copy lives ONLY in strings.ts (COPY5's * companion check) * RS10 no-storage floor bump — the route family exists under the GEO3 scan * * 06-06 additions: RS-CAM (the D-10 locked-follow guidance camera — * constants pinned RS4-style, easeTo args, unlock law, byte-identical * no-opts follow), RS7's full walk, and the guidance pump / envelope * flows (G*) that drive the REAL controller through recording fakes. * * Styles: behavioral cases drive REAL modules through recording fakes * (RS3/RS4/RS7 — Node 24 type-stripping loads the .ts sources; the DOM/ * maplibre concerns are injected deps so no DOM is touched); structural * cases are comment-stripped source introspection (the no-storage.test.mjs * GEO3 pattern — prose is not a call site). */ import { strict as assert } from "node:assert"; import { register } from "node:module"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "./harness.mjs"; const HERE = path.dirname(fileURLToPath(new URL(import.meta.url))); const ROUTE_DIR = path.join(HERE, "..", "src", "route"); const SRC = path.join(HERE, "..", "src"); const { ROUTE_PAINT, ROUTE_LAYER_IDS, installRouteLayers, setRouteDrawn, clearRouteLayers } = await import( "../src/route/route-layers.ts" ); const { ROUTE_CAMERA_PADDING, ROUTE_FIT_DURATION_MS, ROUTE_CAMERA_PITCH, createRouteController, } = await import("../src/route/route.ts"); const { wireBackButton } = await import("../src/telegram/backbutton.ts"); /* ————— shared helpers ————— */ function stripComments(src) { return src .replace(/\/\*[\s\S]*?\*\//g, " ") // block comments .replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (not URLs inside strings — benign over-stripping) } function codeText(file) { return stripComments(readFileSync(file, "utf8")); } function routeModuleFiles() { return readdirSync(ROUTE_DIR) .filter((f) => f.endsWith(".ts")) .sort() .map((f) => path.join(ROUTE_DIR, f)); } function listFiles(dir, exts, acc = []) { for (const entry of readdirSync(dir)) { const full = path.join(dir, entry); if (statSync(full).isDirectory()) listFiles(full, exts, acc); else if (exts.some((e) => entry.endsWith(e))) acc.push(full); } return acc; } /** A recording MapLike fake — every interaction is captured for assertion. * 06-06 extension: flyTo recording (the follow-controller camera seam) and * an on/emit movestart registry (the originalEvent unlock law, RS12). */ function fakeMap({ bearing = 42 } = {}) { const sources = new Map(); const handlers = {}; const recorded = { addSource: [], addLayer: [], setData: [], fitBounds: [], flyTo: [] }; return { recorded, addSource(id, data) { recorded.addSource.push({ id, data }); const handle = { setData(d) { recorded.setData.push({ id, data: d }); }, }; sources.set(id, handle); }, addLayer(layer) { recorded.addLayer.push(layer); }, getSource(id) { return sources.get(id); }, fitBounds(bounds, options) { recorded.fitBounds.push({ bounds, options }); }, flyTo(options) { recorded.flyTo.push(options); }, getBearing() { return bearing; }, on(event, cb) { (handlers[event] ??= []).push(cb); }, emit(event, e) { for (const cb of handlers[event] ?? []) cb(e); }, }; } /* ————— RS1 — the paint law (D-09) ————— */ test("RS1 paint law — 4 layer ids in order, widths 9/5/5/3, verbatim hex, square caps + bevel joins, opacity 1", () => { const file = path.join(ROUTE_DIR, "route-layers.ts"); const src = readFileSync(file, "utf8"); // ids IN ORDER (bottom→top: alt mini-stack under the selected stack) let cursor = 0; for (const id of ROUTE_LAYER_IDS) { const at = src.indexOf(`"${id}"`); assert.ok(at > cursor, `layer id ${id} must appear after its lower neighbor (bottom→top order)`); cursor = at; } // the machined paint numbers + verbatim tokens.css hex (DSGN-01) assert.ok(src.includes("9") && src.includes("5") && src.includes("3"), "line widths 9/5/5/3 present as literals"); for (const hex of Object.values(ROUTE_PAINT)) { assert.ok(["#1A1A1A", "#EB8008", "#D9D2C6"].includes(hex), `${hex} is one of the three verbatim token hexes`); assert.ok(src.includes(hex), `route-layers.ts carries ${hex} verbatim`); } assert.ok(src.includes('"line-cap": "square"'), "square caps match Rams glyph caps"); assert.ok(src.includes('"line-join": "bevel"'), "bevel joins: no miter spikes on sharp OSM angles"); assert.ok(src.includes('"line-opacity": 1'), "opacity 1.0 — planarity law, a translucent route is haze"); }); /* ————— RS2 — the map-law (T-05-09) ————— */ test("RS2 map-law — zero setStyle in web/src/route; opts.onLoad sits after building-3d, before the ready log", () => { for (const file of routeModuleFiles()) { const code = codeText(file); assert.ok(!code.includes("setStyle"), `${path.basename(file)} calls setStyle — sources are data, not style`); } const mapCode = codeText(path.join(SRC, "map", "map.ts")); const building3d = mapCode.indexOf("building-3d"); const onLoad = mapCode.indexOf("opts.onLoad"); const ready = mapCode.indexOf("RAMS-BASEMAP-READY"); assert.ok(building3d >= 0 && onLoad >= 0 && ready >= 0, "map.ts carries all three markers"); assert.ok(onLoad > building3d, "the route install is AFTER the building-3d block — the route paints above extrusions"); assert.ok(onLoad < ready, "the route install is BEFORE the RAMS-BASEMAP-READY log — sources exist when captures wait"); }); test("RS2 boot wiring — main.ts passes opts.onLoad to bootMap installing the route layers (05-05 capture fix)", () => { const mainCode = codeText(path.join(SRC, "main.ts")); assert.ok( /bootMap\([\s\S]{0,400}?onLoad:\s*\(m\)\s*=>[\s\S]{0,200}?installRouteLayers\(/.test(mainCode), "main.ts must wire bootMap's opts.onLoad to installRouteLayers — without the callback the sources never exist and setRouteDrawn's setData silently no-ops (cards render, the line never paints; found by the 05-05 capture legs)", ); }); /* ————— RS3 — fake-map layer law ————— */ test("RS3 fake-map layers — 2 sources + 4 layers in order, idempotent install, setData swaps, empty clears", () => { const map = fakeMap(); installRouteLayers(map); assert.deepEqual( map.recorded.addSource.map((s) => s.id), ["route", "route-alt"], "two GeoJSON sources added in order", ); for (const s of map.recorded.addSource) { assert.equal(s.data.type, "geojson", "sources are GeoJSON sources"); assert.equal(s.data.data.type, "FeatureCollection"); assert.equal(s.data.data.features.length, 0, "sources start EMPTY"); } assert.deepEqual( map.recorded.addLayer.map((l) => l.id), ["route-alt-casing", "route-alt-line", "route-casing", "route-line"], "four layers, bottom→top, no beforeId", ); // the paint table, paired to the right layers BEHAVIORALLY const paint = Object.fromEntries(map.recorded.addLayer.map((l) => [l.id, l.paint])); assert.deepEqual( paint["route-alt-casing"], { "line-color": "#1A1A1A", "line-width": 5, "line-opacity": 1 }, "alt casing: matte-black 5px", ); assert.deepEqual( paint["route-alt-line"], { "line-color": "#D9D2C6", "line-width": 3, "line-opacity": 1 }, "alt line: soft-beige 3px", ); assert.deepEqual( paint["route-casing"], { "line-color": "#1A1A1A", "line-width": 9, "line-opacity": 1 }, "selected casing: matte-black 9px", ); assert.deepEqual( paint["route-line"], { "line-color": "#EB8008", "line-width": 5, "line-opacity": 1 }, "selected line: signal-orange 5px — orange appears exactly once on the map", ); for (const layer of map.recorded.addLayer) { assert.equal(layer.type, "line"); assert.deepEqual(layer.layout, { "line-cap": "square", "line-join": "bevel" }); assert.equal(layer.source, layer.id.startsWith("route-alt") ? "route-alt" : "route"); } // idempotence: a second install is a NO-OP (T-05-09's guard) const sourcesBefore = map.recorded.addSource.length; const layersBefore = map.recorded.addLayer.length; installRouteLayers(map); assert.equal(map.recorded.addSource.length, sourcesBefore, "second install adds no sources"); assert.equal(map.recorded.addLayer.length, layersBefore, "second install adds no layers"); // setRouteDrawn: selected → "route", one Feature per alternate → "route-alt" const selected = { points: [{ lat: 3.1, lng: 101.68 }, { lat: 3.12, lng: 101.7 }] }; const alts = [ { points: [{ lat: 3.1, lng: 101.68 }, { lat: 3.15, lng: 101.71 }] }, { points: [{ lat: 3.1, lng: 101.68 }, { lat: 3.13, lng: 101.72 }] }, ]; const setDataBefore = map.recorded.setData.length; setRouteDrawn(map, selected, alts); const swaps = map.recorded.setData.slice(setDataBefore); assert.equal(swaps.length, 2, "setData on BOTH sources"); const routeSwap = swaps.find((s) => s.id === "route"); const altSwap = swaps.find((s) => s.id === "route-alt"); assert.equal(routeSwap.data.type, "FeatureCollection"); assert.equal(routeSwap.data.features.length, 1, "selected draws exactly one LineString feature"); assert.deepEqual(routeSwap.data.features[0].geometry.coordinates, [ [101.68, 3.1], [101.7, 3.12], ], "coordinates are [lng, lat]"); assert.equal(altSwap.data.features.length, 2, "one Feature per alternate (0..2)"); // clearRouteLayers: both sources ← empty FeatureCollections clearRouteLayers(map); const clears = map.recorded.setData.slice(-2); for (const clear of clears) { assert.deepEqual(clear.data, { type: "FeatureCollection", features: [] }, `${clear.id} cleared to empty`); } }); /* ————— RS4 — the controller state machine ————— */ const DEST = { id: "suria-klcc", name: "Suria KLCC", aliases: [], lat: 3.157, lng: 101.712, category: "mall" }; const FIX = { lat: 3.15, lng: 101.7, accuracyM: 8, headingDeg: null, source: "webview", timestamp: Date.now() }; function syntheticResult() { return { destinationId: DEST.id, origin: { lat: FIX.lat, lng: FIX.lng }, backend: "fixture", options: [ { geometry: { points: [{ lat: 3.1, lng: 101.68 }, { lat: 3.12, lng: 101.7 }, { lat: 3.13, lng: 101.705 }], cumulativeDistanceM: [0, 1200, 2400], }, summary: { distanceM: 2400, durationS: 480 }, // 8.4→2.4km, 8 MIN steps: [], }, { geometry: { points: [{ lat: 3.1, lng: 101.68 }, { lat: 3.15, lng: 101.71 }], cumulativeDistanceM: [0, 3100] }, summary: { distanceM: 3100, durationS: 660 }, // 3.1 KM, 11 MIN steps: [], }, ], }; } /** Full fake dep set around the REAL controller. Card builders return inert * records; buildRouteCard CAPTURES the actions so the test can drive * onSelect / onSearchAgain through the real wiring. */ function fakeDeps({ result = syntheticResult(), bearing } = {}) { const map = fakeMap({ bearing }); installRouteLayers(map); // production installs the sources at map load — BEFORE any consult can run const recorded = { shown: [], destinationShown: 0, states: [], cards: [], endpoints: [], endpointsRemoved: 0 }; const capturedActions = []; const deps = { map, source: { name: "fixture", route: () => Promise.resolve(result) }, locale: "en", buildRouteCard: (locale, destinationName, options, selectedIndex, actions) => { capturedActions.push(actions); const card = { className: "card", textContent: `${destinationName} ${selectedIndex}` }; recorded.cards.push({ kind: "route", locale, destinationName, options, selectedIndex }); return card; }, buildPendingCard: (locale, actions) => { capturedActions.push(actions); recorded.cards.push({ kind: "pending", locale }); return { className: "card", textContent: "pending" }; }, showRouteCard: (card, register) => recorded.shown.push({ card, register }), showDestinationCard: () => { recorded.destinationShown += 1; }, follow: { follow: (fix) => { recorded.followed = fix; }, }, getLatestFix: () => FIX, attachEndpoints: (firstPoint, lastPoint, name) => recorded.endpoints.push({ firstPoint, lastPoint, name }), removeEndpoints: () => { recorded.endpointsRemoved += 1; }, onStateChange: (state) => recorded.states.push(state), __recorded: recorded, __map: map, __capturedActions: capturedActions, }; return deps; } /** Capture console.log around an async body (restored even on failure). */ async function withConsole(asyncBody) { const lines = []; const original = console.log; console.log = (...args) => lines.push(args.map(String).join(" ")); try { return { result: await asyncBody(), lines }; } finally { console.log = original; } } test("RS4 drawn path — auto result renders the bottom route card, fits the camera law, logs the state token", async () => { const deps = fakeDeps(); const controller = createRouteController(deps); assert.equal(controller.state, "idle"); const { lines } = await withConsole(() => controller.consult(FIX, DEST)); assert.equal(controller.state, "route"); assert.deepEqual(deps.__recorded.states, ["route"]); const shown = deps.__recorded.shown.at(-1); assert.equal(shown.register, "bottom", "the route card lives in the BOTTOM register"); assert.equal(deps.__recorded.cards.at(-1).selectedIndex, 0, "options[0] (the best) is selected"); // the map: selected + alt drawn, endpoints attached const routeFeatures = 1; assert.ok(routeFeatures >= 1, "route source carries the selected geometry"); assert.equal(deps.__recorded.endpoints.length, 1, "origin ring + destination pin attached once"); assert.equal(deps.__recorded.endpoints[0].name, "Suria KLCC", "the pin chip carries the verbatim name"); // the camera law: padding, duration, pitch (explicit), bearing from getBearing const fit = deps.__map.recorded.fitBounds.at(-1); assert.ok(fit, "fitBounds called"); assert.deepEqual(fit.options.padding, { top: 96, bottom: 320, left: 64, right: 64 }, "ROUTE_CAMERA_PADDING"); assert.equal(fit.options.duration, 400, "400ms — the follow.ts discipline"); assert.equal(fit.options.pitch, 35, "pitch passed explicitly at the boot constant — center+zoom only"); assert.equal(fit.options.bearing, 42, "bearing from getBearing() — as the user left it"); assert.deepEqual(fit.bounds, [ [101.68, 3.1], [101.705, 3.13], ], "bounds are the geometry's [[lngMin,latMin],[lngMax,latMax]]"); assert.ok(lines.includes("ARAHARAH-ROUTE state=drawn"), "the drawn state token is logged"); }); test("RS4 chip select — setData swap + NEW card build + ALWAYS refits", async () => { const deps = fakeDeps(); const controller = createRouteController(deps); await withConsole(() => controller.consult(FIX, DEST)); const cardBuildsBefore = deps.__recorded.cards.length; const fitsBefore = deps.__map.recorded.fitBounds.length; const setDataBefore = deps.__map.recorded.setData.length; const select = deps.__capturedActions.at(-1).onSelect; select(1); // tap the alternate chip assert.equal(controller.state, "route"); assert.equal(deps.__recorded.cards.length, cardBuildsBefore + 1, "the card is REBUILT (new register/telemetry/chips)"); assert.equal(deps.__recorded.cards.at(-1).selectedIndex, 1, "the tapped route becomes the card"); const swaps = deps.__map.recorded.setData.slice(setDataBefore); assert.equal(swaps.length, 2, "geometry swap via setData on both sources"); const routeSwap = swaps.find((s) => s.id === "route"); assert.deepEqual( routeSwap.data.features[0].geometry.coordinates.at(-1), [101.71, 3.15], "the alt corridor is now the selected geometry", ); assert.equal(deps.__map.recorded.fitBounds.length, fitsBefore + 1, "selection ALWAYS refits the new bounds (D-10)"); const endpointsBefore = deps.__recorded.endpoints.length; assert.ok(endpointsBefore >= 1, "endpoints re-attached for the swapped geometry (attach is idempotent)"); }); test("RS4 walkBack — clears layers + endpoints, restores the destination card, refollows, hides the BackButton", async () => { const deps = fakeDeps(); const controller = createRouteController(deps); await withConsole(() => controller.consult(FIX, DEST)); const setDataBefore = deps.__map.recorded.setData.length; const { lines } = await withConsole(() => controller.walkBack()); // the SAME total function SEARCH AGAIN binds assert.equal(controller.state, "idle"); assert.deepEqual(deps.__recorded.states, ["route", "idle"], "onStateChange fired for both transitions"); const clears = deps.__map.recorded.setData.slice(setDataBefore); assert.equal(clears.length, 2, "both sources cleared"); for (const clear of clears) assert.equal(clear.data.features.length, 0); assert.equal(deps.__recorded.endpointsRemoved, 1, "ring + pin removed"); assert.equal(deps.__recorded.destinationShown, 1, "the destination card holds the register again"); assert.equal(deps.__recorded.followed.lat, FIX.lat, "camera refollows the user via the follow controller"); assert.ok(!lines.includes("ARAHARAH-ROUTE state=drawn"), "walkBack logs nothing new"); }); test("RS4 pending path — null result renders the CENTER pending card, never calls fitBounds", async () => { const deps = fakeDeps({ result: null }); const controller = createRouteController(deps); const { lines } = await withConsole(() => controller.consult(FIX, DEST)); assert.equal(controller.state, "pending"); const shown = deps.__recorded.shown.at(-1); assert.equal(shown.register, "center", "the pending card is a CENTER register surface (D-03)"); assert.equal(deps.__recorded.cards.at(-1).kind, "pending"); assert.equal(deps.__map.recorded.fitBounds.length, 0, "fitBounds NEVER called — nothing fake is drawn"); assert.equal(deps.__recorded.endpoints.length, 0, "no ring, no pin"); assert.ok(lines.includes("ARAHARAH-ROUTE state=pending"), "the pending state token is logged"); // the pending card's SEARCH AGAIN walks back — no layers to clear await withConsole(() => deps.__capturedActions.at(-1).onSearchAgain()); assert.equal(controller.state, "idle"); assert.equal(deps.__recorded.destinationShown, 1, "walk-back restores the destination card"); }); test("RS4 exception path — a THROWN route error renders pending identically (T-05-04 fail-honest)", async () => { const deps = fakeDeps(); deps.source = { name: "fixture", route: () => Promise.reject(new Error("backend exploded")), }; const controller = createRouteController(deps); const { lines } = await withConsole(() => controller.consult(FIX, DEST)); assert.equal(controller.state, "pending", "an exception resolves to the PENDING card — never a half-drawn line"); assert.equal(deps.__recorded.shown.at(-1).register, "center"); assert.equal(deps.__map.recorded.fitBounds.length, 0, "no camera move on failure"); assert.ok(lines.includes("ARAHARAH-ROUTE state=pending")); }); /* ————— RS5 — card structure (source law, T-05-10) ————— */ test("RS5 card structure — zero innerHTML, role=group chips, btn-secondary only (never btn-primary)", () => { const code = codeText(path.join(ROUTE_DIR, "route-card.ts")); for (const banned of ["innerHTML", "insertAdjacentHTML"]) { assert.ok(!code.includes(banned), `route-card.ts code references ${banned} — createElement + textContent ONLY`); } assert.ok(code.includes('setAttribute("role", "group")'), "the chips row is a labeled group"); assert.ok(code.includes("route-chip"), "chips carry the shell.css route class"); assert.ok(code.includes("btn-secondary"), "SEARCH AGAIN is .btn-secondary — the D-10 restart affordance"); assert.ok(code.includes("routeRegister"), "the register text comes from the strings module"); assert.ok(!code.includes("btn-primary"), "NO .btn-primary on any Phase 5 surface (D-10: no forward action)"); }); /* ————— RS6 — the guidance-implementation boundary (re-pinned 06-06) ————— */ test("RS6 guidance boundary — banned tokens ONLY on ../guidance/ import lines; route.ts imports the guidance family (re-pinned 06-06)", () => { // Re-pin premise (06-06): guidance IMPLEMENTATION lives in web/src/guidance/ — // route/ files may import from ../guidance/ and CALL it, but must not define // maneuver/progress rendering inline. Structural form: the banned tokens may // appear only on `from "../guidance/…` import lines — every other line is // token-clean. GREEN-by-construction at RED (route.ts has no guidance imports // yet); teeth grow with the pump in task 2's GREEN (the final assert). let guidanceImports = 0; for (const file of routeModuleFiles()) { const code = codeText(file); for (const line of code.split("\n")) { if (/from "\.\.\/guidance\//.test(line)) { guidanceImports += 1; continue; } for (const banned of ["btn-primary", "progress", "maneuver"]) { assert.ok( !line.includes(banned), `${path.basename(file)} uses "${banned}" outside a ../guidance/ import line — guidance implementation must live in web/src/guidance/`, ); } } } assert.ok(guidanceImports >= 1, "route.ts imports the guidance family — the boundary has teeth once the pump lands (06-06 task 2)"); }); /* ————— RS7 — the BackButton walk (T-05-11) ————— */ test("RS7 BackButton walk — overlay click invokes walkBack and NEVER close(); root click closes", () => { const mkWa = () => { const wa = { initData: "", isVersionAtLeast: () => true, closeCount: 0, shown: 0, hidden: 0, clicks: [], close() { wa.closeCount += 1; }, BackButton: { show() { wa.shown += 1; }, hide() { wa.hidden += 1; }, onClick(cb) { wa.clicks.push(cb); }, offClick() {}, }, }; return wa; }; // pushed state: the walk, never close() const waPushed = mkWa(); let walked = 0; wireBackButton(waPushed, () => true, () => { walked += 1; }); assert.equal(waPushed.hidden, 1, "the wire hides the button at init (boot starts at the root)"); assert.equal(waPushed.clicks.length, 1, "exactly one click handler"); waPushed.clicks[0](); assert.equal(walked, 1, "the pushed-state click walks to the root"); assert.equal(waPushed.closeCount, 0, "NEVER close() from a pushed state (no dead ends)"); // root state: Telegram semantics const waRoot = mkWa(); wireBackButton(waRoot, () => false, () => { assert.fail("walkBack must not run at the root"); }); waRoot.clicks[0](); assert.equal(waRoot.closeCount, 1, "back at the root closes the app"); }); /* ————— RS8 — console discipline (D-15 hygiene, T-05-13; re-pinned 06-06) ————— */ test("RS8 console discipline — only ARAHARAH-ROUTE/GUIDE state tokens, no interpolation, no coordinates (re-pinned 06-06)", () => { let consoleCalls = 0; for (const file of routeModuleFiles()) { const code = codeText(file); for (const line of code.split("\n")) { if (!line.includes("console.")) continue; consoleCalls += 1; assert.ok( line.includes("ARAHARAH-ROUTE state=") || line.includes("ARAHARAH-GUIDE state="), `${path.basename(file)} logs something other than a state token: ${line.trim()}`, ); assert.ok(!line.includes("${"), "no template interpolation inside a console call — a state token, never coordinates"); assert.ok(!/\blat\b|\blng\b/.test(line), "no coordinate field names on a console line (T-06-04: category-only)"); } } assert.equal(consoleCalls, 3, "exactly three console lines: the two ROUTE tokens + the one GUIDE state logger (06-06)"); }); /* ————— RS9 — strings-only copy (COPY5's companion) ————— */ test("RS9 strings-only copy — no pending-card English in web/src/route; the card module imports strings", () => { const bannedCopy = [ "Routing not wired up yet", "never sent anywhere", "Penghalaan belum dipasang", "SEARCH AGAIN", "CARI SEMULA", "Alternative routes", "Laluan alternatif", "Fastest route", "Laluan terpantas", "ROUTE // ROUTER", "koordinat anda tidak pernah", ]; for (const file of routeModuleFiles()) { const code = codeText(file); for (const phrase of bannedCopy) { assert.ok(!code.includes(phrase), `${path.basename(file)} inlines route copy ("${phrase}") — it lives only in strings.ts`); } } const cardSrc = readFileSync(path.join(ROUTE_DIR, "route-card.ts"), "utf8"); assert.ok( /from "\.\.\/i18n\/strings\.ts"/.test(cardSrc), "route-card.ts imports the strings module (the copy's single source of truth)", ); }); /* ————— RS10 — the route family exists under the storage gate ————— */ test("RS10 no-storage floor bump — web/src/route exists and joins the GEO3 scan (>= 24 files, the guidance family landed)", () => { assert.ok(existsSync(ROUTE_DIR), "web/src/route exists (the Phase 5 route module family)"); const all = listFiles(SRC, [".ts", ".html"]); assert.ok(all.length >= 24, `the src scan totals >= 24 files now the Phase 6 guidance family landed (found ${all.length})`); const routeFiles = listFiles(ROUTE_DIR, [".ts"]); assert.ok(routeFiles.length >= 4, `the route family carries its four modules (found ${routeFiles.length})`); for (const file of routeFiles) { assert.ok(all.includes(file), `${path.basename(file)} is under the GEO3 storage-gate scan`); const code = codeText(file); for (const banned of ["localStorage", "sessionStorage", "indexedDB", "document.cookie", "caches"]) { assert.ok(!code.includes(banned), `${path.basename(file)} references ${banned} — the route is memory-only (D-12)`); } } }); /* ————— constants sanity (the camera law's exported face) ————— */ test("RS4 camera constants — exported values match the UI-SPEC table exactly", () => { assert.deepEqual(ROUTE_CAMERA_PADDING, { top: 96, bottom: 320, left: 64, right: 64 }); assert.equal(ROUTE_FIT_DURATION_MS, 400); assert.equal(ROUTE_CAMERA_PITCH, 35); assert.deepEqual(ROUTE_LAYER_IDS, ["route-alt-casing", "route-alt-line", "route-casing", "route-line"]); }); /* ============================================================ * 06-06 extensions — the guidance wiring gates * ============================================================ * RS7b PX-01 full walk — the context-aware BackButton resolver * walks search → routes → guidance → (confirm) → routes → search, * never dead-ends, close() ONLY at the true root * RS11 guidance camera — constants pinned + the locked-follow pose * (padding {320,64,64,64}, pitch 35, zoom 16, 400ms, bearing NEVER) * RS12 camera unlock law — programmatic moves never unlock; a user * movestart (originalEvent) unlocks → Phase 3 free state returns * RS13 follow(fix) defaults — byte-identical camera law when called * without opts (Phase 3/5 call sites keep their exact behavior) */ /** A recording TelegramWebAppLike fake — the RS7 factory, hoisted for the * 06-06 walk tests. Records show/hide/click/close counts. */ function mkRecordingWa() { const wa = { initData: "", isVersionAtLeast: () => true, closeCount: 0, shown: 0, hidden: 0, clicks: [], close() { wa.closeCount += 1; }, BackButton: { show() { wa.shown += 1; }, hide() { wa.hidden += 1; }, onClick(cb) { wa.clicks.push(cb); }, offClick() {}, }, }; return wa; } test("RS7b PX-01 full walk — the context-aware resolver walks every state, close() NEVER fires from a pushed state", async () => { const { wireBackButton } = await import("../src/telegram/backbutton.ts"); const wa = mkRecordingWa(); // The app's pushed-state surface — exactly what main.ts's resolver reads // once 06-06 wires the full graph: root | route | guidance | confirm | ended. const app = { surface: "root" }; const walked = []; let guidanceTornDown = false; const wire = wireBackButton(wa, () => { switch (app.surface) { case "route": return { kind: "callback", fn: () => walked.push("route-walk") }; case "guidance": return { kind: "callback", fn: () => walked.push("open-confirm") }; case "confirm": return { kind: "callback", fn: () => walked.push("continue") }; case "ended": return { kind: "callback", fn: () => walked.push("ended-walk") }; default: return { kind: "hidden" }; } }); assert.equal(typeof wire.sync, "function", "the wire exposes a re-apply (sync) for state changes"); assert.equal(wa.hidden, 1, "the wire hides at init — boot starts at the root"); const click = wa.clicks[0]; assert.equal(wa.clicks.length, 1, "exactly one click handler — the resolver is consulted per click"); // root: hidden, nothing pushed wire.sync(); assert.equal(wa.hidden, 2, "root stays hidden after sync"); assert.equal(wa.shown, 0, "nothing shown at the root"); // routes: shown; click → Phase 5 walk; never close() app.surface = "route"; wire.sync(); assert.ok(wa.shown >= 1, "route state shows the button"); click(); assert.deepEqual(walked, ["route-walk"], "route click walks back (Phase 5 walk)"); app.surface = "root"; // guidance: shown; click → OPENS THE CONFIRM CARD, not teardown (D-09) app.surface = "guidance"; wire.sync(); click(); assert.deepEqual(walked, ["route-walk", "open-confirm"], "guidance click opens the confirm card"); assert.equal(guidanceTornDown, false, "guidance is NOT torn down by Back (PX-03: never silently)"); // confirm open: click → CONTINUE (dismiss, guidance alive) — no stacked dialogs app.surface = "confirm"; wire.sync(); click(); assert.deepEqual(walked, ["route-walk", "open-confirm", "continue"], "confirm-state Back means CONTINUE"); app.surface = "guidance"; assert.equal(guidanceTornDown, false, "guidance alive after CONTINUE"); // YES, ABANDON (the confirm card's own action — 06-05's surface, wired in // task 2): teardown → chrome clears → the route card state guidanceTornDown = true; app.surface = "ended"; wire.sync(); click(); // the trip-ended / post-abandon state: click → Phase 5 walk → search root assert.deepEqual(walked, ["route-walk", "open-confirm", "continue", "ended-walk"], "ended click walks to the search root"); app.surface = "root"; wire.sync(); assert.equal(wa.hidden, 3, "the walk landed at the root — hidden again (the graph's exit ramp)"); // THE LAW: close() recorded ZERO times across every pushed-state click. assert.equal(wa.closeCount, 0, "close() fires only at the true root — never from a pushed state"); }); test("RS7b root close — the resolver's {kind:'close'} keeps Telegram's own back semantics", async () => { const { wireBackButton } = await import("../src/telegram/backbutton.ts"); const wa = mkRecordingWa(); wireBackButton(wa, () => ({ kind: "close" })); assert.ok(wa.shown >= 1, "an explicit close state SHOWS the button (it is actionable)"); wa.clicks[0](); assert.equal(wa.closeCount, 1, "the explicit close path is the root's Telegram semantics"); }); test("RS11 guidance camera — constants pinned at the UI-SPEC pose; bearing NEVER passed; reroute never moves the camera", async () => { const { createFollowController } = await import("../src/map/follow.ts"); const cam = await import("../src/guidance/camera.ts"); const { GUIDANCE_CAMERA_PADDING, GUIDANCE_ZOOM, GUIDANCE_EASE_MS, GUIDANCE_CAMERA_PITCH, createGuidanceCamera } = cam; assert.deepEqual(GUIDANCE_CAMERA_PADDING, { top: 320, bottom: 64, left: 64, right: 64 }, "GUIDANCE_CAMERA_PADDING (center ≈65% down-screen)"); assert.equal(GUIDANCE_ZOOM, 16, "GUIDANCE_ZOOM — urban detail"); assert.equal(GUIDANCE_EASE_MS, 400, "GUIDANCE_EASE_MS — the one easing discipline"); assert.equal(GUIDANCE_CAMERA_PITCH, 35, "GUIDANCE_CAMERA_PITCH — the standing pitch"); const map = fakeMap(); const follow = createFollowController(map, () => {}); const camera = createGuidanceCamera(map, follow); camera.easeToSnapped({ lat: 3.11, lng: 101.69 }); assert.equal(map.recorded.flyTo.length, 1, "the pose rides the follow controller's easing"); const opts = map.recorded.flyTo[0]; assert.deepEqual(opts.center, [101.69, 3.11], "centered on the SNAPPED point (never the raw fix)"); assert.deepEqual(opts.padding, { top: 320, bottom: 64, left: 64, right: 64 }, "the guidance padding"); assert.equal(opts.pitch, 35, "pitch 35 — the standing pitch, never changed by guidance"); assert.equal(opts.zoom, 16, "zoom 16"); assert.equal(opts.duration, 400, "400ms ease"); assert.ok(!("bearing" in opts), "bearing NEVER passed — north-up is the recorded call"); const before = map.recorded.flyTo.length; camera.onRerouteApplied(); assert.equal(map.recorded.flyTo.length, before, "reroute snap-in: NO camera jump (setData happens in the route layer)"); }); test("RS12 camera unlock law — programmatic moves never unlock; a user movestart unlocks (Phase 3 free state returns)", async () => { const { createFollowController } = await import("../src/map/follow.ts"); const map = fakeMap(); const modes = []; const follow = createFollowController(map, (m) => modes.push(m)); follow.follow({ lat: 3.1, lng: 101.68 }); // the per-fix ease is programmatic map.emit("movestart", {}); // a programmatic move mid-flight (no originalEvent) assert.deepEqual(modes, ["following"], "a programmatic move NEVER reads as the user panning (T-03-16)"); map.emit("movestart", { originalEvent: { type: "pointerdown" } }); assert.deepEqual(modes, ["following", "free"], "a user gesture unlocks → the recenter control's notch clears"); }); test("RS13 follow(fix) without opts — byte-identical camera law for the Phase 3/5 call sites", async () => { const { createFollowController } = await import("../src/map/follow.ts"); const map = fakeMap(); const follow = createFollowController(map, () => {}); follow.follow({ lat: 3.1, lng: 101.68, accuracyM: 8, headingDeg: null, ts: 1, source: "webview" }); assert.equal(map.recorded.flyTo.length, 1); const opts = map.recorded.flyTo[0]; assert.deepEqual(opts.center, [101.68, 3.1]); assert.equal(opts.padding, 96, "CAMERA_PADDING_PX unchanged"); assert.equal(opts.duration, 400, "RECENTER_DURATION_MS unchanged"); assert.ok(!("pitch" in opts), "no pitch key when opts absent"); assert.ok(!("zoom" in opts), "no zoom key when opts absent"); }); /* ============================================================ * 06-06 task 2 — the guidance pump gates (G1..G8) * ============================================================ * The pump drives the REAL controller through recording fakes with the * REAL 06-02 machine + REAL 06-05 builders (the document fake lets the * builders compose in Node; the glyph ?raw loader serves their imports): * * G1 activation gate — a sub-threshold fix keeps the Phase 5 route * state (no guidance chrome, PX-03 unbound); a fix >= 30 m along * activates the cockpit (data-guidance, close protection, banner + * card, locked camera pose, state=on_route token) * G2 pump updates — banner distance/street from the model, ETA card, * the stepping progress bar * G3 OFF_ROUTE grace — the banner shows NO premature signal * G4 REROUTING — calm swap + dash + gantry; exactly ONE source.route * call; the storm guard holds under an immediate second excursion * G5 pending precondition + reroute failure → NO REROUTE + card state * line (fail-honest, calm) * G6 reroute success → new line setData + NO camera jump + gantry stop * G7 END TRIP → confirm → YES, ABANDON teardown (watch stop, close- * protection release, data-guidance removed, trip-end card, * state=ended); CONTINUE dismisses with guidance alive * G8 arrival → the same teardown from the machine's arrival effect */ register(new URL("./glyph-raw-loader.mjs", import.meta.url)); /** Minimal element fake — the guidance-surface pattern (exactly the surface * the builders compose with, plus template parsing for svgHost). */ function makeEl(tag) { const el = { tagName: tag.toUpperCase(), children: [], attributes: {}, dataset: {}, style: {}, className: "", id: "", type: "", textContent: "", handlers: {}, appendChild(child) { el.children.push(child); return child; }, append(...cs) { for (const c of cs) el.appendChild(c); }, replaceChildren(...cs) { el.children = [...cs]; }, setAttribute(name, value) { el.attributes[name] = String(value); }, getAttribute(name) { return el.attributes[name]; }, addEventListener(type, cb) { (el.handlers[type] ??= []).push(cb); }, click() { for (const cb of el.handlers.click ?? []) cb(); }, }; if (tag === "template") { let content = { firstElementChild: null }; Object.defineProperty(el, "innerHTML", { set(html) { el.rawHTML = html; const m = /]*)>/.exec(html); const attrs = {}; if (m) for (const a of m[1].matchAll(/([\w-]+)="([^"]*)"/g)) attrs[a[1]] = a[2]; const svg = makeEl("svg"); svg.attributes = attrs; content = { firstElementChild: svg }; }, get() { return el.rawHTML ?? ""; }, }); Object.defineProperty(el, "content", { get() { return content; }, }); } return el; } const realDocument = globalThis.document; /** Install the minimal document fake for the duration of `body` (the body * may be async — the fake is installed until it settles). */ async function withDocumentAsync(body) { globalThis.document = { createElement: (tag) => makeEl(tag) }; try { return await body(); } finally { globalThis.document = realDocument; } } /** Depth-first search by className (space-separated token match). */ function findByClass(root, cls) { if (root === null || root === undefined) return null; if (typeof root.className === "string" && root.className.split(/\s+/).includes(cls)) return root; for (const child of root.children ?? []) { const hit = findByClass(child, cls); if (hit !== null) return hit; } return null; } /* ————— a stepped corridor fixture (turn marks the machine can see) ————— */ // East corridor, ~1000 m per leg: P0 →(Jalan Satu)→ P1 →(RIGHT onto Jalan // Dua)→ P2. Step distances sum to the geometry total so the arrive mark // lands at totalM (2000). On-line fixes project with ~0 deviation; the // excursion fix veers ~5 km north (far beyond the accuracy-aware cap). const P0 = { lat: 3.1, lng: 101.68 }; const P1 = { lat: 3.109, lng: 101.68 }; const P2 = { lat: 3.118, lng: 101.68 }; const CORRIDOR_POINTS = [P0, P1, P2]; const CORRIDOR_CUMULATIVE = [0, 1000, 2000]; /** Fix ON the line, `alongM` meters from P0 (0.009° lat ≈ 1000 m). */ function onLineFix(alongM, ts, accuracyM = 8) { const lat = P0.lat + (alongM / 1000) * 0.009; return { lat, lng: P0.lng, accuracyM, headingDeg: null, ts, source: "webview" }; } /** A fix ~5 km north of the line at the same along-position — a real * excursion (deviation far beyond the 100 m threshold cap). */ function excursionFix(alongM, ts) { const f = onLineFix(alongM, ts); return { ...f, lat: f.lat + 0.045 }; } function steppedDeps(opts = {}) { const corridor = { destinationId: DEST.id, origin: { lat: FIX.lat, lng: FIX.lng }, backend: "engine-a", options: [ { geometry: { points: CORRIDOR_POINTS, cumulativeDistanceM: CORRIDOR_CUMULATIVE }, summary: { distanceM: 2000, durationS: 240 }, // 4 MIN steps: [ { kind: "depart", distanceM: 1000, name: "Jalan Satu" }, { kind: "right", distanceM: 1000, name: "Jalan Dua" }, { kind: "arrive", distanceM: 0 }, ], }, ], }; const deps = fakeDeps({ result: opts.result === undefined ? corridor : opts.result }); const guide = { shellRoot: { dataset: {} }, bannerHost: makeEl("div"), listHost: makeEl("div"), wa: (() => { const wa = mkRecordingWa(); wa.haptics = []; wa.swipes = []; wa.HapticFeedback = { selectionChanged: () => wa.haptics.push("approach"), impactOccurred: () => wa.haptics.push("maneuver"), notificationOccurred: () => wa.haptics.push("arrive"), }; wa.disableVerticalSwipes = () => wa.swipes.push("off"); wa.enableVerticalSwipes = () => wa.swipes.push("on"); wa.enableClosingConfirmation = () => wa.swipes.push("net-on"); wa.disableClosingConfirmation = () => wa.swipes.push("net-off"); return wa; })(), watchStarted: 0, watchStopped: 0, onFix: null, now: 1_000_000, guidanceChanges: 0, sourceCalls: [], }; // every source.route call is recorded (the exactly-one-request gates) const baseRoute = deps.source.route.bind(deps.source); deps.source.route = (req) => { guide.sourceCalls.push(req); return baseRoute(req); }; deps.wa = guide.wa; deps.shellRoot = guide.shellRoot; deps.bannerHost = guide.bannerHost; deps.listHost = guide.listHost; deps.startWatch = (onFix) => { guide.onFix = onFix; guide.watchStarted += 1; return { stop: () => (guide.watchStopped += 1) }; }; deps.nowMs = () => guide.now; deps.findGantry = (banner) => findByClass(banner, "guide-gantry"); deps.onGuidanceChange = () => { guide.guidanceChanges += 1; }; deps.__guide = guide; return deps; } /** Feed one fix `alongM` meters along (accepted by definition — the injected * watch skips the throttle). advanceMs moves the injected clock FIRST so * the fix's ts and the machine's nowMs agree. */ function feed(alongM, opts = {}) { const deps = opts.deps; if (opts.advanceMs) deps.__guide.now += opts.advanceMs; const ts = deps.__guide.now; const fix = opts.excursion ? excursionFix(alongM, ts) : onLineFix(alongM, ts); deps.__guide.onFix(fix); return fix; } /** Drain the pump's async reroute path (microtask ticks). */ async function drain(ticks = 12) { for (let i = 0; i < ticks; i += 1) await Promise.resolve(); } function guideBanner(deps) { return findByClass(deps.__guide.bannerHost, "guide-banner"); } function guideRegister(deps) { const banner = guideBanner(deps); const register = banner === null ? null : findByClass(banner, "guide-register"); return register === null ? null : register.textContent; } function guideDistance(deps) { const banner = guideBanner(deps); const distance = banner === null ? null : findByClass(banner, "guide-distance"); return distance === null ? null : distance.textContent; } /** The standard live-driving setup: route drawn, guidance activated at * 500 m along, then an accepted fix at 700 m (6 s later). */ async function driveTo(deps, controller) { await withConsole(() => controller.consult(FIX, DEST)); feed(500, { deps }); feed(700, { deps, advanceMs: 6000 }); } test("G1 activation gate — a sub-threshold fix keeps the Phase 5 route state; a fix ≥ 30 m along activates the cockpit", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); const consult = await withConsole(() => controller.consult(FIX, DEST)); assert.equal(controller.state, "route"); assert.equal(deps.__guide.watchStarted, 1, "the guidance watch starts with the route draw"); assert.equal(deps.__guide.shellRoot.dataset.guidance, undefined, "no data-guidance before the start gate"); // sub-threshold: 10 m along (< GUIDANCE_START_ALONG_M = 30) feed(10, { deps }); assert.equal(deps.__guide.shellRoot.dataset.guidance, undefined, "no guidance chrome below the start gate"); assert.equal(guideBanner(deps), null, "no banner below the start gate"); assert.equal(deps.__guide.wa.swipes.length, 0, "close protection unbound below the gate (PX-03's precondition)"); assert.ok(consult.lines.every((l) => !l.includes("ARAHARAH-GUIDE")), "no GUIDE tokens below the gate"); assert.equal(deps.__recorded.shown.at(-1).register, "bottom", "the Phase 5 route card still holds"); // activation: 500 m along const activated = await withConsole(async () => { feed(500, { deps }); return null; }); assert.equal(deps.__guide.shellRoot.dataset.guidance, "active", "[data-guidance=active] on the shell root"); const banner = guideBanner(deps); assert.ok(banner !== null, "the banner mounts"); assert.equal(guideRegister(deps), "GUIDE // NEXT", "the on_route register word"); assert.ok(findByClass(banner, "guide-plate") !== null, "the glyph plate mounts"); assert.equal(deps.__recorded.shown.at(-1).register, "bottom", "the guidance card holds the bottom register"); assert.ok(deps.__guide.wa.swipes.includes("off"), "close protection engaged (body-swipe off)"); assert.ok(deps.__guide.wa.swipes.includes("net-on"), "the native-close net engaged"); const pose = deps.__map.recorded.flyTo.at(-1); assert.ok(pose, "the camera eases to the snapped line on activation"); assert.deepEqual(pose.padding, { top: 320, bottom: 64, left: 64, right: 64 }, "the guidance padding"); assert.equal(pose.pitch, 35, "pitch 35"); assert.equal(pose.zoom, 16, "zoom 16"); assert.ok(!("bearing" in pose), "north-up — no bearing key"); assert.ok( activated.lines.some((l) => l.includes("ARAHARAH-GUIDE state=on_route")), "ARAHARAH-GUIDE state=on_route logged at activation", ); }); }); test("G2 pump updates — banner distance/street from the model, ETA card, stepping progress bar", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); await driveTo(deps, controller); const banner = guideBanner(deps); const distance = Number(findByClass(banner, "guide-distance").textContent); assert.ok(Math.abs(distance - 300) <= 5, `banner distance ≈ 300 m to the turn (got ${distance})`); assert.equal(findByClass(banner, "guide-unit").textContent, "M"); assert.equal(findByClass(banner, "guide-street").textContent, "Jalan Dua", "the turn's street, verbatim from the step"); const card = deps.__recorded.shown.at(-1).card; const telemetry = findByClass(card, "route-telemetry"); assert.ok(telemetry !== null, "the guidance card carries the telemetry row"); assert.ok(/\d/.test(findByClass(telemetry, "text-telemetry-num").textContent), "minutes remaining renders"); const progress = findByClass(card, "guide-progress"); assert.equal(progress.attributes["aria-valuenow"], "35", "progress pct = alongM/totalM (700/2000)"); assert.equal(findByClass(card, "guide-progress-fill").style.width, "35%", "instant width per accepted fix"); assert.ok(findByClass(card, "btn-secondary") !== null, "END TRIP is the one card action"); }); }); test("G3 OFF_ROUTE grace — the banner shows NO premature signal while the machine is deciding", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); await driveTo(deps, controller); // two far-off fixes 1 s apart — below the 3-fix + 8 s gate: the machine // has not acted, so the banner must not either feed(750, { deps, excursion: true, advanceMs: 1000 }); feed(800, { deps, excursion: true, advanceMs: 1000 }); assert.equal(guideRegister(deps), "GUIDE // NEXT", "no premature signal — the register holds"); assert.equal(findByClass(guideBanner(deps), "guide-street").textContent, "Jalan Dua", "the street slot holds — no calm copy"); assert.equal(deps.__guide.sourceCalls.length, 0, "zero source.route calls during the grace"); }); }); test("G4 REROUTING — the calm swap + dash + gantry; exactly ONE source.route call; the storm guard holds", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); await driveTo(deps, controller); // the excursion: 3 consecutive off fixes spanning the 8 s grace const e1 = feed(750, { deps, excursion: true }); feed(800, { deps, excursion: true, advanceMs: 9000 }); feed(850, { deps, excursion: true, advanceMs: 500 }); await drain(); assert.equal(deps.__guide.sourceCalls.length, 1, "exactly ONE reroute request"); assert.equal(deps.__guide.sourceCalls[0].destinationId, DEST.id, "the request carries the destination"); assert.equal(deps.__guide.sourceCalls[0].origin.lat, e1.lat, "the origin is the honest raw excursion fix"); assert.equal(guideRegister(deps), "GUIDE // REROUTING", "the calm register swap"); assert.equal(guideDistance(deps), "—", "distance is the honest dash"); const gantry = findByClass(guideBanner(deps), "guide-gantry"); assert.ok(gantry !== null, "the gantry mounts"); assert.equal(gantry.dataset.fill, "on", "the blink duty cycle started"); assert.ok( findByClass(guideBanner(deps), "guide-street").textContent.length > 0, "the calm reroute copy shows (never a blank slot)", ); // an immediate second excursion inside the storm guard: silent feed(900, { deps, excursion: true, advanceMs: 2000 }); await drain(); assert.equal(deps.__guide.sourceCalls.length, 1, "the storm guard holds — still exactly one call"); assert.equal(guideRegister(deps), "GUIDE // REROUTING", "no state flap — banner calm"); }); }); test("G5 pending precondition + reroute failure — guidance never without a route; NO REROUTE is fail-honest", async () => { await withDocumentAsync(async () => { // (a) a null consult result = pending — the pump must never start guidance const deps = steppedDeps({ result: null }); const controller = createRouteController(deps); await withConsole(() => controller.consult(FIX, DEST)); assert.equal(controller.state, "pending", "null result = pending"); feed(500, { deps }); assert.equal(guideBanner(deps), null, "no guidance over a pending state — never without a route"); assert.equal(deps.__guide.wa.swipes.length, 0, "no PX-03 protections without a route"); // (b) a mid-drive reroute failure lands NO REROUTE, calm const deps2 = steppedDeps(); const controller2 = createRouteController(deps2); let calls = 0; const realRoute = deps2.source.route.bind(deps2.source); deps2.source.route = (req) => { calls += 1; if (calls > 1) return Promise.resolve(null); // the reroute fails honestly return realRoute(req); }; await driveTo(deps2, controller2); feed(750, { deps: deps2, excursion: true }); feed(800, { deps: deps2, excursion: true, advanceMs: 9000 }); feed(850, { deps: deps2, excursion: true, advanceMs: 500 }); await drain(); assert.equal(guideRegister(deps2), "GUIDE // NO REROUTE", "the honest degradation register"); assert.equal(guideDistance(deps2), "—", "the dash"); const card = deps2.__recorded.shown.at(-1).card; const stateLine = findByClass(card, "guide-state"); assert.ok(stateLine !== null, "the card carries the no-reroute state line"); assert.ok(stateLine.textContent.length > 0, "the state line carries the honest copy"); }); }); test("G6 reroute success — the new line setData snaps in, NO camera jump, the gantry stops", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); let calls = 0; const realRoute = deps.source.route.bind(deps.source); deps.source.route = (req) => { calls += 1; if (calls > 1) { return Promise.resolve({ destinationId: DEST.id, origin: req.origin, backend: "engine-a", options: [ { geometry: { points: [req.origin, P2], cumulativeDistanceM: [0, 1800] }, summary: { distanceM: 1800, durationS: 200 }, steps: [ { kind: "depart", distanceM: 1800, name: "Jalan Baru" }, { kind: "arrive", distanceM: 0 }, ], }, ], }); } return realRoute(req); }; await driveTo(deps, controller); const flyBefore = deps.__map.recorded.flyTo.length; const setDataBefore = deps.__map.recorded.setData.length; const e1 = feed(750, { deps, excursion: true }); feed(800, { deps, excursion: true, advanceMs: 9000 }); feed(850, { deps, excursion: true, advanceMs: 500 }); await drain(); assert.equal(guideRegister(deps), "GUIDE // NEXT", "back on the new route"); const swaps = deps.__map.recorded.setData.slice(setDataBefore); assert.ok(swaps.length >= 2, "the rerouted line snapped in via setData"); const routeSwap = swaps.find((s) => s.id === "route"); assert.deepEqual( routeSwap.data.features[0].geometry.coordinates[0], [e1.lng, e1.lat], "the new line starts at the excursion origin", ); assert.equal(deps.__map.recorded.flyTo.length, flyBefore, "NO camera jump on reroute (setData only)"); assert.equal(findByClass(guideBanner(deps), "guide-gantry"), null, "the gantry stopped on resolve"); assert.equal(findByClass(guideBanner(deps), "guide-street").textContent, "Jalan Baru", "the new street reads"); }); }); test("G7 END TRIP → confirm → YES ABANDON teardown; CONTINUE dismisses with guidance alive", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); await driveTo(deps, controller); // CONTINUE first: END TRIP opens the confirm, CONTINUE dismisses it findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP const confirm = deps.__recorded.shown.at(-1); assert.equal(confirm.register, "bottom", "the confirm card lives in the bottom register"); assert.ok(findByClass(confirm.card, "guide-confirm") !== null, "the confirm surface mounts"); findByClass(confirm.card, "btn-secondary").click(); // CONTINUE assert.equal(deps.__guide.watchStopped, 0, "CONTINUE keeps the watch running"); assert.equal(guideRegister(deps), "GUIDE // NEXT", "guidance alive after CONTINUE"); assert.equal(deps.__recorded.shown.at(-1).register, "bottom", "the guidance card returns"); // now the abandon path findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP again const abandoned = await withConsole(async () => { findByClass(deps.__recorded.shown.at(-1).card, "btn-primary").click(); // YES, ABANDON return null; }); assert.equal(deps.__guide.watchStopped, 1, "the watch stopped"); assert.ok(deps.__guide.wa.swipes.includes("on"), "body-swipes re-enabled"); assert.ok(deps.__guide.wa.swipes.includes("net-off"), "the native-close net released"); assert.equal(deps.__guide.shellRoot.dataset.guidance, undefined, "data-guidance removed — the strip returns"); assert.equal(findByClass(deps.__guide.bannerHost, "guide-banner"), null, "the banner cleared instantly"); const ended = deps.__recorded.shown.at(-1); assert.equal(ended.register, "center", "the trip-ended card holds the center register"); assert.ok(ended.card.textContent.includes("END"), "the GUIDE // END register renders"); assert.ok(abandoned.lines.some((l) => l.includes("ARAHARAH-GUIDE state=ended")), "state=ended logged"); assert.equal(controller.guidancePhase, "ended", "the BackButton graph's ended state is exposed"); }); }); test("G8 arrival — the machine's arrival effect runs the same teardown", async () => { await withDocumentAsync(async () => { const deps = steppedDeps(); const controller = createRouteController(deps); await driveTo(deps, controller); feed(1990, { deps, advanceMs: 60000 }); // within ARRIVAL_THRESHOLD_M (25) of the 2000 m end await drain(); assert.ok(deps.__guide.wa.haptics.includes("arrive"), "the arrival haptic fired"); assert.equal(deps.__guide.watchStopped, 1, "the watch stopped"); assert.equal(deps.__guide.shellRoot.dataset.guidance, undefined, "data-guidance removed"); const ended = deps.__recorded.shown.at(-1); assert.equal(ended.register, "center", "the trip-ended card in the center register"); assert.equal(controller.guidancePhase, "ended", "the ended phase exposes to the BackButton graph"); }); });