From ce07db748eb1ced499e3f9555a88d291b0005485 Mon Sep 17 00:00:00 2001 From: Matysh Date: Sat, 5 Sep 2026 22:59:28 +0300 Subject: [PATCH] Witness synchronous warm confirmation refusal Issue: #470 User-Visible: no --- demo/smoke_danger_confirm_branches.mjs | 22 ++++++++++++++-------- scripts/mutation-gate.mjs | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/demo/smoke_danger_confirm_branches.mjs b/demo/smoke_danger_confirm_branches.mjs index d81c4b59..3a9955e8 100644 --- a/demo/smoke_danger_confirm_branches.mjs +++ b/demo/smoke_danger_confirm_branches.mjs @@ -65,15 +65,20 @@ const warmGate = await page.evaluate(async () => { new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)), ]); await settle(); + const warmDecision = card._confirmDanger({ + key: 'warm-language-gate', + kind: 'destructive', + title: 'Delete?', + message: 'The plan and all its rooms will be deleted.', + confirmLabel: 'Delete', + cancelLabel: 'Cancel', + }); + // Refusal is a synchronous registration barrier, not eventual cleanup by + // the next warm render. A briefly pending request has no decision surface. + const warmRequestWasNeverRegistered = card._dangerConfirm === null + && card._dangerConfirmController.state === null; const decision = await Promise.race([ - card._confirmDanger({ - key: 'warm-language-gate', - kind: 'destructive', - title: 'Delete?', - message: 'The plan and all its rooms will be deleted.', - confirmLabel: 'Delete', - cancelLabel: 'Cancel', - }), + warmDecision, new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)), ]); const result = { @@ -81,6 +86,7 @@ const warmGate = await page.evaluate(async () => { readyConfirmationOpenedBeforeWarm: openedWhileReady, readyToWarmCancelsOpenConfirmation: cancelledOnTransition === false, readyToWarmKeepsCommittedBody: root().querySelector('ha-card')?.outerHTML === committedBody, + warmLanguageGateNeverRegisters: warmRequestWasNeverRegistered, warmLanguageGateRefusesImmediately: decision === false, warmLanguageGateKeepsControllerEmpty: card._dangerConfirm === null && card._dangerConfirmController.state === null, diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 041e9b9f..2c87450f 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -1623,8 +1623,8 @@ const MUTANT_DEFINITIONS = [ { id: 'danger-confirm-warm-language-guard-removed', guard: 'node demo/smoke_danger_confirm_branches.mjs', - because: 'the warm language branch returns Lit noChange, so a newly registered ' - + 'confirmation has no rendered decision source and hangs (#417 AC2)', + because: 'a request made while the language gate is already warm must be rejected ' + + 'synchronously without ever entering the confirmation controller (#417 AC2/AC3)', patches: [{ file: 'src/houseplan-card.ts', find: " if (this._syncDangerConfirmLocaleGate() === 'warm') return Promise.resolve(false);",