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
This commit is contained in:
Codex
2026-09-10 15:16:12 +03:00
parent 5acf04fa33
commit be6d57e96f
2 changed files with 21 additions and 0 deletions
+7
View File
@@ -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 });
+14
View File
@@ -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',