feat(06-06): envelope flows + composed ladder — minimize POST, resume GET, trip-end DELETE (GREEN)
- route.ts: onDeactivated POSTs the FROZEN six-field snapshot with keepalive (the minimize race's belt) and stores the server-minted id; onActivated DELETEs (the cron pauses — structural) and RETAINS the id; teardown's deleteEnvelope completes the idempotent pair; a live envelope is never double-minted; best-effort honesty throughout (D-13) - resume(trip): the controller computes the projection from startedAtSec + avg speed (the cron's own math, one formula), renders GUIDE // RESUMING + gantry + projected card, draws the rebuilt line, arms the watch, DELETEs immediately (a resumed boot is foregrounded), and seeds the session mid-route on the first fix through the machine's own resuming path — synthesized terminal marks keep the arrive mark at the line's END (a steps-less envelope model would instantly arrive) - backAction(): the PX-01 resolver's single authority — driving → confirm, confirm → CONTINUE, ended/route → the walk, root → close - guidance.ts: resumingView keeps the progress math in guidance/ (RS6) - main.ts: the COMPOSED LADDER (engine-a first, fixture fallback — code-constructed, zero env reads), the ?g= UUID-validated reader with the defensive envelope validation (fail-honest → honest boot), the capability-guarded activated/deactivated wiring, the recenter re-lock at the guidance pose, the chain-fix camera skip while driving, and the no-double-consult resume guard - merged suite: G9-G12 + the E-family (co-executor tests reconciled: pre-gate probe, builder-format asserts, terminal-mark fixture) — 146 passed, typecheck clean
This commit is contained in:
@@ -1426,7 +1426,7 @@ test("G11 ?g= resume — RESUMING chrome with projected numbers; the first fix f
|
||||
destinationName: DEST.name,
|
||||
};
|
||||
const resumed = await withConsole(async () => {
|
||||
controller.resume({ model, durationS: 240, projectedRemainingM: 1400, startedAtSec: 900, envelopeId: "env-9" });
|
||||
controller.resume({ model, envelopeId: "env-9", distanceM: 2000, durationS: 240, startedAtSec: 900 });
|
||||
return null;
|
||||
});
|
||||
assert.equal(controller.state, "route", "the resumed trip holds the route state");
|
||||
@@ -1434,12 +1434,19 @@ test("G11 ?g= resume — RESUMING chrome with projected numbers; the first fix f
|
||||
assert.equal(deps.__guide.shellRoot.dataset.guidance, "active", "the strip compacts for the resumed trip");
|
||||
assert.equal(guideRegister(deps), "GUIDE // RESUMING", "the projected register");
|
||||
assert.ok(findByClass(guideBanner(deps), "guide-gantry") !== null, "the gantry rides the wait state");
|
||||
assert.equal(guideDistance(deps), "1.4", "the projected remaining distance renders (RESUMING legally shows numbers)");
|
||||
assert.equal(guideDistance(deps), "1.2", "the controller-computed projection renders (≈1167 m of 2000)");
|
||||
assert.equal(deps.__guide.watchStarted, 1, "the watch arms for the resumed trip");
|
||||
const routeSwap = deps.__map.recorded.setData.find((s) => s.id === "route");
|
||||
assert.equal(routeSwap.data.features[0].geometry.coordinates.length, 3, "the resumed line draws");
|
||||
assert.ok(resumed.lines.some((l) => l.includes("ARAHARAH-GUIDE state=resuming")), "state=resuming logged");
|
||||
// the first accepted fix flips to live ON_ROUTE (the machine's own path)
|
||||
// the resume DELETE pauses the cron immediately (a resumed boot is foregrounded)
|
||||
assert.equal(
|
||||
deps.__guide.fetches.filter((f) => f.init.method === "DELETE").length,
|
||||
1,
|
||||
"the resume DELETE fired with the ?g= id",
|
||||
);
|
||||
// the first accepted fix flips to live ON_ROUTE (the machine's own path) —
|
||||
// the synthesized terminal marks keep the arrive mark at the line's end
|
||||
const live = await withConsole(async () => {
|
||||
feed(600, { deps });
|
||||
return null;
|
||||
@@ -1447,13 +1454,19 @@ test("G11 ?g= resume — RESUMING chrome with projected numbers; the first fix f
|
||||
assert.equal(guideRegister(deps), "GUIDE // NEXT", "live ON_ROUTE after the first fix");
|
||||
assert.equal(findByClass(guideBanner(deps), "guide-gantry"), null, "the gantry stopped on the live flip");
|
||||
assert.ok(live.lines.some((l) => l.includes("ARAHARAH-GUIDE state=on_route")), "state=on_route logged");
|
||||
// the resumed trip's envelope deletes at teardown (the ?g= id)
|
||||
// the resumed trip's envelope deletes at teardown (the ?g= id, idempotent pair)
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP
|
||||
await withConsole(async () => {
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-primary").click(); // YES, ABANDON
|
||||
return null;
|
||||
});
|
||||
await drain();
|
||||
assert.equal(controller.guidancePhase, "ended", "the resumed trip ends through the same confirm");
|
||||
assert.equal(
|
||||
deps.__guide.fetches.filter((f) => f.init.method === "DELETE").length,
|
||||
2,
|
||||
"the teardown DELETE completes the idempotent pair",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1464,7 +1477,8 @@ test("G12 main.ts composition — the ladder, the ?g= carrier, the envelope even
|
||||
assert.ok(!mainCode.includes("process.env"), "no env reads — the backend selection is code-constructed (T-05-04)");
|
||||
assert.ok(/get\("g"\)/.test(mainCode), "the ?g= carrier is read from the URL (the destination-preload pattern)");
|
||||
assert.ok(
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(mainCode),
|
||||
mainCode.includes("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") ||
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(mainCode),
|
||||
"the resume id is UUID-validated before any GET",
|
||||
);
|
||||
assert.ok(
|
||||
@@ -1477,3 +1491,137 @@ test("G12 main.ts composition — the ladder, the ?g= carrier, the envelope even
|
||||
assert.ok(/resumeId === null/.test(mainCode), "a resume boot never double-consults");
|
||||
assert.ok(/kind: "callback"/.test(mainCode) || /kind: "hidden"/.test(mainCode), "the BackButton resolver walks the full graph");
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
* 06-06 task 3 — the complementary envelope/resume gates (E1..E5)
|
||||
* ============================================================
|
||||
* G9-G12 pin the core flows; these pin the deltas they do not:
|
||||
* E1 the pre-gate guard — fixes below the start gate POST nothing
|
||||
* E3' the COMPUTED projection input (distanceM + startedAtSec) renders
|
||||
* the same RESUMING chrome, and the resume DELETE pauses the cron
|
||||
* E4 a 404 second delete is tolerated — fail-honest, chrome completes
|
||||
* E5 backAction — the controller's resolver walks driving → confirm →
|
||||
* ended → root with zero dead ends (the wire's contract, task 1)
|
||||
* E6 main.ts structural deltas — no env reads in the ladder region,
|
||||
* the onEvent capability guard, the resolver delegation
|
||||
*/
|
||||
|
||||
const RESUME_ENVELOPE_ID = "0b501b1e-0000-4000-8000-00000000e1e5";
|
||||
|
||||
test("E1 pre-gate guard — fixes below the start gate never mint an envelope", async () => {
|
||||
await withDocumentAsync(async () => {
|
||||
const deps = steppedDeps();
|
||||
const controller = createRouteController(deps);
|
||||
await withConsole(() => controller.consult(FIX, DEST));
|
||||
feed(10, { deps }); // below GUIDANCE_START_ALONG_M — guidance never activates
|
||||
await controller.onDeactivated();
|
||||
assert.equal(deps.__guide.fetches.length, 0, "no envelope before guidance is live");
|
||||
feed(500, { deps }); // activation
|
||||
await controller.onDeactivated();
|
||||
assert.equal(deps.__guide.fetches.length, 1, "the live drive POSTs on minimize");
|
||||
});
|
||||
});
|
||||
|
||||
test("E3' computed projection — resume({distanceM, startedAtSec}) renders the projected RESUMING chrome; the resume DELETE pauses the cron", async () => {
|
||||
await withDocumentAsync(async () => {
|
||||
const deps = steppedDeps();
|
||||
const controller = createRouteController(deps);
|
||||
controller.resume({
|
||||
model: {
|
||||
points: CORRIDOR_POINTS,
|
||||
cumulative: CORRIDOR_CUMULATIVE,
|
||||
steps: [],
|
||||
destinationId: DEST.id,
|
||||
destinationName: DEST.name,
|
||||
},
|
||||
envelopeId: RESUME_ENVELOPE_ID,
|
||||
distanceM: 2000,
|
||||
durationS: 240,
|
||||
startedAtSec: 900, // now() = 1_000_000 ms → 100 s elapsed → remaining ≈ 1167 m / 140 s
|
||||
});
|
||||
assert.equal(guideRegister(deps), "GUIDE // RESUMING");
|
||||
assert.equal(guideDistance(deps), "1.2", "the computed projection renders (1167 m)");
|
||||
const card = deps.__recorded.shown.at(-1).card;
|
||||
const minutes = Number(findByClass(findByClass(card, "route-telemetry"), "text-telemetry-num").textContent);
|
||||
assert.ok(minutes === 2, `projected minutes render rounded (got ${minutes})`); // 140 s
|
||||
const deletes = deps.__guide.fetches.filter((f) => f.init.method === "DELETE");
|
||||
assert.equal(deletes.length, 1, "the resume deletes the envelope — the cron pauses NOW (D-11)");
|
||||
assert.equal(deletes[0].url, `/api/guidance?id=${RESUME_ENVELOPE_ID}`);
|
||||
const { stopGantryBlink } = await import("../src/guidance/banner.ts");
|
||||
feed(500, { deps }); // the live flip
|
||||
const staleGantry = findByClass(guideBanner(deps), "guide-gantry");
|
||||
if (staleGantry !== null) stopGantryBlink(staleGantry);
|
||||
assert.equal(guideRegister(deps), "GUIDE // NEXT");
|
||||
});
|
||||
});
|
||||
|
||||
test("E4 trip end deletes idempotently — a 404 second delete is tolerated, the chrome still completes", async () => {
|
||||
await withDocumentAsync(async () => {
|
||||
const deps = steppedDeps();
|
||||
deps.__guide.fetchMode = "404"; // every DELETE answers 404 not_found
|
||||
const controller = createRouteController(deps);
|
||||
controller.resume({
|
||||
model: {
|
||||
points: CORRIDOR_POINTS,
|
||||
cumulative: CORRIDOR_CUMULATIVE,
|
||||
steps: [
|
||||
{ kind: "depart", distanceM: 2000 },
|
||||
{ kind: "arrive", distanceM: 0 },
|
||||
],
|
||||
destinationId: DEST.id,
|
||||
destinationName: DEST.name,
|
||||
},
|
||||
envelopeId: RESUME_ENVELOPE_ID,
|
||||
distanceM: 2000,
|
||||
durationS: 240,
|
||||
startedAtSec: 900,
|
||||
});
|
||||
feed(500, { deps });
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-primary").click(); // YES, ABANDON
|
||||
await drain();
|
||||
const deletes = deps.__guide.fetches.filter((f) => f.init.method === "DELETE");
|
||||
assert.ok(deletes.length >= 2, "resume delete + teardown delete (the idempotent pair)");
|
||||
assert.equal(controller.guidancePhase, "ended", "the 404 changed nothing — fail-honest");
|
||||
assert.equal(deps.__guide.shellRoot.dataset.guidance, undefined, "the chrome completed");
|
||||
assert.equal(findByClass(deps.__guide.bannerHost, "guide-banner"), null, "the banner cleared");
|
||||
});
|
||||
});
|
||||
|
||||
test("E5 backAction — the resolver walks driving → confirm → ended → root; close only at the root", async () => {
|
||||
await withDocumentAsync(async () => {
|
||||
const deps = steppedDeps();
|
||||
const controller = createRouteController(deps);
|
||||
await driveTo(deps, controller);
|
||||
feed(500, { deps });
|
||||
let action = controller.backAction();
|
||||
assert.equal(action.kind, "callback", "driving: back means the confirm card");
|
||||
action.fn();
|
||||
assert.equal(controller.guidancePhase, "confirm");
|
||||
action = controller.backAction();
|
||||
assert.equal(action.kind, "callback", "confirm open: back means CONTINUE");
|
||||
action.fn();
|
||||
assert.equal(controller.guidancePhase, "driving", "guidance alive after CONTINUE");
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP
|
||||
findByClass(deps.__recorded.shown.at(-1).card, "btn-primary").click(); // YES, ABANDON
|
||||
action = controller.backAction();
|
||||
assert.equal(action.kind, "callback", "ended: back walks to the search root");
|
||||
action.fn();
|
||||
assert.equal(controller.state, "idle", "the walk landed at the root");
|
||||
action = controller.backAction();
|
||||
assert.deepEqual(action, { kind: "close" }, "the true root keeps Telegram's own semantics");
|
||||
});
|
||||
});
|
||||
|
||||
test("E6 main.ts structural deltas — no env reads in the ladder region, the onEvent capability guard, the resolver delegation", () => {
|
||||
const mainCode = codeText(path.join(SRC, "main.ts"));
|
||||
const ladderStart = mainCode.indexOf("createFixtureRoutingSource");
|
||||
const ladderEnd = mainCode.indexOf("createRouteController(");
|
||||
assert.ok(ladderStart >= 0 && ladderEnd > ladderStart, "the ladder region exists");
|
||||
assert.ok(
|
||||
!mainCode.slice(ladderStart, ladderEnd).includes("import.meta.env"),
|
||||
"backend selection is code-constructed — no env reads in the ladder region (T-05-04)",
|
||||
);
|
||||
assert.ok(/typeof wa\.onEvent === "function"/.test(mainCode), "the onEvent wiring is capability-guarded (T-06-06)");
|
||||
assert.ok(/wireBackButton\(\s*wa,\s*\(\)\s*=>\s*backResolve\(\)\)/.test(mainCode), "the wire delegates to the controller's resolver");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user