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:
araharah-executor
2026-10-24 14:47:39 +08:00
parent 7dcaa00ea3
commit 77b62da111
+200 -5
View File
@@ -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");
});