test(core): every surviving mutant killed or argued equivalent
Max-testing pass, step 8. The final mutation run left 112 survivors (89% caught). Three agents triaged them by file, by the playbook's table: spec the boundary then test it, a law rather than one more pinned value, assert the real value, never weaken a test. Every expected value is hand-computed. - reports.rs (35): 28 killed, chiefly cash flow (inclusive start date, each line's sign and label, a loss on disposal moving to investing, financing designation), general ledger and members' equity date boundaries, and the balanced() checks (a real report nudged by a cent must stop balancing). 7 equivalent: four >/>= after an early return on zero; two in dead code (removed in this commit); one sign of a zero cleaned on output. - ledger.rs, kernel.rs, journal.rs, money.rs (52, plus one new): 49 killed, including every replay_* mutant at the core level (ids, accumulation, status, vendor tags, out-of-order refusal), FX rates, 0%/100% deductible boundaries, a dividend sweep with no income, the legacy member rounding at close, a bill paid on its own date, a liability-account reconciliation, mixed-currency entries (via serde), ISO 4217 minor units, Display/Debug. 3 equivalent: zero refused before a </<= check; a period whose end is in a closed period refused earlier; columns(0). - schedules, tax, analysis, payouts, assets, form1099 (25): 23 killed (schedule length 1-600, recurring end on its start, frequency parsing and stepping, cost of goods in tax net, a zero payout, disposal on the purchase date, quarter starts, labels); 2 equivalent (zero adds zero; days_held is provably >= 1). Two small production cleanups the triage surfaced: kernel::columns(0) returned a negative-zero credit that would print "-0" (regression a_zero_movement_has_no_negative_zero_column, shown to fail before); and cash_flow computed a book value it never used. Spec lines added where a boundary wasn't specified (cash flow's inclusive start and line signs, members' equity start date, payout with no deposit, disposal on the purchase date, bill paid on its own date). `just mutants` now runs core and store tests (the store's replay tests), not the whole workspace, which rebuilt the viewer for every mutant. 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
b2b49267db
commit
bf3f5b4cdb
15 files changed
+989
-5
No files matched your search
@@ -252,6 +252,8 @@ pub struct DisposalAmounts {
|
||||
}
|
||||
|
||||
/// Compute disposal amounts. Pure math; posting is the caller's job.
|
||||
/// A disposal may fall on the purchase date (one day held, accrual being
|
||||
/// inclusive, B-15 in docs/spec/rules.md); one before it is an error.
|
||||
pub fn disposal_amounts(
|
||||
asset: &Asset,
|
||||
date: NaiveDate,
|
||||
|
||||
@@ -233,6 +233,9 @@ pub fn normal_balance<T: Amount>(account_type: AccountType, movement: T) -> Opti
|
||||
pub fn columns<T: Amount>(movement: T) -> Option<(T, T)> {
|
||||
if movement > T::ZERO {
|
||||
Some((movement, T::ZERO))
|
||||
} else if movement == T::ZERO {
|
||||
// Never a negative zero: money never prints "-0".
|
||||
Some((T::ZERO, T::ZERO))
|
||||
} else {
|
||||
Some((T::ZERO, movement.checked_neg()?))
|
||||
}
|
||||
@@ -381,6 +384,64 @@ mod tests {
|
||||
assert_eq!(parts, [1805, 0]);
|
||||
}
|
||||
|
||||
/// The i64 model's line range: up to 2^40 either way, inclusive.
|
||||
#[test]
|
||||
fn i64_line_range_is_two_to_the_fortieth() {
|
||||
assert!((1i64 << 40).in_line_range());
|
||||
assert!((-(1i64 << 40)).in_line_range());
|
||||
assert!(!((1i64 << 40) + 1).in_line_range());
|
||||
assert!(!i64::MIN.in_line_range());
|
||||
}
|
||||
|
||||
/// B-9: a reversal flips every side and keeps accounts and amounts, so
|
||||
/// with the original it nets every account to zero.
|
||||
#[test]
|
||||
fn a_reversal_flips_every_line_and_nets_to_zero() {
|
||||
let lines = [
|
||||
(1u8, Side::Debit, 70i64),
|
||||
(2, Side::Credit, 50),
|
||||
(3, Side::Credit, 20),
|
||||
];
|
||||
let rev = reversal(&lines);
|
||||
assert_eq!(rev.len(), lines.len());
|
||||
for (o, r) in lines.iter().zip(&rev) {
|
||||
assert_eq!((r.0, r.1, r.2), (o.0, flip(o.1), o.2));
|
||||
}
|
||||
let both = movements(lines.iter().chain(&rev).copied()).unwrap();
|
||||
assert_eq!(both, vec![(1, 0), (2, 0), (3, 0)]);
|
||||
}
|
||||
|
||||
/// Spec scenario 1's preconditions: parts and remainders pair up, and
|
||||
/// the unit is positive; otherwise nothing is apportioned.
|
||||
#[test]
|
||||
fn apportion_refuses_mismatched_inputs_or_a_nonpositive_unit() {
|
||||
let mut parts = [0i64, 0];
|
||||
assert_eq!(apportion(0, &mut parts, &[0], 1), None);
|
||||
assert_eq!(parts, [0, 0]);
|
||||
let mut none: [i64; 0] = [];
|
||||
assert_eq!(apportion(0, &mut none, &[], 0), None);
|
||||
assert_eq!(apportion(0, &mut none, &[], -1), None);
|
||||
}
|
||||
|
||||
/// With no parts, only a zero whole is fully apportioned.
|
||||
#[test]
|
||||
fn apportion_to_no_parts_accepts_only_zero() {
|
||||
let mut none: [i64; 0] = [];
|
||||
assert_eq!(apportion(0, &mut none, &[], 1), Some(()));
|
||||
assert_eq!(apportion(5, &mut none, &[], 1), None);
|
||||
}
|
||||
|
||||
/// A zero movement's columns are both a plain zero, never "-0" (found
|
||||
/// by mutation triage, 2026-10-13: it returned a negative-zero credit).
|
||||
#[test]
|
||||
fn a_zero_movement_has_no_negative_zero_column() {
|
||||
let (d, c) = columns(Decimal::ZERO).unwrap();
|
||||
assert_eq!(
|
||||
(d.to_string(), c.to_string()),
|
||||
("0".to_string(), "0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credit_line_sides() {
|
||||
assert_eq!(credit_line(5i64), Some((Side::Credit, 5)));
|
||||
|
||||
@@ -1892,6 +1892,8 @@ impl Ledger {
|
||||
/// SCENARIOS:
|
||||
/// 1. IF the amount isn't positive, or exceeds what's still owed: SHOULD
|
||||
/// refuse.
|
||||
/// 2. IF dated before the bill: SHOULD refuse (PaymentBeforeDocument);
|
||||
/// the bill's own date is fine (DECISION, owner 2026-10-13).
|
||||
/// ```
|
||||
pub fn pay_bill(
|
||||
&mut self,
|
||||
|
||||
@@ -171,6 +171,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// ISO 4217 list one (docs/spec/sources/iso-4217.md): one code from
|
||||
/// each exponent class, and the codes with no minor unit.
|
||||
#[test]
|
||||
fn minor_units_follow_iso_4217() {
|
||||
let units = |c: &str| Currency::new(c).unwrap().minor_units();
|
||||
assert_eq!(units("USD"), Some(2));
|
||||
assert_eq!(units("JPY"), Some(0));
|
||||
assert_eq!(units("BHD"), Some(3));
|
||||
assert_eq!(units("KWD"), Some(3));
|
||||
assert_eq!(units("CLF"), Some(4));
|
||||
assert_eq!(units("UYW"), Some(4));
|
||||
assert_eq!(units("XAU"), None);
|
||||
assert_eq!(units("XDR"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency_and_money_print_their_code_and_amount() {
|
||||
assert_eq!(Currency::new("usd").unwrap().to_string(), "USD");
|
||||
assert_eq!(format!("{:?}", Currency::EUR), "Currency(EUR)");
|
||||
assert_eq!(usd(d(1250)).to_string(), "12.50 USD");
|
||||
assert_eq!(format!("{:?}", usd(d(-5))), "-0.05 USD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_zero_means_exactly_zero() {
|
||||
assert!(Money::zero(Currency::USD).is_zero());
|
||||
assert!(usd(d(0)).is_zero());
|
||||
assert!(!usd(d(1)).is_zero());
|
||||
assert!(!usd(d(-1)).is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_arithmetic_rejects_currency_mismatch() {
|
||||
let eur = Money::new(d(100), Currency::EUR);
|
||||
|
||||
@@ -84,6 +84,8 @@ pub type PayoutPlanLine = (AccountId, Side, Decimal, Option<ClassId>);
|
||||
/// revenue or liability accounts, e.g. deferred revenue or sales tax
|
||||
/// collected; fees debit expense accounts), and the payout reconciles to
|
||||
/// the cent.
|
||||
/// A zero deposit (fees and adjustments ate the sales exactly) has no
|
||||
/// deposit line: every planned line is positive (B-17, docs/spec/rules.md).
|
||||
pub fn payout_plan(
|
||||
payout: &Payout,
|
||||
chart: &ChartOfAccounts,
|
||||
|
||||
@@ -826,6 +826,7 @@ pub struct MembersEquityStatement {
|
||||
/// 3. Unclosed earnings and the period's income MUST be split by share,
|
||||
/// to the cent, by largest remainder (DECISION, owner 2026-10-13).
|
||||
/// 4. NEVER hide an unexplained change: it goes in "other".
|
||||
/// 5. An entry dated `start` is period activity, not beginning capital.
|
||||
/// SCENARIOS:
|
||||
/// 1. IF shares don't sum to 1, or are not positive: SHOULD refuse.
|
||||
/// ```
|
||||
@@ -1111,6 +1112,12 @@ pub struct CashFlowStatement {
|
||||
/// 4. Financing MUST hold changes in equity (other than earnings) and in
|
||||
/// the liabilities named as financing.
|
||||
/// 5. NEVER count closing entries.
|
||||
/// 6. The period is `[start, end]`, inclusive: an entry dated `start` is
|
||||
/// period activity, not part of the beginning cash balance.
|
||||
/// 7. Every line's amount is its cash effect: positive is cash in. Its
|
||||
/// label names the direction the balance moved ("Increase in" /
|
||||
/// "Decrease in"; "Proceeds from" / "Repayment of" for financing
|
||||
/// debt; "Owner contributions" / "Owner distributions" for equity).
|
||||
/// ```
|
||||
pub fn cash_flow(
|
||||
ledger: &Ledger,
|
||||
@@ -1330,10 +1337,8 @@ pub fn cash_flow(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let book_value = cost_amount - accum_debit;
|
||||
// Sanity: the entry balances, so proceeds + accum debit = cost + gain - loss.
|
||||
debug_assert!((proceeds + accum_debit - cost_amount - gain + loss).is_zero());
|
||||
let _ = book_value;
|
||||
disposal_gain_loss += gain - loss;
|
||||
investing.push(CashFlowLine {
|
||||
label: format!("Sale of fixed assets — {proceeds_label}"),
|
||||
|
||||
Reference in new issue
Block a user