Files
AutistMask/tests/persistedFieldContract.test.js
T
sneak 9c40142d8f
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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
2026-10-07 09:00:35 +00:00

983 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// What the floor under each persisted field actually guarantees — as a table
// that RUNS, one row per field.
//
// This file replaces a hand-written per-field justification in the header of
// src/shared/stateSchema.js. That comment shipped a false claim in three
// consecutive changes: every author wrote plausible prose about thirty fields,
// every reviewer re-derived it by hand, and it kept being wrong in a different
// place each time. The artifact was the problem. A claim nobody can execute is
// worse than no claim, because it is believed.
//
// So the claim is a row here instead:
//
// KIND.REFUSED assertStateUsable() refuses the record outright. Proven by
// stateProblem() naming a problem for every hostile value.
// KIND.ENTRIES normalizePersisted() floors the container AND its entries.
// Proven by holds() over the normalized value.
// KIND.SCALAR normalizePersisted() floors it to one scalar type, or to a
// fixed fallback. Proven the same way.
// KIND.LOOSE `saved.x || default`, no type check at all. The claim is
// that no structural dereference of it is reachable from a
// stored record — which cannot be argued, only driven, so the
// proof is a boot of the REAL popup entry point over a stored
// record carrying the hostile value, ONTO EVERY RESTORABLE
// VIEW. Home is not where this class of defect lives.
//
// Every row is driven through a boot regardless of kind, but only a LOOSE row
// (or a row that sets `alsoSweep`) is swept across the restore path: that is
// what declaring LOOSE costs. ENTRIES and SCALAR rows are proven by their
// holds() instead, because a floored value is not hostile by the time a
// renderer sees it. A LOOSE row must additionally prove it is loose: if
// someone floors the field — even partially — and leaves the row saying LOOSE,
// the "survives verbatim" assertion fails. A field added to PERSISTED_FIELDS
// with no row fails the first test in the file.
//
// The sweep is what makes a LOOSE row falsifiable, so read how it is driven
// before trusting it. A row the ROUTER reads (`routes`) gets its own boot per
// view, because a hostile value in it legitimately changes which view renders.
// Every other swept field is corrupted on the SAME boot, one boot per view per
// slot, and that boot has to land on the view it stored — so a field that does
// move the routing cannot hide in the crowd. Every swept field is driven at
// BOTH POLARITIES: a value nothing in src/ writes is a wrong-typed one and so
// always truthy, which leaves `if (!state.x) { state.y.deref() }` unentered on
// the very boot that corrupts x. The last slot is the falsy one for that
// reason, and a field that cannot be falsy after the floor says so in its row
// and is proven so.
//
// READ THE CLAIM NARROWLY. What this file proves is: NO STRUCTURAL
// DEREFERENCE ON THE CODE PATHS A WHOLLY-CORRUPTED PROFILE TAKES. That is not
// 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. 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 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
// out of scope — proving no field is dereferenced on any reachable render path
// is exhaustive verification of the popup, not a floor under a stored record.
//
// The three claims this replaced, all false, all caught here by construction:
// rpcUrl reaching `new JsonRpcProvider()` (a synchronous throw, not a caught
// request); viewData's ENTRIES being dereferenced by four restore branches
// that gate on one truthy field each; and selectedWallet, where a stale
// integer index is the SAFE case and `wallets["map"]` is the throwing one.
const {
PERSISTED_FIELDS,
normalizePersisted,
} = require("../src/shared/persistedState");
const { stateProblem } = require("../src/shared/stateSchema");
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
ADDRESS,
TOKEN_ADDRESS,
} = require("./support/popupBoot");
const KIND = {
REFUSED: "refused by the gate",
ENTRIES: "container and entries type-checked",
SCALAR: "scalar type-checked",
LOOSE: "loosely floored; safety proven by driving the popup",
};
const isText = (v) => typeof v === "string";
const isRecord = (v) =>
typeof v === "object" && v !== null && !Array.isArray(v);
const isIndexOrNull = (v) => v === null || (Number.isInteger(v) && v >= 0);
const isTextOrNull = (v) => v === null || (isText(v) && v !== "");
const everyEntry = (v, fn) => Array.isArray(v) && v.every(fn);
// A row is SWEPT — driven onto every restorable view rather than only onto
// Home — when its claim is that no restore path dereferences the field. That
// is what LOOSE means. The two index rows opt in with `alsoSweep` although
// they are floored, because the restore path is precisely why they gained a
// floor and the sweep is the regression guard on it.
const swept = (row) => row.kind === KIND.LOOSE || Boolean(row.alsoSweep);
// Every value a swept row drives through a boot: the hostile set, plus the
// falsy slot that gives the field its other polarity. `hostile` values are all
// TRUTHY by nature — a value nothing in src/ writes is a wrong-typed one, and
// wrong-typed values are objects, non-empty strings and non-zero numbers. A
// field that is only ever truthy on the boot that corrupts it cannot falsify
// `if (!state.x) { state.y.deref() }`, so the falsy slot is not optional.
const sweptValues = (row) => [...row.hostile, ...(row.falsy || [])];
// ------------------------------------------------------------------ the table
//
// `hostile` is values a stored record can carry that nothing in src/ ever
// writes. Each one is driven through the floor AND through a real popup boot —
// and, for a swept row, through one boot per restorable view — so keep the
// 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. 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
// DEFAULT_STATE default, which is the branch every ordinary install takes —
// and that is the point: without it, a dereference behind `if (!state.x)` is
// unreachable on the one boot that corrupts x. A swept row that cannot supply
// one says `neverFalsy` instead, which is proven rather than asserted: every
// 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",
kind: KIND.REFUSED,
hostile: [42, "notastructure", { a: 1 }, [null], [{ addresses: 1 }]],
},
{
field: "networkId",
kind: KIND.REFUSED,
hostile: [42, "notanetwork", { a: 1 }, "__proto__"],
},
{
field: "trackedTokens",
kind: KIND.ENTRIES,
hostile: [42, "notalist", { a: 1 }],
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
},
{
field: "allowedSites",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { [ADDRESS]: "notalist" }],
floorOnly: [
[ADDRESS],
{ [ADDRESS]: 42 },
{ [ADDRESS]: [42, null, {}] },
JSON.parse('{"__proto__":["https://evil.invalid"]}'),
],
holds: siteMapHolds,
},
{
field: "deniedSites",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { [ADDRESS]: "notalist" }],
floorOnly: [
[ADDRESS],
{ [ADDRESS]: 42 },
{ [ADDRESS]: [42, null, {}] },
JSON.parse('{"__proto__":["https://evil.invalid"]}'),
],
holds: siteMapHolds,
},
{
field: "fraudContracts",
kind: KIND.ENTRIES,
hostile: [42, "notalist", { a: 1 }],
floorOnly: [[42], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, isText),
},
{
field: "viewStack",
kind: KIND.ENTRIES,
hostile: [42, "notalist", ["main", "show-phrase", "settings"]],
floorOnly: [[1, 2], [null], [{}], ["export-privkey"]],
// Truncated at the first entry the popup will not reopen onto, rather
// than filtered: every surviving entry's Back target has to stay the
// one it had. restorableStack() may also substitute ["main"] under a
// view restored below the root, so this is the one ENTRIES field whose
// result is not always a subset of what was stored.
holds: (v) => everyEntry(v, (e) => RESTORABLE_VIEWS.has(e)),
},
{
field: "networkEndpoints",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { mainnet: "notapair" }],
floorOnly: [
[1, 2],
{ mainnet: { rpcUrl: 42, blockscoutUrl: {} } },
{ mainnet: { rpcUrl: "", blockscoutUrl: [] } },
{ sepolia: 42 },
],
// Entries are coerced rather than dropped: an unknown network id is
// KEPT, so a profile that has been on a build with more networks does
// not lose their endpoints here. What is floored is the two URL fields
// inside the pair, which applyChainSwitchFields() assigns straight onto
// s.rpcUrl / s.blockscoutUrl on the next switch.
holds: (v) =>
isRecord(v) &&
Object.keys(v).every((id) => {
const pair = v[id];
return (
isRecord(pair) &&
(pair.rpcUrl === undefined ||
(isText(pair.rpcUrl) && pair.rpcUrl !== "")) &&
(pair.blockscoutUrl === undefined ||
(isText(pair.blockscoutUrl) &&
pair.blockscoutUrl !== ""))
);
}),
},
{
field: "rpcUrl",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[], "", null],
holds: (v) => isText(v) && v !== "",
// The claim this row replaced said a bad value "fails the request on a
// path that already catches". It does not: getProvider() hands rpcUrl
// to `new JsonRpcProvider()`, which throws SYNCHRONOUSLY, from two call
// sites outside any try — and a stored `currentView: "wait-tx"` reaches
// one of them through restoreView(). So the row proves the claim
// against the real constructor rather than describing it.
alsoProven: (normalized, hostile) => {
// requireActual: bootPopup() mocks this module out for the boots
// above, and a mocked getProvider() would prove nothing at all
// about the constructor this row is a claim about.
const { getProvider } = jest.requireActual(
"../src/shared/balances",
);
expect(() => getProvider(hostile, "mainnet")).toThrow();
const provider = getProvider(normalized, "mainnet");
expect(provider).toBeTruthy();
provider.destroy();
},
},
{
field: "blockscoutUrl",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[], "", null],
holds: (v) => isText(v) && v !== "",
},
{
field: "activeAddress",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[ADDRESS], ""],
holds: isTextOrNull,
},
{
field: "selectedToken",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[TOKEN_ADDRESS], ""],
holds: isTextOrNull,
},
{
field: "selectedWallet",
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 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,
// 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,
// 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,
},
{
field: "currentView",
kind: KIND.LOOSE,
routes: true,
// Compared, and concatenated into the debug banner's textContent
// (src/popup/views/helpers.js) with no gate in front of it, which
// coerces. Nothing renders FROM it without RESTORABLE_VIEWS.has()
// first, and Set.has() answers false for any value.
hostile: [42, "no-such-view", { a: 1 }],
// `saved.currentView || null`: the falsy polarity is the popup landing
// on Home, which every boot in "booting onto Home" below also drives.
falsy: [""],
},
{
field: "viewData",
kind: KIND.LOOSE,
routes: true,
// The container is taken verbatim; what makes its ENTRIES safe is the
// per-branch guard in src/popup/viewRouter.js. The sweep drives the
// 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. 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
// drive.
neverFalsy: true,
hostileRestore: [
// success-tx passes on `data.hash`, and renderSuccess() then calls
// toAddressHtml(d.to) -> addressTitle() -> address.toLowerCase().
{
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: {
hash: "0x1",
to: ADDRESS,
decoded: { details: [{ address: 42 }] },
},
views: ["success-tx"],
restored: false,
},
// error-tx passes on `data.message`, same dereference.
{
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"],
restored: false,
},
{
value: {
tx: {
hash: "0x1",
from: ADDRESS,
to: ADDRESS,
contractAddress: 42,
},
},
views: ["transaction"],
restored: false,
},
// confirm-tx passes on `data.pendingTx`.
{
value: { pendingTx: { amount: "1" } },
views: ["confirm-tx"],
restored: false,
},
{
value: {
pendingTx: {
token: 42,
from: ADDRESS,
to: ADDRESS,
amount: "1",
},
},
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
// regression guard.
{
value: {
pendingWait: {
hash: "0x1",
txInfo: { to: 42, amount: "1" },
},
},
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. 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",
],
),
],
},
{
field: "lastBalanceRefresh",
kind: KIND.LOOSE,
// Arithmetic only: `now - (s.lastBalanceRefresh || 0)` compares false
// for a non-number and forces a refresh.
hostile: [true, "notatime", { a: 1 }],
// `|| 0` collapses every falsy stored value to 0, so 0 IS the whole
// falsy polarity of this field — and it is the DEFAULT_STATE default,
// the value a profile carries until its first refresh lands.
falsy: [0],
},
{
field: "tokenHolderCache",
kind: KIND.LOOSE,
// Nothing DEREFERENCES it structurally. It is read by the
// field-agnostic snapshotPersisted()/deepEqual() in
// src/shared/state.js, which are safe for any value, and otherwise
// only reset wholesale in src/shared/chainSwitchFields.js.
hostile: [42, "notarecord", [1, 2]],
// `structuredClone(saved.tokenHolderCache || {})`.
neverFalsy: true,
},
{
field: "theme",
kind: KIND.LOOSE,
// Compared against "dark"/"light" in applyTheme() and otherwise falls
// to the system branch; assigned into an input .value, which coerces.
hostile: [42, "chartreuse", { a: 1 }],
// `saved.theme || "system"`.
neverFalsy: true,
},
{
field: "dustThresholdGwei",
kind: KIND.LOOSE,
hostile: ["notanumber", true, { a: 1 }],
// Survives verbatim, so the falsy slot is also wrong-typed: "" reaches
// filterTransactions() as a comparand and a settings input .value.
falsy: [""],
},
...[
"rememberSiteChoice",
"showZeroBalanceTokens",
"hideSpoofedSymbols",
"hideLowHolderTokens",
"hideFraudContracts",
"hideDustTransactions",
"utcTimestamps",
"debugMode",
].map((field) => ({
field,
kind: KIND.LOOSE,
// A flag: only ever tested for truthiness, and written back verbatim.
hostile: [42, "notabool", { a: 1 }],
// Both answers to that truthiness test have to be driven, and 0 is a
// value src/ never writes for a flag. For utcTimestamps and debugMode
// the falsy answer is also the DEFAULT_STATE default.
falsy: [0],
})),
];
function siteMapHolds(v) {
return (
isRecord(v) &&
Object.getPrototypeOf(v) === Object.prototype &&
!Object.prototype.hasOwnProperty.call(v, "__proto__") &&
Object.keys(v).every((key) => everyEntry(v[key], isText))
);
}
afterEach(() => {
cleanupPopup();
});
// -------------------------------------------------------------- exhaustive
describe("the contract covers the record", () => {
test("every persisted field has exactly one row, and no row invents one", () => {
const rows = CONTRACT.map((row) => row.field);
expect([...rows].sort()).toEqual([...PERSISTED_FIELDS].sort());
});
test("every row declares a kind this file knows how to prove", () => {
const kinds = Object.values(KIND);
for (const row of CONTRACT) {
expect(kinds).toContain(row.kind);
expect(row.hostile.length).toBeGreaterThan(0);
}
});
});
// ------------------------------------------------------------- the floors
function profileWith(field, value) {
return unversionedValidProfile({ [field]: value });
}
describe("the floor each row claims", () => {
// The falsy slot is deliberately NOT in here. `saved.x || default` is a
// floor on falsy values and on nothing else, so a falsy value is the one
// thing a LOOSE field need not carry through verbatim; what it has to carry
// through is being falsy, which "both polarities" below asserts.
for (const row of CONTRACT) {
const values = [...row.hostile, ...(row.floorOnly || [])];
if (row.kind === KIND.REFUSED) {
test(`${row.field}: the gate refuses it`, () => {
for (const value of values) {
expect(
typeof stateProblem(profileWith(row.field, value)),
).toBe("string");
}
});
continue;
}
test(`${row.field}: ${row.kind}`, () => {
for (const value of values) {
const out = normalizePersisted(profileWith(row.field, value));
if (row.kind === KIND.LOOSE) {
// The claim IS that there is no floor. A field that grows
// one has to move to another kind rather than keep a row
// saying its readers are what make it safe.
continue;
}
expect({
value: value,
holds: row.holds(out[row.field]),
}).toEqual({ value: value, holds: true });
}
});
if (row.kind === KIND.LOOSE) {
test(`${row.field}: is genuinely unfloored`, () => {
// EVERY value, not some: a PARTIAL floor is still a floor, and
// a row that keeps saying LOOSE because one hostile value out
// of three still survives is exactly the stale claim this file
// exists to stop.
for (const value of values) {
const out = normalizePersisted(
profileWith(row.field, value),
);
expect({
value: value,
survived: JSON.stringify(out[row.field]),
}).toEqual({
value: value,
survived: JSON.stringify(value),
});
}
});
}
}
});
// --------------------------------------------- driving the real popup boot
// A booted popup is healthy when nothing threw out of init() and something is
// on screen. A throw out of restoreView() is neither: init() does not guard it,
// so the rest of popup init never runs and the user gets a popup with no view,
// no message and no control on it.
async function bootHealth(profile) {
const env = await bootPopup(profile);
return {
errors: env.pageErrors,
blank: env.visibleViews().length === 0,
};
}
const HEALTHY = { errors: [], blank: false };
// unversionedValidProfile() stores no currentView, so every boot in here lands
// on Home. That is the cheap half of the proof; the restore path below is the
// half that matters.
// Both polarities of every swept field are driven, or the field is proven
// unable to take one of them. This is the guard on the sweep itself: a hostile
// 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) =>
normalizePersisted(profileWith(field, value))[field];
for (const row of CONTRACT) {
if (!swept(row)) continue;
if (row.neverFalsy) {
test(`${row.field}: cannot be falsy in state at all`, () => {
for (const value of FALSY_STORED) {
expect({
stored: value,
truthy: Boolean(floored(row.field, value)),
}).toEqual({ stored: value, truthy: true });
}
});
continue;
}
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).map((value) =>
floored(row.field, value),
);
expect({
truthy: driven.some((value) => Boolean(value)),
falsy: driven.some((value) => !value),
}).toEqual({ truthy: true, falsy: true });
});
}
});
describe("a hostile value for one field, booting onto Home", () => {
for (const row of CONTRACT) {
for (const value of sweptValues(row)) {
test(`${row.field} = ${JSON.stringify(value)}`, async () => {
await expect(
bootHealth(profileWith(row.field, value)),
).resolves.toEqual(HEALTHY);
});
}
}
});
describe("a row's extra proof against the real reader", () => {
for (const row of CONTRACT) {
if (!row.alsoProven) continue;
test(row.field, () => {
for (const value of row.hostile) {
const out = normalizePersisted(profileWith(row.field, value));
row.alsoProven(out[row.field], value);
}
});
}
});
// ------------------------------------------------ driving the restore path
// Everything above lands on Home. Home is not where this class of defect
// lives: all three of the false claims this file replaced were falsified by a
// RESTORE, through the unguarded restoreView() in src/popup/index.js. So a
// swept row's hostile values are driven onto EVERY restorable view, one boot
// each.
//
// This is what makes a LOOSE row falsifiable. A field that gains a structural
// dereference on any restorable view — `state.theme.toLowerCase()` in a view's
// show(), say — turns the row red here, instead of waiting for a reviewer to
// re-derive the claim by hand.
// restoreWait() resumes from this, so it has to be a finite number and recent
// enough that the resumed deadline has not already passed — a wait that has
// outlived its deadline resolves on the first poll instead of staying on
// screen. Read once at module load, so every boot in one run shares it.
const BROADCAST_TIME = Date.now();
// A viewData well formed for every restorable branch at once, so the only
// thing a swept boot can fail on is the field the row corrupts. "the base
// profile the sweep corrupts" below proves this really does render each view
// rather than falling back — without that, a sweep could pass by never
// reaching a renderer at all.
const WELL_FORMED_DATA = {
hash: "0x1",
message: "boom",
to: ADDRESS,
decoded: { details: [{ address: TOKEN_ADDRESS }] },
tx: { hash: "0x1", from: ADDRESS, to: ADDRESS, contractAddress: null },
pendingTx: {
token: "ETH",
from: ADDRESS,
to: ADDRESS,
amount: "1",
balance: "2",
},
pendingWait: {
hash: "0x1",
txInfo: { to: ADDRESS, amount: "1" },
broadcastTime: BROADCAST_TIME,
},
};
function restoringOnto(view, extra) {
return unversionedValidProfile({
currentView: view,
selectedWallet: 0,
selectedAddress: 0,
selectedToken: TOKEN_ADDRESS,
viewStack: ["main"],
viewData: WELL_FORMED_DATA,
...extra,
});
}
// A boot that RESTORED is healthy and landed on the view it stored, rather
// than falling back to Home — which a healthy boot also does, and which would
// let a sweep pass by never running the renderer it is aimed at.
async function restoredHealth(profile, view) {
const env = await bootPopup(profile);
return {
errors: env.pageErrors,
restored: env.visibleViews().includes(view),
};
}
const RESTORED = { errors: [], restored: true };
describe("the base profile the sweep corrupts", () => {
for (const view of RESTORABLE_VIEWS) {
test(`renders ${view} rather than falling back`, async () => {
await expect(
restoredHealth(restoringOnto(view), view),
).resolves.toEqual(RESTORED);
});
}
});
// Home, AddressDetail and AddressToken load their transactions inside a catch
// that only logs, so a boot that fails there still renders the view and passes
// the tests above while none of that code runs.
describe("the base profile loads transactions", () => {
for (const view of ["main", "address", "address-token"]) {
test(`on ${view} without logging a failure`, async () => {
const consoleError = jest.spyOn(console, "error");
try {
await bootPopup(restoringOnto(view));
const failures = consoleError.mock.calls
.map((args) => args.join(" "))
.filter((line) =>
/loadHomeTxs failed|loadTransactions failed/.test(line),
);
expect(failures).toEqual([]);
} finally {
consoleError.mockRestore();
}
});
}
});
// A field the ROUTER itself reads — the two it gates on and the two
// hasValidAddress() indexes with. A hostile value in one of these legitimately
// changes which view renders, so each gets its own boot per view and is held
// only to "healthy", not to "restored onto the view it stored".
const routes = (row) => Boolean(row.routes);
// Every routing row × every hostile value × every restorable view. Profiles
// are deduplicated because a hostile `currentView` REPLACES the view being
// restored onto, which would otherwise be the same boot eleven times.
describe("a hostile routing value restoring onto", () => {
for (const row of CONTRACT) {
if (!swept(row) || !routes(row)) continue;
const seen = new Set();
for (const value of sweptValues(row)) {
for (const view of RESTORABLE_VIEWS) {
const profile = restoringOnto(view, { [row.field]: value });
const key = JSON.stringify(profile);
if (seen.has(key)) continue;
seen.add(key);
test(`${view}: ${row.field} = ${JSON.stringify(
value,
)}`, async () => {
await expect(bootHealth(profile)).resolves.toEqual(HEALTHY);
});
}
}
}
});
// Every OTHER swept field, corrupted at once, one boot per view per hostile
// slot: twelve fields on one boot rather than twelve boots. A field is only in
// here because it is not one the router reads — and that is ASSERTED, not
// argued, because the boot has to land on `view`. A field that does move the
// routing turns this red and has to declare `routes` and take the individual
// sweep above.
//
// Combining hides one thing, and the last slot is what stops it. A hostile
// value is wrong-typed and therefore TRUTHY, so on a boot where every swept
// field is hostile, no `if (!state.x)` branch is entered — and a dereference
// inside such a branch would go unseen however loudly it throws. The last slot
// is the falsy one: every swept field that CAN be falsy is falsy on it, which
// is also the state an ordinary install boots in for three of them, while the
// fields that cannot be falsy stay hostile-truthy. That makes it a MIX, and a
// deliberate one — the interaction between a falsy flag and a still-hostile
// theme is a shape a stored record really produces.
//
// The verdict is the combined boot, always. When it goes red the same view is
// re-booted one field at a time, so the failure NAMES a culprit instead of
// leaving a reader to bisect twelve fields — but that loop only decorates the
// message. It cannot clear the failure. A dereference that needs two corrupted
// fields at once is reproduced by neither field alone, and a version of this
// file that asserted on the named list reported exactly that case green while
// watching the popup die.
const UNROUTED = CONTRACT.filter((row) => swept(row) && !routes(row));
const HOSTILE_SLOTS = Math.max(
...UNROUTED.map((row) => sweptValues(row).length),
);
function unroutedValues(slot) {
const fields = {};
for (const row of UNROUTED) {
const values = sweptValues(row);
fields[row.field] = values[slot % values.length];
}
return fields;
}
describe("every field the router does not read, corrupted at once, onto", () => {
for (const view of RESTORABLE_VIEWS) {
for (let slot = 0; slot < HOSTILE_SLOTS; slot++) {
test(`${view}: hostile value ${slot + 1} in all ${
UNROUTED.length
} of them`, async () => {
const fields = unroutedValues(slot);
const together = await restoredHealth(
restoringOnto(view, fields),
view,
);
// The per-field re-boot only DECORATES the message. The
// verdict is `together`, unconditionally: a dereference that
// needs two corrupted fields at once is reproduced by NEITHER
// field alone, so an assertion on the named list would report
// an observed dead popup as green.
const named = [];
if (together.errors.length > 0 || !together.restored) {
for (const row of UNROUTED) {
const one = await restoredHealth(
restoringOnto(view, {
[row.field]: fields[row.field],
}),
view,
);
if (one.errors.length === 0 && one.restored) continue;
named.push(
`${row.field}=${JSON.stringify(
fields[row.field],
)}: ` +
(one.errors.join("; ") || `fell off ${view}`),
);
}
if (named.length === 0) {
named.push(
"no single field reproduces it; it takes two or " +
`more of ${JSON.stringify(fields)}`,
);
}
}
expect({
view: view,
together: together,
fields: named,
}).toEqual({ view: view, together: RESTORED, fields: [] });
});
}
}
});
// 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 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) {
test(`${view}: ${row.field} = ${JSON.stringify(
entry.value,
)}`, async () => {
const env = await bootPopup(
restoringOnto(view, { [row.field]: entry.value }),
);
expect({
errors: env.pageErrors,
visible: env.visibleViews(),
}).toEqual({
errors: [],
visible: [entry.restored ? view : "main"],
});
});
}
}
}
});