From bb60b399eca06a915242ab92ff5107565d14c452 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Wed, 7 Oct 2026 11:59:14 +0200 Subject: [PATCH] test: tighten two checks in the persisted-field harness (closes #379) The polarity check now counts only `hostile` and `falsy` values, not a `hostileRestore` value, which reaches only the views its entry names. The stale index 5 moves into `hostile` for `selectedWallet` and `selectedAddress`, as each field's one value still truthy after the floor. Each `hostileRestore` entry declares whether its boot lands on its view or falls back to Home, and the test checks the one view on screen. A value driven onto every restorable view lists only the views it falls back on, so a view added later is driven by default. The header and the README restate the remaining limits as built and say which boots are held to where the popup lands. Model: opus-5-5 --- README.md | 23 ++-- TODO.md | 14 ++ tests/persistedFieldContract.test.js | 192 ++++++++++++++++++++------- 3 files changed, 173 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index bd10903..1854836 100644 --- a/README.md +++ b/README.md @@ -1263,14 +1263,21 @@ path rather than on the home screen. Read the claim narrowly, as that file states it: what those boots prove is no structural dereference on the code paths a WHOLLY-CORRUPTED PROFILE takes, which is not every path a stored record takes. Not driven: any pairing of values the four slots do not produce, a view only -forward navigation opens, anything behind a click, and everything a healthy -profile reaches. Within that boundary the verdict is unconditional — if one of -those boots leaves the popup unhealthy or off the view it stored, `make check` -fails, including when it takes two corrupted fields at once, because the verdict -is the combined boot and the per-field re-boot that names a culprit can only -decorate the message. So does a field that gains a floor while its row still -claims it has none, and so does a field added to `PERSISTED_FIELDS` with no row -at all. The per-field justification that used to live in the header of +forward navigation opens, anything behind a click or a timer, and, of what a +healthy profile reaches, anything beyond its boot onto each view the popup can +reopen onto. The fields the router does not read share a slot on each boot, so +one of them truthy while another is falsy is reached only where the falsy slot +pairs a field that cannot be falsy with one that is. Within that boundary the +verdict is unconditional — if one of those boots leaves the popup unhealthy, +`make check` fails, including when it takes two corrupted fields at once, +because the verdict is the combined boot and the per-field re-boot that names a +culprit can only decorate the message. The combined boot must also land on the +view it stored, and each value driven only onto the restore path must land on +its view, or fall back to Home, as its row declares; a field the router reads +can legitimately change which view renders, so its own sweep is held to health +alone. `make check` also fails on a field that gains a floor while its row still +claims it has none, and on a field added to `PERSISTED_FIELDS` with no row at +all. The per-field justification that used to live in the header of `src/shared/stateSchema.js` shipped a false claim in three consecutive changes, each caught only by a reviewer re-deriving thirty fields by hand. diff --git a/TODO.md b/TODO.md index 1612ee4..e2d5100 100644 --- a/TODO.md +++ b/TODO.md @@ -44,6 +44,20 @@ then continue tagging as milestones land. # Completed Steps +- 2026-10-07: Two holes in what `tests/persistedFieldContract.test.js` checks + are closed ([#379](https://git.eeqj.de/sneak/AutistMask/issues/379)). The + check that every swept field is driven both truthy and falsy counts only + `hostile` and `falsy` values, which the sweep drives onto every restorable + view, and no longer a `hostileRestore` value, which reaches only the views its + entry names; the stale index 5 moves into `hostile` for `selectedWallet` and + `selectedAddress`, as the one value of either still truthy after the floor. + Each `hostileRestore` entry declares whether its boot lands on its view or + falls back to Home, and is held to it. The limits that remain, fields that + share a slot on one boot and anything no stored record reaches by itself, are + restated as built in the file's header and the README, which now also say + which boots are held to where the popup lands and that a healthy profile is + booted onto every restorable view. + - 2026-10-07: Every password error in the popup is shown the same way ([#493](https://git.eeqj.de/sneak/AutistMask/issues/493)): with `showError()` and `hideError()` in a fixed-height error line below the password field, as diff --git a/tests/persistedFieldContract.test.js b/tests/persistedFieldContract.test.js index 091c35b..637d2ab 100644 --- a/tests/persistedFieldContract.test.js +++ b/tests/persistedFieldContract.test.js @@ -49,22 +49,37 @@ // every path a stored record takes, and the difference is the whole of what // this file does not cover: // -// - Only the values in the table, in the SLOT arrangement below: four value -// combinations per view, not the product of twelve fields. A dereference -// reached only under a pairing no slot produces is not driven at all. -// - Only what a stored record reaches by ITSELF. A view only forward -// navigation opens, and anything behind a click, is not driven. -// - Nothing about the paths a HEALTHY profile takes, which is most of the -// popup. This file is a floor under one defect class, not a proof about -// the renderers. +// - Only the values in the table, in the SLOT arrangement below. On the +// restore path the twelve fields the router does not read are corrupted +// together, every field on the same slot, so a view gets four value +// combinations of them, not their product. A branch entered only when one +// of them is truthy and another falsy is reached only where the falsy +// slot happens to pair a field that cannot be falsy with one that is. +// Each field the router reads is corrupted alone, over an otherwise +// well-formed record. +// - Only what a stored record reaches by ITSELF, as the boot renders it. A +// view only forward navigation opens, anything behind a click, and +// anything behind a timer (bootPopup() records every interval, and this +// file never runs one) is not driven. +// - Of the paths a HEALTHY profile takes, only its boot onto each +// restorable view ("the base profile the sweep corrupts" below). The rest +// of the popup is not covered: this file is a floor under one defect +// class, not a proof about the renderers. // -// Within that boundary it is unconditional: if one of these boots leaves the -// popup unhealthy or off the view it stored, this file goes red — including -// when it takes two corrupted fields at once, because the verdict is the -// combined boot itself and the per-field re-boot below can only decorate the -// message. That last part is the one thing an earlier version got wrong: it -// asserted on the per-field list, so an observed dead popup that no single -// field reproduced was reported green. +// Within that boundary it is unconditional: if a boot that corrupts a field +// leaves the popup unhealthy, this file goes red — including when it takes +// two corrupted fields at once, because the verdict is the combined boot +// itself and the per-field re-boot below can only decorate the message. That +// last part is the one thing an earlier version got wrong: it asserted on the +// per-field list, so an observed dead popup that no single field reproduced +// was reported green. +// +// Where the popup lands is held for some of those boots and not others. The +// combined boot must land on the view it stored, and each `hostileRestore` +// value must land on its view, or fall back to Home, as its entry declares. +// The boots in "a hostile routing value restoring onto" are held to health +// alone, because a value in a field the router reads legitimately changes +// which view renders; so are the boots onto Home, which store no view. // // Booting every field separately at every value would be several hundred boots // and most of the suite's budget; this is forty-four. Widening it further is @@ -128,7 +143,11 @@ const sweptValues = (row) => [...row.hostile, ...(row.falsy || [])]; // list short and pointed. `floorOnly` is extra values checked against the // floor alone, which is pure and free. `hostileRestore` is extra values driven // through the restore path only, for a value that means nothing until a -// particular branch's gate has let it past. +// particular branch's gate has let it past. Each of its entries names the +// `views` it is driven onto and declares whether the boot lands on them +// (`restored: true`) or falls back to Home (`restored: false`), so a value +// written for one renderer cannot stop reaching it unnoticed. A value driven +// onto every restorable view is written with everyRestorableView() below. // // `falsy` is the other POLARITY of a swept field, driven for the same reason. // It is not a value src/ never writes — for three of these fields it is the @@ -139,6 +158,23 @@ const sweptValues = (row) => [...row.hostile, ...(row.falsy || [])]; // falsy value stored under that field comes back TRUTHY from the floor, so no // `!state.x` branch is reachable from a stored record at all. +// The `hostileRestore` entries for a value driven onto every restorable view: +// it falls back to Home on the views listed in `fallsBackOn` and lands on every +// other one, so a view added to RESTORABLE_VIEWS is driven, and expected to +// land, without editing the row. +function everyRestorableView(value, fallsBackOn) { + return [ + { value, views: fallsBackOn, restored: false }, + { + value, + views: [...RESTORABLE_VIEWS].filter( + (view) => !fallsBackOn.includes(view), + ), + restored: true, + }, + ]; +} + const CONTRACT = [ { field: "wallets", @@ -280,28 +316,38 @@ const CONTRACT = [ kind: KIND.SCALAR, // The prototype members are the whole point: `wallets["map"]` is // TRUTHY, so hasValidAddress()'s `&&` does not short-circuit and - // `.addresses[…]` throws. A stale INTEGER is the safe case. - hostile: ["map", "__proto__", { a: 1 }], + // `.addresses[…]` throws. A stale INTEGER is the safe case and has to + // stay so. 5 is one, out of range for the one wallet in the profile, + // and it is also this field's truthy polarity: every other value here + // comes back from the floor as null. + hostile: ["map", "__proto__", { a: 1 }, 5], floorOnly: ["length", "constructor", "toString", "0", -1, 1.5, true], holds: isIndexOrNull, // SCALAR, and swept anyway: the restore path is precisely why this // field gained a floor, so the sweep is the regression guard on it. alsoSweep: true, routes: true, - // A stale INTEGER index, which reaches the restore path by a different - // route from the prototype members above — falsy or out of range - // rather than truthy — and has to keep being the safe case. - hostileRestore: [{ value: "length" }, { value: 5 }], + // Comes back from the floor as null, which hasValidAddress() reads as + // nothing selected: the popup falls back to Home on the five views + // that need an address, and lands on every other one. + hostileRestore: everyRestorableView("length", [ + "address", + "address-token", + "receive", + "transaction", + "confirm-tx", + ]), }, { field: "selectedAddress", kind: KIND.SCALAR, - hostile: ["map", "__proto__", { a: 1 }], + // 5 for the same reason as in selectedWallet: a stale index, and the + // one value here still truthy after the floor. + hostile: ["map", "__proto__", { a: 1 }, 5], floorOnly: ["length", "constructor", "toString", "0", -1, 1.5, true], holds: isIndexOrNull, alsoSweep: true, routes: true, - hostileRestore: [{ value: 5 }], }, { field: "currentView", @@ -325,7 +371,9 @@ const CONTRACT = [ // container shapes below onto every restorable view; hostileRestore // adds the records that PASS a branch's gate and then hand its // renderer something it dereferences, which is where the entries are - // actually decided. + // actually decided. The guard refuses each single-view record below, + // so each is declared to fall back to Home: one that started landing + // would be reaching the renderer it was written against. hostile: [42, "notarecord", { a: 1 }, [1, 2]], // `structuredClone(saved.viewData || {})`: the container is never falsy // in state whatever was stored, so no `!state.viewData` branch exists to @@ -334,11 +382,20 @@ const CONTRACT = [ hostileRestore: [ // success-tx passes on `data.hash`, and renderSuccess() then calls // toAddressHtml(d.to) -> addressTitle() -> address.toLowerCase(). - { value: { hash: "0x1" }, views: ["success-tx"] }, - { value: { hash: "0x1", to: 42 }, views: ["success-tx"] }, + { + value: { hash: "0x1" }, + views: ["success-tx"], + restored: false, + }, + { + value: { hash: "0x1", to: 42 }, + views: ["success-tx"], + restored: false, + }, { value: { hash: "0x1", to: ADDRESS, decoded: { details: 7 } }, views: ["success-tx"], + restored: false, }, { value: { @@ -347,12 +404,25 @@ const CONTRACT = [ decoded: { details: [{ address: 42 }] }, }, views: ["success-tx"], + restored: false, }, // error-tx passes on `data.message`, same dereference. - { value: { message: "boom" }, views: ["error-tx"] }, - { value: { message: "boom", to: 42 }, views: ["error-tx"] }, + { + value: { message: "boom" }, + views: ["error-tx"], + restored: false, + }, + { + value: { message: "boom", to: 42 }, + views: ["error-tx"], + restored: false, + }, // transaction passes on `data.tx`. - { value: { tx: { hash: "0x1" } }, views: ["transaction"] }, + { + value: { tx: { hash: "0x1" } }, + views: ["transaction"], + restored: false, + }, { value: { tx: { @@ -363,9 +433,14 @@ const CONTRACT = [ }, }, views: ["transaction"], + restored: false, }, // confirm-tx passes on `data.pendingTx`. - { value: { pendingTx: { amount: "1" } }, views: ["confirm-tx"] }, + { + value: { pendingTx: { amount: "1" } }, + views: ["confirm-tx"], + restored: false, + }, { value: { pendingTx: { @@ -376,6 +451,7 @@ const CONTRACT = [ }, }, views: ["confirm-tx"], + restored: false, }, // wait-tx passes on `pendingWait.hash`; restoreWait() has checked // the fields below it since it was written, and this is the @@ -388,20 +464,29 @@ const CONTRACT = [ }, }, views: ["wait-tx"], + restored: false, }, // A record that passes EVERY branch's gate at once, driven onto // every restorable view: a branch a view does not read must stay - // one it does not read, and each renderer must survive the fields - // another branch left behind. - { - value: { + // one it does not read. The five views with a viewData branch + // refuse it; every other one renders it, and must survive the + // fields every branch left behind. + ...everyRestorableView( + { hash: "0x1", message: "boom", tx: { hash: "0x1" }, pendingTx: { amount: "1" }, pendingWait: { hash: "0x1" }, }, - }, + [ + "confirm-tx", + "transaction", + "wait-tx", + "success-tx", + "error-tx", + ], + ), ], }, { @@ -583,6 +668,11 @@ const HEALTHY = { errors: [], blank: false }; // set is all-truthy by construction, so without a falsy slot a dereference // behind `if (!state.x)` is never reached on the boot that corrupts x — the // same falsy-collapse blind spot the fields below were floored for. +// +// Only `hostile` and `falsy` values count. Those go through the sweep below, +// which covers every restorable view; a `hostileRestore` entry is driven onto +// only the views it names, so a polarity it alone supplied might reach a single +// renderer. describe("both polarities of every swept field are driven", () => { const FALSY_STORED = [0, "", false, null]; const floored = (field, value) => @@ -606,10 +696,9 @@ describe("both polarities of every swept field are driven", () => { test(`${row.field}: truthy and falsy`, () => { // What the boots below actually drive, floored the way a renderer // sees it — not what the row says it drives. - const driven = [ - ...sweptValues(row), - ...(row.hostileRestore || []).map((entry) => entry.value), - ].map((value) => floored(row.field, value)); + const driven = sweptValues(row).map((value) => + floored(row.field, value), + ); expect({ truthy: driven.some((value) => Boolean(value)), @@ -865,20 +954,27 @@ describe("every field the router does not read, corrupted at once, onto", () => // The values that only mean something on the restore path: a viewData that // PASSES a branch's gate and then hands its renderer something dereferenced, -// and the index values whose route through hasValidAddress() differs from the -// row's own hostile set. +// and a selectedWallet the floor turns into nothing selected. Each boot must +// throw nothing and show exactly the view its entry says it lands on: its own, +// or Home, so a value written for one renderer cannot stop reaching it and +// still pass. describe("a restore-only hostile value onto", () => { for (const row of CONTRACT) { for (const entry of row.hostileRestore || []) { - for (const view of entry.views || RESTORABLE_VIEWS) { + for (const view of entry.views) { test(`${view}: ${row.field} = ${JSON.stringify( entry.value, )}`, async () => { - await expect( - bootHealth( - restoringOnto(view, { [row.field]: entry.value }), - ), - ).resolves.toEqual(HEALTHY); + const env = await bootPopup( + restoringOnto(view, { [row.field]: entry.value }), + ); + expect({ + errors: env.pageErrors, + visible: env.visibleViews(), + }).toEqual({ + errors: [], + visible: [entry.restored ? view : "main"], + }); }); } }