|
|
|
@@ -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"],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|