From a275bf766c0c3e67a4b63ed762051e3fa408a888 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 16 Aug 2026 01:12:09 +0530 Subject: [PATCH] test(folio): the walk drives the composition the spec runs countSubmitsInWindow never saw an amountText here, so every walk test counted submits the app must have refused. with the field passed, a refused submit no longer buys a later double tap an alibi: without it the window reads 3, not 1. --- pkg/spec/test/folio-ledger-window.test.ts | 39 ++++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/pkg/spec/test/folio-ledger-window.test.ts b/pkg/spec/test/folio-ledger-window.test.ts index 5f47229..a0fa6ae 100644 --- a/pkg/spec/test/folio-ledger-window.test.ts +++ b/pkg/spec/test/folio-ledger-window.test.ts @@ -317,8 +317,11 @@ test("Home shows every account's money, so it is not this comparison's scale", ( }); // A step of the walk: the frame it landed on, the balance node that frame -// carried, what was in the amount field the step before, and the action that -// got there. Driven through the same carrier and window the spec holds. +// carried, the amount field as that frame shows it, and the action that got +// there. Driven through the same carrier and window the spec holds, `typed` +// included: the spec hands the landing frame's field to countSubmitsInWindow +// and the previous frame's to the property, and a walk that skips the first +// half drives a composition the spec never runs. interface Frame { route: string | null; balanceText?: string; @@ -342,6 +345,7 @@ function walk(frames: readonly Frame[]) { const window = countSubmitsInWindow({ previousCount: submits, lastAction: frame.lastAction, + amountText: frame.route === "add-transaction" ? (frame.typed ?? "") : undefined, fresh: reading.fresh, }); submits = window.next; @@ -389,9 +393,34 @@ test("the Home window cannot judge a walk that never goes Home", () => { } }); -// The same trajectory, judged where the app actually is. The frame the double -// tap lands on is the account's own ledger, so the window that closes there -// holds exactly the one action. +// The trajectory the recorded iOS run took to the frames this property judges, +// with the taps that reach TxnSubmit over an empty field: 40 of its 61 submit +// taps landed back on the transaction screen, and the field they read is the +// one the landing frame shows. Counting those as submits is what the run +// measures as the difference between 4 convictions and 0. Here the balance node +// is off the viewport while they happen, so nothing resets the window and the +// slack survives to the frame that matters. +test("submits the app must have refused do not buy a double tap an alibi", () => { + const verdicts = walk([ + { route: "ledger", balanceText: "$100.00", lastAction: openLedger }, + { route: "add-transaction", lastAction: openAddTxn }, + { route: "add-transaction", lastAction: submit }, + { route: "add-transaction", lastAction: submit }, + { route: "add-transaction", typed: "196", lastAction: typeAmount }, + { route: "ledger", balanceText: "$492.00", lastAction: doubleSubmit }, + ]); + assert.equal(verdicts[5]?.submits, 1); + assert.equal(verdicts[5]?.violated, true); +}); + +// The same trajectory, judged where the app actually is. The double tap sends +// two Submit events, and the frame it lands on says which of the two shapes +// they took: two commits and two pops reach Home, where the counting invariant +// judges them, and two commits with the second pop cancelled by the first stop +// on the account's own ledger, which is this one. The recorded iOS run took +// three double taps and all three landed on Home, so this frame is reasoned +// from the app's code (AddTransactionViewModel.submit commits inside +// viewModelScope, then pops) rather than measured. test("the double tap is convicted on the frame it lands on", () => { const verdicts = walk([ { route: "ledger", balanceText: "$100.00", lastAction: openLedger },