mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(folio): drop a name two cards answer to
homeTxnCountsOf keyed on the account name and let the last card win, so two accounts the fuzzer named the same collapsed into one entry. a reading that saw one Travel card and a later one that saw both then subtracted two different accounts' counts, and submitCommitsOneTransactionPerAction convicted a healthy app of double-submitting. it is a gated property in folio-run.sh, so that reads as "found the submit bug" over a card scrolling into view. same rule createdAccountHasNonZeroBalance already applies: a name nothing can attribute is no evidence. counted over every card, since an unreadable twin spoils the identity too.
This commit is contained in:
1 parent
ea4d40e507
commit
ade3e77ff5
2 files changed
+105
-1
No files matched your search
@@ -280,10 +280,24 @@ export function homeAccountsOf(cards: readonly CardReading[]): Account[] | null
|
|||||||
// A card whose name or count is unreadable is left out rather than guessed at;
|
// A card whose name or count is unreadable is left out rather than guessed at;
|
||||||
// committedTransactionsExceedSubmits treats a missing account as no evidence.
|
// committedTransactionsExceedSubmits treats a missing account as no evidence.
|
||||||
// Every card being unreadable leaves nothing to compare, which is unknown.
|
// Every card being unreadable leaves nothing to compare, which is unknown.
|
||||||
|
//
|
||||||
|
// A name carried by more than one card is left out for the same reason, the
|
||||||
|
// rule createdAccountHasNonZeroBalance applies with `matches.length === 1`:
|
||||||
|
// nothing here can say which of them a count came from. Folio accepts the same
|
||||||
|
// account name twice and Home lists whatever fits the viewport, so a reading
|
||||||
|
// that saw one Travel card and a later one that saw two would otherwise
|
||||||
|
// subtract two DIFFERENT accounts' counts and convict a healthy app of
|
||||||
|
// double-submitting. The twin does not have to be readable to spoil the
|
||||||
|
// identity, so duplicates are counted over every card, not just the usable
|
||||||
|
// ones. Dropping a card can only ever cost a detection.
|
||||||
export function homeTxnCountsOf(cards: readonly CardReading[]): Record<string, TxnCount> | null {
|
export function homeTxnCountsOf(cards: readonly CardReading[]): Record<string, TxnCount> | null {
|
||||||
|
const cardsPerName = new Map<string, number>();
|
||||||
|
for (const card of cards) cardsPerName.set(card.name, (cardsPerName.get(card.name) ?? 0) + 1);
|
||||||
const counts: Record<string, TxnCount> = {};
|
const counts: Record<string, TxnCount> = {};
|
||||||
for (const card of cards) {
|
for (const card of cards) {
|
||||||
if (card.name !== "" && card.count !== undefined) counts[card.name] = card.count;
|
if (card.name === "" || card.count === undefined) continue;
|
||||||
|
if (cardsPerName.get(card.name) !== 1) continue;
|
||||||
|
counts[card.name] = card.count;
|
||||||
}
|
}
|
||||||
return Object.keys(counts).length === 0 ? null : counts;
|
return Object.keys(counts).length === 0 ? null : counts;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,3 +360,93 @@ test("the merged-text card list drops the same pair", () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Home lists whatever fits the viewport, and Folio lets two accounts share a
|
||||||
|
// name, so one name can arrive on two cards. Keying counts by name collapsed
|
||||||
|
// them onto the last card, and the two readings a window compares then came off
|
||||||
|
// DIFFERENT cards: the probe below is a healthy app, one submit, and a scroll.
|
||||||
|
test("two cards sharing a name do not become one count", () => {
|
||||||
|
const before = homeTxnCountsOf([{ name: "Travel", balance: 0, count: 0 }]);
|
||||||
|
const after = homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: 0 },
|
||||||
|
{ name: "Travel", balance: 500, count: 8 },
|
||||||
|
]);
|
||||||
|
assert.equal(
|
||||||
|
committedTransactionsExceedSubmits({
|
||||||
|
countsBefore: before,
|
||||||
|
countsAfter: after,
|
||||||
|
submitsInWindow: 1,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.deepEqual(after, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name on two cards is dropped from both readings", () => {
|
||||||
|
const before = homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: "3" },
|
||||||
|
{ name: "Travel", balance: 0, count: "9" },
|
||||||
|
{ name: "Checking", balance: 0, count: "2" },
|
||||||
|
]);
|
||||||
|
const after = homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: "3" },
|
||||||
|
{ name: "Travel", balance: 0, count: "11" },
|
||||||
|
{ name: "Checking", balance: 0, count: "2" },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(before, { Checking: "2" });
|
||||||
|
assert.deepEqual(after, { Checking: "2" });
|
||||||
|
assert.equal(
|
||||||
|
committedTransactionsExceedSubmits({
|
||||||
|
countsBefore: before,
|
||||||
|
countsAfter: after,
|
||||||
|
submitsInWindow: 0,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The other card need not be readable to spoil the identity: an unreadable
|
||||||
|
// count still means the name on the map may not be the card that was read.
|
||||||
|
test("a duplicate name is dropped even when the twin has no count", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: 4 },
|
||||||
|
{ name: "Travel", balance: 0, count: undefined },
|
||||||
|
{ name: "Savings", balance: 0, count: 1 },
|
||||||
|
]),
|
||||||
|
{ Savings: 1 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dropping the ambiguous name must not disable the property for the rest.
|
||||||
|
test("a unique name is still counted beside a dropped duplicate", () => {
|
||||||
|
const before = homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: 3 },
|
||||||
|
{ name: "Travel", balance: 0, count: 1 },
|
||||||
|
{ name: "Checking", balance: 0, count: 4 },
|
||||||
|
]);
|
||||||
|
const after = homeTxnCountsOf([
|
||||||
|
{ name: "Travel", balance: 0, count: 3 },
|
||||||
|
{ name: "Travel", balance: 0, count: 1 },
|
||||||
|
{ name: "Checking", balance: 0, count: 6 },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(before, { Checking: 4 });
|
||||||
|
assert.equal(
|
||||||
|
committedTransactionsExceedSubmits({
|
||||||
|
countsBefore: before,
|
||||||
|
countsAfter: after,
|
||||||
|
submitsInWindow: 1,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("distinct names are all counted", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
homeTxnCountsOf([
|
||||||
|
{ name: "Checking", balance: 0, count: "3" },
|
||||||
|
{ name: "Savings", balance: 0, count: "1" },
|
||||||
|
]),
|
||||||
|
{ Checking: "3", Savings: "1" },
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user