test(core): every surviving mutant killed or argued equivalent
check / check (push) Canceled after 0s

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:
swaitsandClaude Opus 5.5 committed 2026-10-13 17:00:45 -06:00
1 parent 8c8ba19471
commit d0490bf106
15 files changed
+989 -5

No files matched your search

+61
View File
@@ -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)));