test(folio): pin the safe-integer guard and its boundary

This commit is contained in:
pj committed 2026-08-14 11:22:07 +05:30
1 parent 0ad8a015ca
commit 646c3663e0
2 files changed
+167 -4

No files matched your search

@@ -241,3 +241,141 @@ test("route gate: home landing with double-insert delta fires", () => {
false, false,
); );
}); });
// Precision. Cents are integers in float64 here, so the equality only means
// something while every number involved is exactly representable. The app takes
// any amount that fits a Kotlin Long, and an iOS run reached a balance around
// 1e18 cents, where representable values sit 128 apart: the delta of a
// perfectly healthy single submit no longer reads back as the typed amount.
const HUGE_BALANCE = 999999999999999900;
test("above 2^53 the arithmetic itself is wrong, which is why the guard exists", () => {
assert.notEqual(Math.abs(HUGE_BALANCE + 1600 - HUGE_BALANCE), 1600);
});
test("above 2^53 a healthy single submit is not reported", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 1600,
prevTotalBalance: HUGE_BALANCE,
currTotalBalance: HUGE_BALANCE + 1600,
}),
true,
);
});
test("above 2^53 a double-submit delta is not reported either", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 1600,
prevTotalBalance: HUGE_BALANCE,
currTotalBalance: HUGE_BALANCE + 3200,
}),
true,
);
});
test("an unreadable previous balance above 2^53 is not evidence", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 1600,
prevTotalBalance: HUGE_BALANCE,
currTotalBalance: 5000,
}),
true,
);
});
// A typed amount past the safe range cannot be compared either. parseTypedAmount
// returns 0 for those now, but the predicate takes the number from its caller
// and must not convict on one it cannot hold.
test("typed amount above 2^53 is not evidence", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 1e23,
prevTotalBalance: 0,
currTotalBalance: 0,
}),
true,
);
});
// The boundary, from both sides. MAX_SAFE_INTEGER still gets judged; one cent
// more is where counting stops being exact.
test("boundary: a double submit landing exactly on MAX_SAFE_INTEGER still fires", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 4503599627370495,
prevTotalBalance: 0,
currTotalBalance: 9007199254740990,
}),
false,
);
});
test("boundary: a single submit landing exactly on MAX_SAFE_INTEGER passes", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 9007199254740991,
prevTotalBalance: 0,
currTotalBalance: 9007199254740991,
}),
true,
);
});
test("boundary: one cent past MAX_SAFE_INTEGER stops being evidence", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 4503599627370496,
prevTotalBalance: 0,
currTotalBalance: 9007199254740992,
}),
true,
);
});
// The guard covers the balances and the typed amount, not their difference: two
// safe balances subtract exactly whenever the result could have matched a safe
// typed amount, so a mismatch here is real and must still be reported.
test("a large but exact difference between safe balances still fires", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: 500,
prevTotalBalance: -9007199254740991,
currTotalBalance: 9007199254740991,
}),
false,
);
});
// The 21-digit corpus amount end to end: the app refuses it, so nothing moves,
// 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", () => {
assert.equal(
submitChangesBalanceByTypedAmount({
route: "home",
lastAction: { kind: "Tap", on: submitOn },
typedAmount: parseTypedAmount("999999999999999999999"),
prevTotalBalance: 220900,
currTotalBalance: 220900,
}),
true,
);
});
+29 -4
View File
@@ -43,14 +43,39 @@ test("zero returns 0", () => {
assert.equal(parseTypedAmount("0"), 0); assert.equal(parseTypedAmount("0"), 0);
}); });
test("leading plus sign tolerated as positive", () => { // The app's parseCents matches ^\d+(\.\d{1,2})?$ against the trimmed input, so
assert.equal(parseTypedAmount("+50"), 5000); // a sign is rejected and no transaction is created. Reading "-50" as 5000 cents
// would make the balance property demand a move the app never made.
test("leading plus sign rejected, like the app", () => {
assert.equal(parseTypedAmount("+50"), 0);
}); });
test("leading minus sign tolerated as positive", () => { test("leading minus sign rejected, like the app", () => {
assert.equal(parseTypedAmount("-50"), 5000); assert.equal(parseTypedAmount("-50"), 0);
}); });
test("comma-separated thousands accepted", () => { test("comma-separated thousands accepted", () => {
assert.equal(parseTypedAmount("1,234.56"), 123456); assert.equal(parseTypedAmount("1,234.56"), 123456);
}); });
// The input corpus types this 21-digit run into every field. parseCents calls
// toLongOrNull on the whole part, which is null past Long.MAX, so the app
// refuses the submit; float64 would have read it as 1e23 and asked the property
// to find a balance move of 1e23 cents that never happened.
test("21-digit corpus amount returns 0: the app rejects it", () => {
assert.equal(parseTypedAmount("999999999999999999999"), 0);
});
test("amount too large for exact cents returns 0", () => {
assert.equal(parseTypedAmount("100000000000000"), 0);
});
// 9007199254740991 cents is Number.MAX_SAFE_INTEGER: the last amount whose
// cents survive the multiply intact.
test("largest exactly representable amount is kept", () => {
assert.equal(parseTypedAmount("90071992547409.91"), 9007199254740991);
});
test("one cent past the safe range returns 0", () => {
assert.equal(parseTypedAmount("90071992547409.92"), 0);
});