From be6d57e96f425ceefa7ce5888785ac522e277b26 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 10 Sep 2026 15:16:12 +0300 Subject: [PATCH] test(adoption): the post-write smoke resets its own pending writes Review r2 M1. The smoke's reset() left the previous scenario's debounced config/layout write pending; _deleteSpace flushes whatever is pending before it writes, the fake socket answers without a rev, and the documented rev+1 fallback then moved the revision on a body from another scenario. The refused branch looked as if it had adopted: onboardingDeleteRefusedAdoptsNothing was red on every run. The scenarios are supposed to be independent, so reset() now cancels both debounced writers, as smoke_danger_confirmation already does. 37/37 green, three runs in a row; with the cancel removed the same single check goes red again. The refused-tail fix from r1 had no witness in CI at all: no mutant named this smoke as its guard, so the review gate never ran it and a red witness survived a whole round. A witness that never runs is not a witness, so the early return in _undoPlanOptimization now has a mutant that names the smoke. Issue: #500 User-Visible: no --- demo/smoke_post_write_adoption.mjs | 7 +++++++ scripts/mutation-gate.mjs | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/demo/smoke_post_write_adoption.mjs b/demo/smoke_post_write_adoption.mjs index 1cd3f8a9..a322964f 100644 --- a/demo/smoke_post_write_adoption.mjs +++ b/demo/smoke_post_write_adoption.mjs @@ -80,6 +80,13 @@ const out = await page.evaluate(async () => { }; const reset = async (scenario) => { + // Ревью r2 M1: отложенная запись предыдущего сценария иначе доживает до + // следующего и флашится безусловным `if (pending) flush()` внутри + // `_deleteSpace`; мок отвечает `{}` без `rev`, запасной путь «rev + 1» + // двигает ревизию по ЧУЖОМУ телу, и отказная ветка выглядит принявшей. + // Сценарии обязаны быть независимы — так же чистится smoke_danger_confirmation. + card._saveConfigDebounced.cancel(); + card._persistLayout.cancel(); server = { config: freshConfig(), layout: {}, cfgRev: 10, layRev: 20 }; concurrentHref = `media-source://image/${scenario}-500`; adoption.restoreCached({ config: structuredClone(server.config), rev: 10, layout: {}, layout_rev: 20 }); diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 52baf458..7406b4f4 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -8555,6 +8555,20 @@ const MUTANT_DEFINITIONS = [ + ' || contentFingerprint(current) !== attempt.attemptedFingerprint) return false;', }], }, + { + id: 'post-write-tail-runs-on-refused-gate', + guard: 'node demo/smoke_post_write_adoption.mjs', + because: '#500 r1 M1: a post-write caller whose adoption was refused must skip its tail — ' + + 'clearing history, bumping the geometry epoch and toasting «отменено» on a body the card ' + + 'never adopted leaves the frontend describing state the server does not have; the scheduled ' + + 'reload owns the tail. Guarded by the smoke, so the review gate runs it (r2 M1: the witness ' + + 'was red for a round and nobody saw it — CI never ran this smoke)', + patches: [{ + file: 'src/houseplan-editor-runtime.ts', + find: " if (adopted.status !== 'adopted') return; // asset wait: the scheduled reload owns the tail", + replace: ' void adopted; // mutant: the tail runs on a body that was never adopted', + }], + }, { id: 'post-write-skips-asset-gate', guard: 'node --test test/config-adoption.test.mjs',