mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
refactor(folio): name the balance property for the bound it asserts
it stopped being an equality and became |delta| <= typed, so the old name demanded more than the property does. renamed with the ci gate's GATED_PROPERTIES in the same commit so the gate never sees a name it does not know.
This commit is contained in:
1 parent
6a7764eac7
commit
112d347432
8 files changed
+57
-57
No files matched your search
@@ -95,7 +95,7 @@ steps=0
|
|||||||
|
|
||||||
# The two properties that state folio's double-submit. Anything else the spec
|
# The two properties that state folio's double-submit. Anything else the spec
|
||||||
# proves false is a different finding, and this leg has nothing to say about it.
|
# proves false is a different finding, and this leg has nothing to say about it.
|
||||||
GATED_PROPERTIES="submitMovesBalanceByTypedAmount,submitCommitsOneTransactionPerAction"
|
GATED_PROPERTIES="submitMovesBalanceByAtMostTypedAmount,submitCommitsOneTransactionPerAction"
|
||||||
|
|
||||||
# Exit 2 means "the run recorded a violation", and that is NOT the same as "the
|
# Exit 2 means "the run recorded a violation", and that is NOT the same as "the
|
||||||
# run convicted folio". A predicate that THROWS is recorded as a violation too,
|
# run convicted folio". A predicate that THROWS is recorded as a violation too,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ SEED=9 MAX_STEPS=200 .github/scripts/folio-run.sh android
|
|||||||
|
|
||||||
**web and ios expect the bug.** Folio double-submits a transaction when the
|
**web and ios expect the bug.** Folio double-submits a transaction when the
|
||||||
submit button is double-tapped, and two properties catch it:
|
submit button is double-tapped, and two properties catch it:
|
||||||
`submitMovesBalanceByTypedAmount`, which demands the total balance move by
|
`submitMovesBalanceByAtMostTypedAmount`, which demands the total balance move by
|
||||||
exactly the amount typed, and `submitCommitsOneTransactionPerAction`, which
|
exactly the amount typed, and `submitCommitsOneTransactionPerAction`, which
|
||||||
demands no more transactions committed over a window than there were submit
|
demands no more transactions committed over a window than there were submit
|
||||||
actions in it. A double tap is one action committing two transactions, so it
|
actions in it. A double tap is one action committing two transactions, so it
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ You do not script the double tap. You state the invariant and let sanderling fin
|
|||||||
The amount the user types must equal the amount the balance moves:
|
The amount the user types must equal the amount the balance moves:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const submitMovesBalanceByTypedAmount = always(
|
const submitMovesBalanceByAtMostTypedAmount = always(
|
||||||
next(() => {
|
next(() => {
|
||||||
if (route.current !== "home") return true;
|
if (route.current !== "home") return true;
|
||||||
const action = lastAction.current;
|
const action = lastAction.current;
|
||||||
@@ -179,7 +179,7 @@ That is the whole input. Three invariants, a way in, and a weighted sense of whe
|
|||||||
|
|
||||||
## What the run does
|
## What the run does
|
||||||
|
|
||||||
sanderling launches Folio, logs in, and starts exploring. Most steps are unremarkable: open an account, add a transaction, watch the balance move by exactly what was typed, `submitMovesBalanceByTypedAmount` holds.
|
sanderling launches Folio, logs in, and starts exploring. Most steps are unremarkable: open an account, add a transaction, watch the balance move by exactly what was typed, `submitMovesBalanceByAtMostTypedAmount` holds.
|
||||||
|
|
||||||
Then a step lands two taps on submit before the first save settles. Two transactions post. The balance jumps by twice the typed amount. At that step the formula evaluates false and the run records a violation: the step, the screenshot, the offending action, and the residual formula that failed.
|
Then a step lands two taps on submit before the first save settles. Two transactions post. The balance jumps by twice the typed amount. At that step the formula evaluates false and the run records a violation: the step, the screenshot, the offending action, and the residual formula that failed.
|
||||||
|
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ export function committedTransactionsExceedSubmits(args: {
|
|||||||
// submit action can commit one transaction, so the account's balance cannot
|
// submit action can commit one transaction, so the account's balance cannot
|
||||||
// move by more than the amount that submit typed.
|
// move by more than the amount that submit typed.
|
||||||
//
|
//
|
||||||
// An UPPER BOUND, not the equality submitChangesBalanceByTypedAmount uses, and
|
// An UPPER BOUND, not the equality submitChangesBalanceByAtMostTypedAmount uses, and
|
||||||
// that is what makes a one-action window safe. A balance that has not moved is
|
// that is what makes a one-action window safe. A balance that has not moved is
|
||||||
// a commit still in flight (createTransaction runs in a coroutine), a submit
|
// a commit still in flight (createTransaction runs in a coroutine), a submit
|
||||||
// the app rejected, or a tap that never landed, and none of those is evidence
|
// the app rejected, or a tap that never landed, and none of those is evidence
|
||||||
@@ -621,7 +621,7 @@ export function committedTransactionsExceedSubmits(args: {
|
|||||||
// mirrors parseCents), so every rejected amount and every amount too large to
|
// mirrors parseCents), so every rejected amount and every amount too large to
|
||||||
// hold exactly arrives here as 0 and is vacuous.
|
// hold exactly arrives here as 0 and is vacuous.
|
||||||
//
|
//
|
||||||
// The float guards are the ones submitChangesBalanceByTypedAmount explains:
|
// The float guards are the ones submitChangesBalanceByAtMostTypedAmount explains:
|
||||||
// each balance and the typed amount lose precision on their own past
|
// each balance and the typed amount lose precision on their own past
|
||||||
// Number.MAX_SAFE_INTEGER. Their difference needs none, because a difference
|
// Number.MAX_SAFE_INTEGER. Their difference needs none, because a difference
|
||||||
// that is really within a safe typedAmount is itself safe and comes out exact.
|
// that is really within a safe typedAmount is itself safe and comes out exact.
|
||||||
@@ -723,7 +723,7 @@ export function parseTypedAmount(text: string | undefined | null): number {
|
|||||||
// evidence about the amount typed into any one submit, so anything other than
|
// evidence about the amount typed into any one submit, so anything other than
|
||||||
// exactly one submit action in the window is vacuous. Exactly one still catches
|
// exactly one submit action in the window is vacuous. Exactly one still catches
|
||||||
// the bug: the double-tap is a single action.
|
// the bug: the double-tap is a single action.
|
||||||
export function submitChangesBalanceByTypedAmount(args: {
|
export function submitChangesBalanceByAtMostTypedAmount(args: {
|
||||||
route: string | null;
|
route: string | null;
|
||||||
lastAction: ObservedAction | null;
|
lastAction: ObservedAction | null;
|
||||||
submitsInWindow: number;
|
submitsInWindow: number;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
readHomeCards,
|
readHomeCards,
|
||||||
readHomeTotalBalance,
|
readHomeTotalBalance,
|
||||||
routeOfFrame,
|
routeOfFrame,
|
||||||
submitChangesBalanceByTypedAmount,
|
submitChangesBalanceByAtMostTypedAmount,
|
||||||
} from "./predicates";
|
} from "./predicates";
|
||||||
import type { Account, CardReading, TxnCount } from "./predicates";
|
import type { Account, CardReading, TxnCount } from "./predicates";
|
||||||
|
|
||||||
@@ -264,12 +264,12 @@ const newAccountBalanceIsZero = always(
|
|||||||
// predicate skips off-Home landings where totalBalance.current is the carrier.
|
// predicate skips off-Home landings where totalBalance.current is the carrier.
|
||||||
//
|
//
|
||||||
// The name is the property's, and the gate keys on it, so it stays; what it
|
// The name is the property's, and the gate keys on it, so it stays; what it
|
||||||
// demands is the bound, for the reason submitChangesBalanceByTypedAmount gives:
|
// demands is the bound, for the reason submitChangesBalanceByAtMostTypedAmount gives:
|
||||||
// a total that has not re-rendered yet has not moved, and an equality convicts a
|
// a total that has not re-rendered yet has not moved, and an equality convicts a
|
||||||
// healthy app for a frame that has not caught up.
|
// healthy app for a frame that has not caught up.
|
||||||
const submitMovesBalanceByTypedAmount = always(
|
const submitMovesBalanceByAtMostTypedAmount = always(
|
||||||
next(() =>
|
next(() =>
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: route.current,
|
route: route.current,
|
||||||
lastAction: lastAction.current,
|
lastAction: lastAction.current,
|
||||||
submitsInWindow: submitsInWindow.current,
|
submitsInWindow: submitsInWindow.current,
|
||||||
@@ -367,7 +367,7 @@ const addTxn = whenRoute(route, ["home", "ledger", "add-transaction"], () => {
|
|||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
newAccountBalanceIsZero,
|
newAccountBalanceIsZero,
|
||||||
submitMovesBalanceByTypedAmount,
|
submitMovesBalanceByAtMostTypedAmount,
|
||||||
submitCommitsOneTransactionPerAction,
|
submitCommitsOneTransactionPerAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { test } from "node:test";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
parseTypedAmount,
|
parseTypedAmount,
|
||||||
submitChangesBalanceByTypedAmount,
|
submitChangesBalanceByAtMostTypedAmount,
|
||||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
} from "../../../examples/folio/sanderling/predicates.ts";
|
||||||
|
|
||||||
const submitOn = "testTag:LedgerScreen > testTag:TxnSubmit";
|
const submitOn = "testTag:LedgerScreen > testTag:TxnSubmit";
|
||||||
|
|
||||||
test("single submit: delta matches typed amount", () => {
|
test("single submit: delta matches typed amount", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -24,7 +24,7 @@ test("single submit: delta matches typed amount", () => {
|
|||||||
|
|
||||||
test("double submit: delta is twice the typed amount, fires", () => {
|
test("double submit: delta is twice the typed amount, fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -38,7 +38,7 @@ test("double submit: delta is twice the typed amount, fires", () => {
|
|||||||
|
|
||||||
test("DoubleTap kind also caught when delta exceeds typed amount", () => {
|
test("DoubleTap kind also caught when delta exceeds typed amount", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -52,7 +52,7 @@ test("DoubleTap kind also caught when delta exceeds typed amount", () => {
|
|||||||
|
|
||||||
test("wrong action kind: vacuous true even with mismatch", () => {
|
test("wrong action kind: vacuous true even with mismatch", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "InputText", on: submitOn, applied: true },
|
lastAction: { kind: "InputText", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -66,7 +66,7 @@ test("wrong action kind: vacuous true even with mismatch", () => {
|
|||||||
|
|
||||||
test("wrong target: vacuous true even with mismatch", () => {
|
test("wrong target: vacuous true even with mismatch", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: "testTag:LoginScreen > testTag:LoginSubmit", applied: true },
|
lastAction: { kind: "Tap", on: "testTag:LoginScreen > testTag:LoginSubmit", applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -80,7 +80,7 @@ test("wrong target: vacuous true even with mismatch", () => {
|
|||||||
|
|
||||||
test("null lastAction: vacuous true", () => {
|
test("null lastAction: vacuous true", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: null,
|
lastAction: null,
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -94,7 +94,7 @@ test("null lastAction: vacuous true", () => {
|
|||||||
|
|
||||||
test("zero typedAmount: vacuous true", () => {
|
test("zero typedAmount: vacuous true", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -108,7 +108,7 @@ test("zero typedAmount: vacuous true", () => {
|
|||||||
|
|
||||||
test("selector as object: coerced safely and TxnSubmit detected", () => {
|
test("selector as object: coerced safely and TxnSubmit detected", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: { testTag: "TxnSubmit" }, applied: true },
|
lastAction: { kind: "Tap", on: { testTag: "TxnSubmit" }, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -122,7 +122,7 @@ test("selector as object: coerced safely and TxnSubmit detected", () => {
|
|||||||
|
|
||||||
test("selector as object without TxnSubmit: vacuous true", () => {
|
test("selector as object without TxnSubmit: vacuous true", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: { testTag: "LoginSubmit" }, applied: true },
|
lastAction: { kind: "Tap", on: { testTag: "LoginSubmit" }, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -136,7 +136,7 @@ test("selector as object without TxnSubmit: vacuous true", () => {
|
|||||||
|
|
||||||
test("raw whole-dollar input: single submit clears", () => {
|
test("raw whole-dollar input: single submit clears", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -150,7 +150,7 @@ test("raw whole-dollar input: single submit clears", () => {
|
|||||||
|
|
||||||
test("raw whole-dollar input: double submit fires", () => {
|
test("raw whole-dollar input: double submit fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -164,7 +164,7 @@ test("raw whole-dollar input: double submit fires", () => {
|
|||||||
|
|
||||||
test("decimal input from empty prior balance clears", () => {
|
test("decimal input from empty prior balance clears", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -178,7 +178,7 @@ test("decimal input from empty prior balance clears", () => {
|
|||||||
|
|
||||||
test("DoubleTap kind with raw whole-dollar input fires", () => {
|
test("DoubleTap kind with raw whole-dollar input fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -192,7 +192,7 @@ test("DoubleTap kind with raw whole-dollar input fires", () => {
|
|||||||
|
|
||||||
test("route gate: ledger landing with stale carrier is skipped", () => {
|
test("route gate: ledger landing with stale carrier is skipped", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "ledger",
|
route: "ledger",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -206,7 +206,7 @@ test("route gate: ledger landing with stale carrier is skipped", () => {
|
|||||||
|
|
||||||
test("route gate: add-transaction landing with double-submit delta is skipped", () => {
|
test("route gate: add-transaction landing with double-submit delta is skipped", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "add-transaction",
|
route: "add-transaction",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -220,7 +220,7 @@ test("route gate: add-transaction landing with double-submit delta is skipped",
|
|||||||
|
|
||||||
test("route gate: null route is skipped", () => {
|
test("route gate: null route is skipped", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: null,
|
route: null,
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -234,7 +234,7 @@ test("route gate: null route is skipped", () => {
|
|||||||
|
|
||||||
test("route gate: home landing with matching delta passes", () => {
|
test("route gate: home landing with matching delta passes", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -248,7 +248,7 @@ test("route gate: home landing with matching delta passes", () => {
|
|||||||
|
|
||||||
test("route gate: home landing with double-insert delta fires", () => {
|
test("route gate: home landing with double-insert delta fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -273,7 +273,7 @@ test("above 2^53 the arithmetic itself is wrong, which is why the guard exists",
|
|||||||
|
|
||||||
test("above 2^53 a healthy single submit is not reported", () => {
|
test("above 2^53 a healthy single submit is not reported", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -287,7 +287,7 @@ test("above 2^53 a healthy single submit is not reported", () => {
|
|||||||
|
|
||||||
test("above 2^53 a double-submit delta is not reported either", () => {
|
test("above 2^53 a double-submit delta is not reported either", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -301,7 +301,7 @@ test("above 2^53 a double-submit delta is not reported either", () => {
|
|||||||
|
|
||||||
test("an unreadable previous balance above 2^53 is not evidence", () => {
|
test("an unreadable previous balance above 2^53 is not evidence", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -318,7 +318,7 @@ test("an unreadable previous balance above 2^53 is not evidence", () => {
|
|||||||
// and must not convict on one it cannot hold.
|
// and must not convict on one it cannot hold.
|
||||||
test("typed amount above 2^53 is not evidence", () => {
|
test("typed amount above 2^53 is not evidence", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -334,7 +334,7 @@ test("typed amount above 2^53 is not evidence", () => {
|
|||||||
// more is where counting stops being exact.
|
// more is where counting stops being exact.
|
||||||
test("boundary: a double submit landing exactly on MAX_SAFE_INTEGER still fires", () => {
|
test("boundary: a double submit landing exactly on MAX_SAFE_INTEGER still fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -348,7 +348,7 @@ test("boundary: a double submit landing exactly on MAX_SAFE_INTEGER still fires"
|
|||||||
|
|
||||||
test("boundary: a single submit landing exactly on MAX_SAFE_INTEGER passes", () => {
|
test("boundary: a single submit landing exactly on MAX_SAFE_INTEGER passes", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -362,7 +362,7 @@ test("boundary: a single submit landing exactly on MAX_SAFE_INTEGER passes", ()
|
|||||||
|
|
||||||
test("boundary: one cent past MAX_SAFE_INTEGER stops being evidence", () => {
|
test("boundary: one cent past MAX_SAFE_INTEGER stops being evidence", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -379,7 +379,7 @@ test("boundary: one cent past MAX_SAFE_INTEGER stops being evidence", () => {
|
|||||||
// typed amount, so a mismatch here is real and must still be reported.
|
// typed amount, so a mismatch here is real and must still be reported.
|
||||||
test("a large but exact difference between safe balances still fires", () => {
|
test("a large but exact difference between safe balances still fires", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -395,7 +395,7 @@ test("a large but exact difference between safe balances still fires", () => {
|
|||||||
// and the property must stay quiet rather than demand a 1e23-cent move.
|
// and the property must stay quiet rather than demand a 1e23-cent move.
|
||||||
test("21-digit typed amount with an unmoved balance is not a violation", () => {
|
test("21-digit typed amount with an unmoved balance is not a violation", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -415,7 +415,7 @@ test("21-digit typed amount with an unmoved balance is not a violation", () => {
|
|||||||
// and an unrelated 26200 credit.
|
// and an unrelated 26200 credit.
|
||||||
test("freshness: two submits in the window is vacuous, not a conviction", () => {
|
test("freshness: two submits in the window is vacuous, not a conviction", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 2,
|
submitsInWindow: 2,
|
||||||
@@ -429,7 +429,7 @@ test("freshness: two submits in the window is vacuous, not a conviction", () =>
|
|||||||
|
|
||||||
test("freshness: two submits cannot convict even on a clean 2x delta", () => {
|
test("freshness: two submits cannot convict even on a clean 2x delta", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 2,
|
submitsInWindow: 2,
|
||||||
@@ -446,7 +446,7 @@ test("freshness: two submits cannot convict even on a clean 2x delta", () => {
|
|||||||
// (nothing to attribute the move to), and two or more means the move is shared.
|
// (nothing to attribute the move to), and two or more means the move is shared.
|
||||||
test("freshness boundary: exactly one submit is the window that convicts", () => {
|
test("freshness boundary: exactly one submit is the window that convicts", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -460,7 +460,7 @@ test("freshness boundary: exactly one submit is the window that convicts", () =>
|
|||||||
|
|
||||||
test("freshness boundary: one submit with a healthy 1x delta still passes", () => {
|
test("freshness boundary: one submit with a healthy 1x delta still passes", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -474,7 +474,7 @@ test("freshness boundary: one submit with a healthy 1x delta still passes", () =
|
|||||||
|
|
||||||
test("freshness boundary: three submits is vacuous", () => {
|
test("freshness boundary: three submits is vacuous", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 3,
|
submitsInWindow: 3,
|
||||||
@@ -491,7 +491,7 @@ test("freshness boundary: three submits is vacuous", () => {
|
|||||||
// submit in it explains no balance move.
|
// submit in it explains no balance move.
|
||||||
test("freshness boundary: a window with no submit in it is vacuous", () => {
|
test("freshness boundary: a window with no submit in it is vacuous", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 0,
|
submitsInWindow: 0,
|
||||||
@@ -509,7 +509,7 @@ test("freshness boundary: a window with no submit in it is vacuous", () => {
|
|||||||
// did exactly what it should have.
|
// did exactly what it should have.
|
||||||
test("a submit the runner could not confirm demands no balance move", () => {
|
test("a submit the runner could not confirm demands no balance move", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: null },
|
lastAction: { kind: "Tap", on: submitOn, applied: null },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -528,7 +528,7 @@ test("a submit the runner could not confirm demands no balance move", () => {
|
|||||||
// and an equality reads that as the app having ignored the amount.
|
// and an equality reads that as the app having ignored the amount.
|
||||||
test("a commit the Home total has not caught up with is not a violation", () => {
|
test("a commit the Home total has not caught up with is not a violation", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -546,7 +546,7 @@ test("a commit the Home total has not caught up with is not a violation", () =>
|
|||||||
// that fires on both is not evidence about either.
|
// that fires on both is not evidence about either.
|
||||||
test("an under-move is no longer judged, which is the trade", () => {
|
test("an under-move is no longer judged, which is the trade", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -568,7 +568,7 @@ test("the measured double submit still fires under the bound", () => {
|
|||||||
[200000032904800, 200000032911200],
|
[200000032904800, 200000032911200],
|
||||||
]) {
|
]) {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -590,7 +590,7 @@ test("the measured double submit still fires under the bound", () => {
|
|||||||
// the runner's own restart.
|
// the runner's own restart.
|
||||||
test("a submit the runner relaunched across demands no balance move", () => {
|
test("a submit the runner relaunched across demands no balance move", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "Tap", on: submitOn, applied: true, relaunched: true },
|
lastAction: { kind: "Tap", on: submitOn, applied: true, relaunched: true },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -607,7 +607,7 @@ test("a submit the runner relaunched across demands no balance move", () => {
|
|||||||
test("no relaunch reported still convicts a double submit", () => {
|
test("no relaunch reported still convicts a double submit", () => {
|
||||||
for (const relaunched of [null, undefined]) {
|
for (const relaunched of [null, undefined]) {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { kind: "DoubleTap", on: submitOn, applied: true, relaunched },
|
lastAction: { kind: "DoubleTap", on: submitOn, applied: true, relaunched },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
readHomeCards,
|
readHomeCards,
|
||||||
readHomeTotalBalance,
|
readHomeTotalBalance,
|
||||||
routeOfFrame,
|
routeOfFrame,
|
||||||
submitChangesBalanceByTypedAmount,
|
submitChangesBalanceByAtMostTypedAmount,
|
||||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
} from "../../../examples/folio/sanderling/predicates.ts";
|
||||||
|
|
||||||
// The spec's own screen table. A frame is the set of markers its accessibility
|
// The spec's own screen table. A frame is the set of markers its accessibility
|
||||||
@@ -119,7 +119,7 @@ test("the measured android transition chain no longer convicts at delta 0", () =
|
|||||||
const landing = step(["HomeScreen"], "$86,911.00", phantomSubmit);
|
const landing = step(["HomeScreen"], "$86,911.00", phantomSubmit);
|
||||||
assert.equal(landing.submits, 6);
|
assert.equal(landing.submits, 6);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: landing.route,
|
route: landing.route,
|
||||||
lastAction: phantomSubmit,
|
lastAction: phantomSubmit,
|
||||||
submitsInWindow: landing.submits,
|
submitsInWindow: landing.submits,
|
||||||
@@ -137,7 +137,7 @@ test("the measured android transition chain no longer convicts at delta 0", () =
|
|||||||
// than an equality. A balance that did not move is under any typed amount,
|
// than an equality. A balance that did not move is under any typed amount,
|
||||||
// whether nothing was submitted or the total has not caught up yet.
|
// whether nothing was submitted or the total has not caught up yet.
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: phantomSubmit,
|
lastAction: phantomSubmit,
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
@@ -151,7 +151,7 @@ test("the measured android transition chain no longer convicts at delta 0", () =
|
|||||||
// The double tap it was always meant to catch is untouched by that: two
|
// The double tap it was always meant to catch is untouched by that: two
|
||||||
// 33900 debits against one action still exceed the amount typed for it.
|
// 33900 debits against one action still exceed the amount typed for it.
|
||||||
assert.equal(
|
assert.equal(
|
||||||
submitChangesBalanceByTypedAmount({
|
submitChangesBalanceByAtMostTypedAmount({
|
||||||
route: "home",
|
route: "home",
|
||||||
lastAction: { ...phantomSubmit, kind: "DoubleTap" },
|
lastAction: { ...phantomSubmit, kind: "DoubleTap" },
|
||||||
submitsInWindow: 1,
|
submitsInWindow: 1,
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ test("an extractor that returned undefined keeps its index through JSON", () =>
|
|||||||
// state.lastAction is the one piece of state the page cannot observe for
|
// state.lastAction is the one piece of state the page cannot observe for
|
||||||
// itself: only the runner knows which action it actually applied. While the web
|
// itself: only the runner knows which action it actually applied. While the web
|
||||||
// runtime hardcoded null there, a spec property gated on the last action (e.g.
|
// runtime hardcoded null there, a spec property gated on the last action (e.g.
|
||||||
// folio's submitMovesBalanceByTypedAmount, which only looks at taps on
|
// folio's submitMovesBalanceByAtMostTypedAmount, which only looks at taps on
|
||||||
// TxnSubmit) was vacuously true on web forever, and the run went green having
|
// TxnSubmit) was vacuously true on web forever, and the run went green having
|
||||||
// checked nothing.
|
// checked nothing.
|
||||||
function lastActionSeenByASpec(pushed: unknown): unknown {
|
function lastActionSeenByASpec(pushed: unknown): unknown {
|
||||||
|
|||||||
Reference in new issue
Block a user