test(06-06): failing camera-law + PX-01 full-walk gates — RS7b walk, RS11 pose, RS12 unlock, RS13 no-opts byte-identity (RED)
- RS7b: the context-aware BackButton resolver walks search → routes →
guidance → (confirm) → routes → search; close() NEVER fires from a
pushed state; the wire exposes sync() for state changes
- RS11: GUIDANCE_CAMERA_PADDING/{ZOOM,EASE_MS,CAMERA_PITCH} pinned at the
UI-SPEC pose; bearing key NEVER present; onRerouteApplied moves nothing
- RS12: programmatic moves never unlock follow; a user movestart
(originalEvent) unlocks — Phase 3 free state returns
- RS13: follow(fix) without opts stays byte-identical (no pitch/zoom keys)
- fakeMap grows flyTo/on/emit recording; suite header documents the RS6/RS8
re-pins landing with the guidance wiring (tasks 2-3)
This commit is contained in:
@@ -11,13 +11,23 @@
|
||||
* 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 D-10 absence law — zero guidance furniture
|
||||
* RS7 BackButton walk — overlay walks, root closes (T-05-11)
|
||||
* RS8 console discipline — only the two state tokens (D-15/T-05-13)
|
||||
* 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
|
||||
@@ -73,10 +83,13 @@ function listFiles(dir, exts, acc = []) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** A recording MapLike fake — every interaction is captured for assertion. */
|
||||
/** 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 recorded = { addSource: [], addLayer: [], setData: [], fitBounds: [] };
|
||||
const handlers = {};
|
||||
const recorded = { addSource: [], addLayer: [], setData: [], fitBounds: [], flyTo: [] };
|
||||
return {
|
||||
recorded,
|
||||
addSource(id, data) {
|
||||
@@ -97,9 +110,18 @@ function fakeMap({ bearing = 42 } = {}) {
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -566,3 +588,176 @@ test("RS4 camera constants — exported values match the UI-SPEC table exactly",
|
||||
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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user