mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
test(folio): pin the safe-integer guard and its boundary
This commit is contained in:
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,
|
||||
);
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
});
|
||||
@@ -43,14 +43,39 @@ test("zero returns 0", () => {
|
||||
assert.equal(parseTypedAmount("0"), 0);
|
||||
});
|
||||
|
||||
test("leading plus sign tolerated as positive", () => {
|
||||
assert.equal(parseTypedAmount("+50"), 5000);
|
||||
// The app's parseCents matches ^\d+(\.\d{1,2})?$ against the trimmed input, so
|
||||
// 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", () => {
|
||||
assert.equal(parseTypedAmount("-50"), 5000);
|
||||
test("leading minus sign rejected, like the app", () => {
|
||||
assert.equal(parseTypedAmount("-50"), 0);
|
||||
});
|
||||
|
||||
test("comma-separated thousands accepted", () => {
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user