test(06-06): failing guidance-pump suite G1-G8 + RS6/RS8 re-pins (RED)
- G1 activation gate: sub-threshold fix keeps the Phase 5 route state; a fix >= 30 m along mounts the cockpit (data-guidance, close protection, banner + card, the locked camera pose, state=on_route token) - G2 pump updates: banner distance/street from the model, ETA card, stepping progress bar (pct = alongM/totalM) - G3 no premature signal during the OFF_ROUTE grace; G4 REROUTING calm swap + dash + gantry with exactly ONE source.route call under the storm guard; G5 pending precondition + NO REROUTE fail-honest degradation; G6 reroute success = setData snap-in, NO camera jump, gantry stop - G7 END TRIP -> confirm -> YES ABANDON teardown (watch stop, protection release, trip-end card, state=ended); CONTINUE dismisses, guidance alive - G8 arrival runs the same teardown from the machine's arrival effect - RS6 re-pinned: banned tokens only on ../guidance/ import lines (guidance IMPLEMENTATION lives in web/src/guidance/); RS8 re-pinned: the ARAHARAH-GUIDE token family joins the console discipline
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
* GEO3 pattern — prose is not a call site).
|
* GEO3 pattern — prose is not a call site).
|
||||||
*/
|
*/
|
||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
|
import { register } from "node:module";
|
||||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -455,15 +456,32 @@ test("RS5 card structure — zero innerHTML, role=group chips, btn-secondary onl
|
|||||||
assert.ok(!code.includes("btn-primary"), "NO .btn-primary on any Phase 5 surface (D-10: no forward action)");
|
assert.ok(!code.includes("btn-primary"), "NO .btn-primary on any Phase 5 surface (D-10: no forward action)");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ————— RS6 — the D-10 absence law ————— */
|
/* ————— RS6 — the guidance-implementation boundary (re-pinned 06-06) ————— */
|
||||||
|
|
||||||
test("RS6 D-10 absence law — zero guidance furniture in web/src/route (comment-stripped)", () => {
|
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()) {
|
for (const file of routeModuleFiles()) {
|
||||||
const code = codeText(file);
|
const code = codeText(file);
|
||||||
for (const banned of ["btn-primary", "progress", "maneuver"]) {
|
for (const line of code.split("\n")) {
|
||||||
assert.ok(!code.includes(banned), `${path.basename(file)} references ${banned} — D-10 forbids guidance furniture`);
|
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) ————— */
|
/* ————— RS7 — the BackButton walk (T-05-11) ————— */
|
||||||
@@ -515,9 +533,9 @@ test("RS7 BackButton walk — overlay click invokes walkBack and NEVER close();
|
|||||||
assert.equal(waRoot.closeCount, 1, "back at the root closes the app");
|
assert.equal(waRoot.closeCount, 1, "back at the root closes the app");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ————— RS8 — console discipline (D-15 hygiene, T-05-13) ————— */
|
/* ————— RS8 — console discipline (D-15 hygiene, T-05-13; re-pinned 06-06) ————— */
|
||||||
|
|
||||||
test("RS8 console discipline — the ONLY console calls are the two ARAHARAH-ROUTE state tokens, no interpolation", () => {
|
test("RS8 console discipline — only ARAHARAH-ROUTE/GUIDE state tokens, no interpolation, no coordinates (re-pinned 06-06)", () => {
|
||||||
let consoleCalls = 0;
|
let consoleCalls = 0;
|
||||||
for (const file of routeModuleFiles()) {
|
for (const file of routeModuleFiles()) {
|
||||||
const code = codeText(file);
|
const code = codeText(file);
|
||||||
@@ -525,13 +543,14 @@ test("RS8 console discipline — the ONLY console calls are the two ARAHARAH-ROU
|
|||||||
if (!line.includes("console.")) continue;
|
if (!line.includes("console.")) continue;
|
||||||
consoleCalls += 1;
|
consoleCalls += 1;
|
||||||
assert.ok(
|
assert.ok(
|
||||||
line.includes("ARAHARAH-ROUTE state="),
|
line.includes("ARAHARAH-ROUTE state=") || line.includes("ARAHARAH-GUIDE state="),
|
||||||
`${path.basename(file)} logs something other than a state token: ${line.trim()}`,
|
`${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(!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, 2, "exactly two console lines exist: state=drawn and state=pending");
|
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) ————— */
|
/* ————— RS9 — strings-only copy (COPY5's companion) ————— */
|
||||||
@@ -761,3 +780,490 @@ test("RS13 follow(fix) without opts — byte-identical camera law for the Phase
|
|||||||
assert.ok(!("pitch" in opts), "no pitch key when opts absent");
|
assert.ok(!("pitch" in opts), "no pitch key when opts absent");
|
||||||
assert.ok(!("zoom" in opts), "no zoom 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 = /<svg\b([^>]*)>/.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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user