mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(folio): only disambiguate counts that came from merged text
the equal-length digit rule exists because web merges the card and an account named -1 makes '12' ambiguous. a dedicated count node has nothing to disambiguate, so applying it there threw away real evidence.
This commit is contained in:
1 parent
9efed1f669
commit
21ae7fde83
4 files changed
+214
-48
No files matched your search
@@ -11,12 +11,13 @@ import {
|
||||
import type {
|
||||
CardReading,
|
||||
HomeCardReading,
|
||||
TxnCount,
|
||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
||||
|
||||
const card = (name: string, balance: number | null, digits: string | undefined) => ({
|
||||
const card = (name: string, balance: number | null, count: TxnCount | undefined) => ({
|
||||
name,
|
||||
balance,
|
||||
digits,
|
||||
count,
|
||||
});
|
||||
|
||||
test("a laid-out card list reads as an account list and a count map", () => {
|
||||
@@ -83,11 +84,11 @@ test("an un-laid-out Home reports unknown but leaves the carrier intact", () =>
|
||||
// Home it lands on has not drawn its list yet, and the counting invariant must
|
||||
// still be able to see the pair once a real Home comes back.
|
||||
function run(steps: { route: string | null; cards: CardReading[]; lastAction: unknown }[]) {
|
||||
let carrier: Record<string, string> | null = null;
|
||||
let carrier: Record<string, TxnCount> | null = null;
|
||||
let submits = 0;
|
||||
const out: { counts: Record<string, string> | null; submits: number }[] = [];
|
||||
const out: { counts: Record<string, TxnCount> | null; submits: number }[] = [];
|
||||
for (const step of steps) {
|
||||
const reading: HomeCardReading<Record<string, string>> = readHomeCards({
|
||||
const reading: HomeCardReading<Record<string, TxnCount>> = readHomeCards({
|
||||
route: step.route,
|
||||
reading: homeTxnCountsOf(step.cards),
|
||||
previousCarrier: carrier,
|
||||
|
||||
@@ -2,29 +2,32 @@ import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
cardTxnCountDigits,
|
||||
cardTxnCount,
|
||||
committedTransactionsExceedSubmits,
|
||||
homeTxnCountsOf,
|
||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
||||
|
||||
// Android and iOS give the count its own node; web merges the card into one
|
||||
// string, where the count sits between the name and the balance.
|
||||
test("structured child wins over the merged card text", () => {
|
||||
// string, where the count sits between the name and the balance. The reading
|
||||
// carries which of the two it came from: a number is a count nothing else could
|
||||
// have leaked into, a string is a digit run that may have.
|
||||
test("a dedicated count node reads as a number, not a digit run", () => {
|
||||
assert.equal(
|
||||
cardTxnCountDigits({ childText: "12 transactions", cardText: "INInvestments12 transactions$2,589.00" }),
|
||||
"12",
|
||||
cardTxnCount({ childText: "12 transactions", cardText: "INInvestments12 transactions$2,589.00" }),
|
||||
12,
|
||||
);
|
||||
});
|
||||
|
||||
test("merged card text: the count is taken from in front of the balance", () => {
|
||||
assert.equal(
|
||||
cardTxnCountDigits({ childText: undefined, cardText: "INInvestments12 transactions$2,589.00" }),
|
||||
cardTxnCount({ childText: undefined, cardText: "INInvestments12 transactions$2,589.00" }),
|
||||
"12",
|
||||
);
|
||||
});
|
||||
|
||||
test("merged card text: the singular label parses too", () => {
|
||||
assert.equal(
|
||||
cardTxnCountDigits({ childText: undefined, cardText: "SASavings1 transaction$118.00" }),
|
||||
cardTxnCount({ childText: undefined, cardText: "SASavings1 transaction$118.00" }),
|
||||
"1",
|
||||
);
|
||||
});
|
||||
@@ -32,16 +35,16 @@ test("merged card text: the singular label parses too", () => {
|
||||
// The balance has to come off first, or a name ending in digits would be read
|
||||
// as the count.
|
||||
test("a card with no readable count is unknown, not zero", () => {
|
||||
assert.equal(cardTxnCountDigits({ childText: undefined, cardText: undefined }), undefined);
|
||||
assert.equal(cardTxnCountDigits({ childText: undefined, cardText: "no digits here" }), undefined);
|
||||
assert.equal(cardTxnCountDigits({ childText: "", cardText: "AA" + "a".repeat(198) }), undefined);
|
||||
assert.equal(cardTxnCount({ childText: undefined, cardText: undefined }), undefined);
|
||||
assert.equal(cardTxnCount({ childText: undefined, cardText: "no digits here" }), undefined);
|
||||
assert.equal(cardTxnCount({ childText: "", cardText: "AA" + "a".repeat(198) }), undefined);
|
||||
});
|
||||
|
||||
// Measured on a real web run: the account named "-1" holding 2 transactions
|
||||
// merges to "-1-12 transactions-$119.00", and the maximal digit run reads 12.
|
||||
test("merged text runs a digit-ending name into the count", () => {
|
||||
assert.equal(
|
||||
cardTxnCountDigits({ childText: undefined, cardText: "-1-12 transactions-$119.00" }),
|
||||
cardTxnCount({ childText: undefined, cardText: "-1-12 transactions-$119.00" }),
|
||||
"12",
|
||||
);
|
||||
});
|
||||
@@ -229,3 +232,131 @@ test("an unreadably long run is not evidence", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// The other side of that rule, and the reason it is scoped to merged text: a
|
||||
// count read off its own node has no account name in front of it, so its digits
|
||||
// ARE the count and a decade crossing is just a number getting longer. Both
|
||||
// windows below are real android seed-9 readings that the unscoped length rule
|
||||
// threw away, in runs that then finished clean.
|
||||
test("a dedicated node's count crossing a decade is usable evidence", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: 7 },
|
||||
countsAfter: { Checking: 12 },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Savings: 4 },
|
||||
countsAfter: { Savings: 10 },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Recovering the window is only worth anything if it still acquits the healthy
|
||||
// case, so the same crossing under a submit that earned it must not fire.
|
||||
test("a dedicated node's healthy decade crossing does not convict", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: 9 },
|
||||
countsAfter: { Checking: 10 },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: 9 },
|
||||
countsAfter: { Checking: 11 },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// The same numbers off merged text, where the digits may not be the count at
|
||||
// all: still dropped.
|
||||
test("the merged-text equivalent of that crossing is still dropped", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: "9" },
|
||||
countsAfter: { Checking: "10" },
|
||||
submitsInWindow: 0,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: "7" },
|
||||
countsAfter: { Checking: "12" },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// The boundary itself. A pair whose two readings came from different sources is
|
||||
// vouched for by neither rule: the string may carry a name prefix the number
|
||||
// does not, so subtracting them is not a transaction count.
|
||||
test("a pair straddling the two sources is not comparable", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: 7 },
|
||||
countsAfter: { Checking: "12" },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: { Checking: "7" },
|
||||
countsAfter: { Checking: 12 },
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// End to end from the two accessibility shapes, which is where the distinction
|
||||
// is actually made: the same account, the same true counts, read once off a
|
||||
// dedicated node and once off merged card text.
|
||||
const dedicated = (name: string, count: number) => ({
|
||||
name,
|
||||
balance: 0,
|
||||
count: cardTxnCount({ childText: `${count} transactions`, cardText: undefined }),
|
||||
});
|
||||
|
||||
const merged = (initials: string, name: string, count: number) => ({
|
||||
name,
|
||||
balance: 0,
|
||||
count: cardTxnCount({
|
||||
childText: undefined,
|
||||
cardText: `${initials}${name}${count} transactions$0.00`,
|
||||
}),
|
||||
});
|
||||
|
||||
test("a dedicated-node card list convicts across a decade", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: homeTxnCountsOf([dedicated("Checking", 9)]),
|
||||
countsAfter: homeTxnCountsOf([dedicated("Checking", 11)]),
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("the merged-text card list drops the same pair", () => {
|
||||
assert.equal(
|
||||
committedTransactionsExceedSubmits({
|
||||
countsBefore: homeTxnCountsOf([merged("CH", "Checking", 9)]),
|
||||
countsAfter: homeTxnCountsOf([merged("CH", "Checking", 11)]),
|
||||
submitsInWindow: 1,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user