Each formerly ASSUMED behavior is now CONFIRMED, CONVENTION, DESIGN, or CHANGE (pending), with its source (Reg. S-X, ASC 230 via Deloitte DART, IRS K-1 instructions, OpenStax). Two need engine work: members' equity should allocate cumulatively so each member's beginning equals the prior ending (#7), and the cash flow should show financing gross and classify investing by account role (#8). Two questions for the CPA are listed. The test doc comments that relied on them now cite the verdicts. Core mutation run: 1,011 caught + 13 timeouts of 1,024 viable, 0 missed. No benchmark budget, by decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
8.8 KiB
Testing strategy
How wanderbooks knows its books are right. Strongest oracle first: a
test's worth is where its idea of "correct" comes from, not what it runs.
Formal proofs are in docs/verification.md; the authorities every rule
comes from are in docs/spec/. Scope so far: ledger-core in full, and
the store's journal safety; the other crates come next (gap list).
Layers
| Layer | Oracle (rank) | Where | Command | Runtime | Gates |
|---|---|---|---|---|---|
| Spec-derived examples | written specs with sources (1) | tests/spec_*.rs, invariants.rs, statements.rs, assets.rs, ... |
just check |
seconds | every change |
| Regressions | the bug, named (1) | beside the code they cover, *_regression/named for the defect |
just check |
seconds | every change |
| Compatibility contracts | journals written under older rules must open (1) | ledger-store/tests/store.rs (books_written_under_older_rules_still_open) |
just check |
<1s | every change |
| Properties | laws over all inputs (2) | properties.rs, spec_*.rs, kernel_bridge.rs |
just check |
seconds | every change |
| Metamorphic | scaling, reversal, ordering invariance (2) | spec_reports.rs |
just check |
seconds | every change |
| Refusal law (store) | a refused command never reaches the journal; the books reopen to the same reports (2, 3) | store.rs refused_commands_never_reach_the_journal |
just check (STORE_CASES=5000 deeper) |
~20s | every change |
| Fault injection (store) | a full disk or a cut-off append leaves whole, openable books (1) | ledger-store/tests/faults.rs, store.rs (power_loss_at_any_byte_of_the_last_append_is_recovered) |
just check |
~5s | every change |
| Model-based stateful | the Lean state machine, transcribed (3) | tests/model.rs |
just check |
~3s | every change |
| Differential: Decimal vs i64 | the kernel at two types (3) | kernel_bridge.rs |
just check |
<1s | every change |
| Differential: GnuCash | another implementation, adjudicated (3) | tests/oracle_gnucash.rs, oracle/gnucash/ |
just oracle |
~2 min | weekly |
| Formal proofs | Kani (bounded) and Lean (unbounded) | src/proofs.rs, verify/lean/ |
just prove |
~3 min | weekly |
| Fuzzing | crash, hang, broken books (4) | fuzz/fuzz_targets/, corpora in fuzz/corpus/ |
just fuzz <target> <secs> |
10 min each | weekly |
| Miri | undefined behavior in our use of dependencies (4) | the core's unit, bridge, invariant tests | just miri |
~2 min | on demand |
| Golden | tests/golden/fixture_company.json (5) |
invariants.rs |
just check |
<1s | every change |
| Benchmarks | a baseline (6) | benches/books.rs |
just bench |
~1 min | none (no budget, by decision) |
| Mutation | can the tests fail? | ledger-core (2026-10-13: 1,011 caught + 13 timeouts of 1,024 viable, 0 missed) |
just mutants |
~3 h | weekly |
CI: .github/workflows/check.yml runs just check on every push;
scheduled.yml runs proofs, the oracle, fuzzing, and mutation weekly.
Seeds and failure databases: proptest's *.proptest-regressions files
are checked in and replay on every run. The GnuCash oracle's books come
from fixed seeds (SEED in oracle_gnucash.rs). Fuzz corpora live in
fuzz/corpus/<target>.
Golden review: fixture_company.json is hand-verified (docs/oracle.md,
Layer 1). The van depreciation golden was replaced on 2026-10-13 by a
hand computation (van_schedule_is_the_hand_computed_one) after every
row was checked against it.
Benchmarks (baseline 2026-10-13)
No budget, by decision (Stephen, 2026-10-13: the books do little work and are fast enough). The numbers are a baseline to spot a regression.
40-account chart, two-line entries across a year, on the development laptop:
| 1k entries | 10k entries | |
|---|---|---|
| clone the ledger (store rehearsal, per command) | 56 µs | 1.0 ms |
| post one more entry | 1.0 µs | 5.8 µs |
| trial balance | 44 µs | 1.07 ms |
| balance sheet | 47 µs | 1.04 ms |
| one month's P&L | 25 µs | 248 µs |
Formerly assumed: checked against the standards (2026-10-13)
Stephen isn't an accountant and won't adjudicate these; each was checked against published sources instead (rank: FASB ASC via the Deloitte DART reproduction, SEC, IRS; then textbooks; software practice is evidence of convention only). Verdicts: CONFIRMED (a source supports it), CONVENTION (no standard governs; matches mainstream practice), DESIGN (not an accounting question), CHANGE (a source says otherwise; work pending).
| # | Behavior | Verdict | Source / note |
|---|---|---|---|
| 1 | A P&L whose start is after its end is refused | DESIGN | No standard governs a reversed range; refusing beats silently swapping a typo |
| 2 | A balance sheet omits zero-balance accounts | CONFIRMED | Reg. S-X 210.4-03(a): "No caption should be shown ... as to which the items and conditions are not present." A comparative statement must still show a zero beside a non-zero prior column |
| 3 | The lifetime profit_and_loss counts closing entries |
CONFIRMED | Used as earnings not yet closed on the balance sheet; closed income is already in capital (OpenStax Principles of Financial Accounting 5.1). Its name misleads: rename to unclosed earnings (pending) |
| 4 | Closing a period with nothing to close still locks it | CONVENTION | A close is a lock (B-12); a dormant month must still lock and count for close-in-order |
| 5 | Draws and distributions are closed into capital at period end | CONFIRMED | OpenStax 5.1: dividends "are closed to the permanent account"; drawing accounts close to owner's capital. Monthly closes mean the year's K-1 Item L withdrawals sum across months |
| 6 | Reconciliation balances | CONFIRMED | Signed by the account's normal side (a card reads as owed), as statements show it; reconciling_a_card_reads_its_normal_side_... |
| 7 | A member's ending on one equity statement may differ by a cent from the next statement's beginning | CHANGE (pending) | A capital account is one running balance (Form 1065 K-1 instructions, Item L: beginning and ending capital "based on the partnership's books and records"). Allocate cumulatively so beginning equals the prior ending per member. CPA question below |
| 8 | Cash flow nets one line per account; investing comes only from registered assets | CHANGE (pending) | ASC 230-10-45-7: gross receipts and payments "more relevant" than net; 45-8 limits netting to quick-turnover items. Financing: contributions apart from distributions, borrowings apart from repayments. ASC 230-10-20/45-13: loans made and investments bought are investing, so classify by account role |
| 9 | An asset's life ends at purchase + life months, that day not held; day-count proration | CONVENTION | Exactly N months of service; day proration and the mid-month convention are both accepted book policies (ASC 360-10-35-4 not read: login) |
| 10 | A payroll entry has one line per account total | CONFIRMED | The per-employee detail the IRS requires ("Amounts and dates of all wage ... payments", Employment Tax Recordkeeping) is kept in the run; a summary entry is standard |
| 11 | A period with exactly the dates of an existing one is allowed | DESIGN | Nothing governs duplicate periods; harmless (the second close locks only). Refusing exact duplicates is an option |
Questions for Krista Baker (CPA):
- Interim members' equity: is a per-member allocation of unclosed earnings acceptable on interim statements, or should the books close monthly so the capital accounts carry it?
- How should 2100 Due to Shareholders, and expenses the owners paid personally, appear on the cash flow statement: operating, financing, or as a non-cash disclosure (ASC 230-10-50-3, not yet read)?
Gap list: what isn't covered, and what would bring it in
| Technique | Status | Trigger |
|---|---|---|
| Fault injection | SKIP for the core (no I/O, clock, or network); DONE for the store's journal (full disk, cut-off append) | the first I/O or clock in ledger-core |
| Integration | SKIP for the core (no collaborators) | the core gains one; covered at the store and MCP layers |
| End-to-end | SKIP for the core (a library) | MCP stdio tests cover the journeys |
| Security properties | SKIP for the core (no roles, no secrets) | the first permission concept, or a secret-bearing field |
| Concurrency stress | SKIP for the core (no shared state) | the first Arc, atomic, or lock in the core; the GUI and MCP server have threads (next) |
| Lean: apportion's sum law for any n | OPEN | Kani covers n <= 3, properties n <= 5; needs a permutation lemma |
| ASC paragraphs not yet read | OPEN | ASC 360-10-35-4, 340-10, 230-10-45-10, 606-10-55-37B, 210: needs a FASB basic-view login |
| GnuCash's own close | OPEN | the oracle books closes as ordinary entries; comparing GnuCash's Close Book needs its Scheme API |
| Store: differential replay vs. projection, concurrency (writer + snapshot reader), benchmarks at 10k, mutation | NEXT | then import, MCP, config, GUI, the same playbook |