# 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 ` | 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/`. 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 | CHANGED 2026-10-13 | A capital account is one running balance (Form 1065 K-1 instructions, Item L). Undistributed earnings now split cumulatively as of each date, so each member's capital chains exactly (`each_members_capital_chains_across_statements`; it fails on the old code); rules.md B-13 | | 8 | Cash flow nets one line per account; investing comes only from registered assets | CHANGED 2026-10-13 | ASC 230-10-45-7: gross receipts and payments "more relevant" than net; 45-8 limits netting to quick-turnover items. Financing and investing are now gross; loans made and investments bought are investing (230-10-20, 45-13) via `[books] investing_accounts`. Owner-advanced expenses: PwC 6.9.3.1 constructive treatment via `[books] financing_accounts` (rules.md B-14) | | 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 are settled from the sources, not by any one company's CPA (wanderbooks is general software). Where the standards leave a real choice, it is a per-company setting with a sourced default, for that company's accountant to review: the cash flow account roles (rules.md B-14) are the first. ## 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 |