mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
The runner now keeps lastAction and marks it relaunched: true where it used to report nothing at all, so the two properties that demand an effect judge a step whose process may have died before the write landed. submitChangesBalanceByTypedAmount and createdAccountHasNonZeroBalance both decline there. The counting bound does not: a relaunch cannot manufacture a transaction, and the submit is counted, so declining would throw away the detection the runner fix restored.
375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
import {
|
|
createdAccountHasNonZeroBalance,
|
|
initialsOf,
|
|
} from "../../../examples/folio/sanderling/predicates.ts";
|
|
|
|
const created = {
|
|
kind: "Tap",
|
|
on: "testTag:AddAccountScreen > testTag:AddAccountSubmit",
|
|
applied: true as const,
|
|
};
|
|
const idle = { kind: "Tap", on: "testTag:HomeScreen > testTag:AccountCard", applied: true as const };
|
|
|
|
const account = (name: string, balance: number | null) => ({ name, balance });
|
|
|
|
// The property still has teeth: the account the fuzzer just asked for, holding
|
|
// money on the step its creation landed on Home, is a real violation.
|
|
test("an account created holding money is a violation", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("a double-tapped create is judged the same way", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: { kind: "DoubleTap", on: "id:AddAccountSubmit", applied: true },
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("an account created empty is what the app is supposed to do", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 0)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("a balance that could not be read is not evidence", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", null)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// The false positives this replaces. Home lists the accounts that fit the
|
|
// viewport, so an account arrives in a later reading for reasons that have
|
|
// nothing to do with being created: the list scrolled, a clipped card finished
|
|
// laying out, or (before the route fix) the earlier reading came off a
|
|
// half-rendered Home mid-transition. Measured on android: an existing Travel
|
|
// holding $24,112.00 and an existing Savings holding $429,585.00, convicted for
|
|
// coming into view.
|
|
test("an account scrolling into view is not an account being created", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: idle,
|
|
typedName: "Travel",
|
|
before: [account("Emergency Fund", 461012300), account("Checking", 0)],
|
|
after: [
|
|
account("Emergency Fund", 461012300),
|
|
account("Checking", 0),
|
|
account("Travel", 2411200),
|
|
account("Savings", 0),
|
|
],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("nor is one that appears with no action at all behind it", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: null,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 2411200)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// Even on the creation step, the only card judged is the one that answers to
|
|
// the name the fuzzer typed. A card that came into view alongside it is still
|
|
// just a card that came into view.
|
|
test("a funded account arriving beside the created one is not judged", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Savings",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Savings", 0), account("Travel", 2411200)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("off Home there is no reading to judge", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "ledger",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("a transition frame names no route, so nothing is judged there either", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: null,
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("an unknown reading on either side is not evidence", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: null,
|
|
after: [account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: null,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// defaultActions types edge-case text into the name field, and an empty name is
|
|
// not a name we can find a card by.
|
|
test("an empty typed name attributes nothing", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: " ",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: undefined,
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// Web merges the card into one node whose text opens with the avatar initials,
|
|
// so the identity key carries them: "TRTravel" is the card for "Travel".
|
|
test("the merged web key still matches the name that was typed", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("CHChecking", 0)],
|
|
after: [account("CHChecking", 0), account("TRTravel", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
// The avatar the merged web key opens with, hand-computed off Format.kt rather
|
|
// than off the mirror, because a mirror checked against itself checks nothing.
|
|
// A single word gives its first two characters, several give the first letter
|
|
// of the first and of the last, and an empty name gives "?".
|
|
test("the initials a merged key opens with are the app's", () => {
|
|
const named: [string, string][] = [
|
|
["CH", "Checking"],
|
|
["SA", "Savings"],
|
|
["TR", "Travel"],
|
|
["EF", "Emergency Fund"],
|
|
["IN", "Investments"],
|
|
["FU", "Fund"],
|
|
["T2", "Travel 2024"],
|
|
["A", "a"],
|
|
["X9", "x9"],
|
|
["-1", "-1"],
|
|
["?", ""],
|
|
["?", " "],
|
|
];
|
|
for (const [initials, name] of named) {
|
|
assert.equal(initialsOf(name), initials, `initials for ${JSON.stringify(name)}`);
|
|
}
|
|
});
|
|
|
|
// The attribution used to be a suffix test, and a suffix test hands the verdict
|
|
// to whichever OTHER account happens to end with the typed name. Home lists
|
|
// what fits the viewport, so the card that was just created is clipped out of
|
|
// the reading exactly as easily as any other, and the older account left in it
|
|
// is then judged for money it has held all along.
|
|
test("an older account whose name ends with the typed one is not the created one", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Fund",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Emergency Fund", 461012300)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("the merged web key is matched whole too, not by its ending", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Fund",
|
|
before: [account("CHChecking", 0)],
|
|
after: [account("CHChecking", 0), account("EFEmergency Fund", 461012300)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// The card that was actually asked for is still judged, standing next to the
|
|
// account that merely ends with its name.
|
|
test("the created card is judged beside an account whose name ends with it", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Fund",
|
|
before: [account("Emergency Fund", 461012300)],
|
|
after: [account("Emergency Fund", 461012300), account("Fund", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Fund",
|
|
before: [account("EFEmergency Fund", 461012300)],
|
|
after: [account("EFEmergency Fund", 461012300), account("FUFund", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
// Two cards answering to one typed name leave the appearance unattributable.
|
|
// Accounts.name is UNIQUE and Repository.createAccount rejects a name already
|
|
// taken, so the pair is one card the tree exposed twice on a transition frame,
|
|
// or two names the merged web key cannot tell apart.
|
|
test("two cards matching the typed name are not attributable to the creation", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000), account("Travel", 900)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("a card that was already there is not a card that was just created", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: created,
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0), account("Travel", 2411200)],
|
|
after: [account("Checking", 0), account("Travel", 2411200)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
// The runner's foreground guard restarted the app after the create. A fresh
|
|
// launch draws Home from the top, so the visible set is whatever the new layout
|
|
// fits rather than what was there a step ago, and "appeared in the reading" is
|
|
// even less like "was created" than usual. The process may also have died
|
|
// before the write landed, which makes the card that carries the typed name an
|
|
// older account of that name coming into view.
|
|
test("a create the runner relaunched across attributes nothing", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: { ...created, relaunched: true },
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("no relaunch reported still judges the account that was created", () => {
|
|
for (const relaunched of [null, undefined]) {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: { ...created, relaunched },
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
true,
|
|
);
|
|
}
|
|
});
|
|
|
|
// The apply call failed with the gesture possibly already delivered, so nobody
|
|
// knows whether that account was created. The card carrying the typed name may
|
|
// be an older one that scrolled into view, and attributing it to a creation
|
|
// that may never have happened is a conviction built on a guess.
|
|
test("a create the runner could not confirm attributes nothing", () => {
|
|
assert.equal(
|
|
createdAccountHasNonZeroBalance({
|
|
route: "home",
|
|
lastAction: { ...created, applied: null },
|
|
typedName: "Travel",
|
|
before: [account("Checking", 0)],
|
|
after: [account("Checking", 0), account("Travel", 5000)],
|
|
}),
|
|
false,
|
|
);
|
|
});
|