From fca2a904d013aee483bc1a51b967f671e8beaaa2 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 20:59:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(06-08):=20the=20D-09=20confirm=20card=20sur?= =?UTF-8?q?vives=20a=20live=20fix=20=E2=80=94=20renderCockpit=20guards=20t?= =?UTF-8?q?he=20card=20rebuild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - a fix arriving while the abandon confirm is open rebuilt the guidance card over it (the ONE PX-03 exit guard evaporating mid-decision); the pump now re-renders banner/list only and the confirm surface survives until CONTINUE or YES, ABANDON - G7b pins it: confirm mounts -> live fix -> confirm still shown, guidancePhase=confirm holds, YES ABANDON still tears down - found while building 06-08's scripted END TRIP capture leg; the real drive's step 6 (END TRIP on a live road) would have hit this within one watch fix --- web/src/route/route.ts | 32 +++++++++++++++++++------------- web/test/route-surface.test.mjs | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/web/src/route/route.ts b/web/src/route/route.ts index 79bb152..8ff590e 100644 --- a/web/src/route/route.ts +++ b/web/src/route/route.ts @@ -372,19 +372,25 @@ export function createRouteController(deps: RouteControllerDeps): { } } const eta = g.eta ?? updateEta(null, 0, g.snap.remainingM); - deps.showRouteCard( - buildGuidanceCard( - deps.locale, - { - ...view.card, - minutesRemaining: eta.displayedEtaS / 60, - kmRemaining: eta.displayedRemainingM / 1000, - etaClock: composeEtaClock(eta.displayedEtaS, now()), - }, - { onEndTrip: openConfirm }, - ), - "bottom", - ); + // G7b: while the D-09 confirm card owns the bottom register, the pump's + // per-fix re-render must never rebuild the guidance card over it — the + // confirm is the ONE exit guard (PX-03); a live fix re-renders the + // banner/list but the confirm surface survives until CONTINUE/abandon. + if (!g.confirmOpen) { + deps.showRouteCard( + buildGuidanceCard( + deps.locale, + { + ...view.card, + minutesRemaining: eta.displayedEtaS / 60, + kmRemaining: eta.displayedRemainingM / 1000, + etaClock: composeEtaClock(eta.displayedEtaS, now()), + }, + { onEndTrip: openConfirm }, + ), + "bottom", + ); + } } function toggleList(): void { diff --git a/web/test/route-surface.test.mjs b/web/test/route-surface.test.mjs index 051c64f..f975676 100644 --- a/web/test/route-surface.test.mjs +++ b/web/test/route-surface.test.mjs @@ -1307,6 +1307,29 @@ test("G7 END TRIP → confirm → YES ABANDON teardown; CONTINUE dismisses with }); }); +test("G7b the confirm card survives a live fix — the D-09 surface never evicts mid-decision", async () => { + await withDocumentAsync(async () => { + const deps = steppedDeps(); + const controller = createRouteController(deps); + await driveTo(deps, controller); + findByClass(deps.__recorded.shown.at(-1).card, "btn-secondary").click(); // END TRIP + assert.ok(findByClass(deps.__recorded.shown.at(-1).card, "guide-confirm") !== null, "the confirm surface mounts"); + // a live fix arrives while the driver is deciding — the confirm card is + // the ONE exit guard; a pump re-render must never replace it + feed(1200, { deps, advanceMs: 2500 }); + await drain(); + const shown = deps.__recorded.shown.at(-1); + assert.ok( + findByClass(shown.card, "guide-confirm") !== null, + `the confirm card must survive the fix (register=${shown.register}, got ${findByClass(shown.card, "guide-confirm") === null ? "the guidance card back" : "the confirm card"})`, + ); + assert.equal(controller.guidancePhase, "confirm", "the confirm phase holds across the fix"); + findByClass(shown.card, "btn-primary").click(); // YES, ABANDON — still reachable + assert.equal(deps.__guide.watchStopped, 1, "the teardown still runs from the surviving card"); + assert.equal(controller.guidancePhase, "ended", "the trip ended through the guarded surface"); + }); +}); + test("G8 arrival — the machine's arrival effect runs the same teardown", async () => { await withDocumentAsync(async () => { const deps = steppedDeps();