test(core): spec-blind suites and a live GnuCash oracle; four fixes they found
Max-testing pass, steps 2-4. Spec-blind tests (rank 1-2): four agents, each given only the specs and the public interface (doc comments and signatures, every body removed, never src/ or the existing tests), wrote tests/spec_reports.rs (62), spec_ledger.rs (72), spec_arap.rs (42), spec_assets_payroll.rs (50): hand-computed examples per scenario and a proptest law per invariant (conservation, partition, invariance, scaling, reversal cancels, "a refusal leaves the books unchanged" over random invalid commands). They found: 1. A refused payroll run changed the books: an all-zero run passed validation, then failed at post after creating seven payroll accounts and using up an entry id. validate_payroll_lines now refuses a run with no gross pay. 2. A refused add_account used up an account id (empty code, duplicate, unknown parent). The id is now taken only when the account is added. 3. Stale prose: close_period_to and members_equity still described the old last-member rounding. 4. Open (left #[ignore]d pending Stephen): depreciation_schedule's doc says "through the month containing" the date; the code prorates that month by days. GnuCash oracle (rank 3, an oracle never the spec): `just oracle` builds GnuCash 5.10 in a container pinned by base digest and package versions (oracle/gnucash/) and runs tests/oracle_gnucash.rs: 200 seeded random books built through wanderbooks (entries, foreign lines, reversals, monthly closes), every account's balance at month ends and random dates and each month's P&L, compared exactly: 15,800 comparisons. First run: 4,724 differences, all from foreign lines; without them, zero. Adjudicated from principles (rules.md B-3): wanderbooks booked foreign * rate unrounded ($108.34567); a ledger in one money holds no fractions of its smallest unit, and its cash must match a bank. Stephen decided: book in the base currency's minor unit, half away from zero. add_line_fx now rounds; minor units per ISO 4217 list one (2026-09-17, SIX), generated into Currency::minor_units and recorded in docs/spec/sources/iso-4217.md. Replay now takes the journaled base amount (replay_add_line_fx), so lines booked before replay exactly as booked (contract: books_written_under_older_rules_still_open). Now: zero differences, foreign lines included. Tests: 449, from 223. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
0d4a5f8f53
commit
beb1fcf233
20 files changed
+8110
-51
No files matched your search
@@ -1016,3 +1016,49 @@ fn a_line_on_an_unknown_or_placeholder_account_is_refused_when_added() {
|
||||
"refused lines aren't added"
|
||||
);
|
||||
}
|
||||
|
||||
/// Spec (rules.md B-3; DECISION, Stephen 2026-10-13, after the GnuCash
|
||||
/// cross-check): a foreign line is booked in the base currency's smallest
|
||||
/// unit, half away from zero; the foreign original is kept.
|
||||
#[test]
|
||||
fn a_foreign_line_is_booked_in_whole_base_units() {
|
||||
let book = |base: Currency, foreign: Currency, amount: &str, rate: &str| {
|
||||
let mut l = Ledger::new(base);
|
||||
let a = l
|
||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let e = l.create_entry(date(5), "fx");
|
||||
let r = l.add_line_fx(
|
||||
e,
|
||||
a,
|
||||
Side::Debit,
|
||||
foreign,
|
||||
amount.parse().unwrap(),
|
||||
rate.parse().unwrap(),
|
||||
);
|
||||
r.map(|()| {
|
||||
let line = l.get_entry(e).unwrap().lines()[0].clone();
|
||||
(line.amount.amount, line.foreign_amount)
|
||||
})
|
||||
};
|
||||
let dec = |s: &str| s.parse::<Decimal>().unwrap();
|
||||
// 100.00 EUR x 1.0834567 = 108.34567 USD -> 108.35.
|
||||
assert_eq!(
|
||||
book(Currency::USD, Currency::EUR, "100.00", "1.0834567").unwrap(),
|
||||
(dec("108.35"), Some(dec("100.00")))
|
||||
);
|
||||
// 10.00 USD x 151.237 = 1512.37 JPY -> 1512 (yen have no minor unit).
|
||||
assert_eq!(
|
||||
book(Currency::JPY, Currency::USD, "10.00", "151.237")
|
||||
.unwrap()
|
||||
.0,
|
||||
dec("1512")
|
||||
);
|
||||
// Exactly half a cent rounds away from zero.
|
||||
assert_eq!(
|
||||
book(Currency::USD, Currency::EUR, "1", "0.125").unwrap().0,
|
||||
dec("0.13")
|
||||
);
|
||||
// Rounds to nothing: refused.
|
||||
assert!(book(Currency::USD, Currency::EUR, "0.001", "1").is_err());
|
||||
}
|
||||
Reference in new issue
Block a user