store: refusals leave the live books untouched; per-filing names; spec suite
The refusal property gained the converse law (an accepted command journals something; only batch runs may find nothing to do) and checks the store's class and payroll listings. It found a schedule registered with an out-of-range total whose first month was refused after its entry was begun: the live ledger kept a draft the journal never had. - Core: compound commands (post_schedule_month, reverse_entry, close_period_to, create_invoice, create_bill, receive_payment, pay_bill, run_payroll, remit_payroll_taxes) run inside Ledger::atomically, so a refusal partway leaves the ledger exactly as it was. - Core: a schedule total no line could carry, or finer than a cent, and an asset cost beyond the line range, are refused at registration. Replay keeps older schedules (replay_register_schedule). - Store: add_line, add_line_fx, mark_cleared, set_fx_rate, post_schedule_month and close_period_to changed the live ledger before journaling; they now run on a copy, journal what it produced, then swap it in, so a failed write leaves the live books as journaled. New spec-blind suite tests/spec_store.rs (57 tests written from the store's specs alone). Its two findings, adjudicated: - A file filed twice under different names listed the first name and type for both (the projection kept them per blob). Schema v8 gives each attachment its own; older databases backfill and heal by verify. - Withdrawn attachments stay in the store's listing, marked (the MCP listing hides them): the spec and doc said "omit" and are corrected. Also removes journal::truncate_to_last_good_seq: unused, and it dropped everything after the first bad complete line, which the books now treat as corruption for a person, never repaired automatically. 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
27da4b63bf
commit
111a5af470
11 files changed
+2075
-75
No files matched your search
@@ -353,3 +353,57 @@ fn frequencies_parse_and_step_from_the_start() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression (found by the store's refusal property, 2026-10-13): a
|
||||
/// schedule whose total no line could carry, or finer than a cent, is
|
||||
/// refused when registered, not at a month end.
|
||||
#[test]
|
||||
fn a_schedule_total_no_month_could_post_is_refused() {
|
||||
let mut b = books();
|
||||
let new = |total: &str| NewSchedule {
|
||||
description: "Insurance".into(),
|
||||
kind: ScheduleKind::PrepaidExpense,
|
||||
total: usd(total),
|
||||
start: d(1, 1),
|
||||
months: 6,
|
||||
balance_account: b.prepaid,
|
||||
pl_account: b.insurance,
|
||||
};
|
||||
let huge = new("46116860184273879.03");
|
||||
let sub_cent = new("120.005");
|
||||
assert!(matches!(
|
||||
b.l.register_schedule(huge),
|
||||
Err(LedgerError::AmountOutOfRange)
|
||||
));
|
||||
assert!(matches!(
|
||||
b.l.register_schedule(sub_cent),
|
||||
Err(LedgerError::FinerThanMinorUnit { .. })
|
||||
));
|
||||
}
|
||||
|
||||
/// Regression: a schedule month refused after its entry was begun leaves
|
||||
/// the ledger exactly as it was: no half-built draft, no id spent
|
||||
/// (Ledger::atomically). The doomed schedule can only come from an older
|
||||
/// journal now, so it is registered through the replay path.
|
||||
#[test]
|
||||
fn a_refused_schedule_month_leaves_nothing_behind() {
|
||||
let mut b = books();
|
||||
let id =
|
||||
b.l.replay_register_schedule(NewSchedule {
|
||||
description: "Insurance".into(),
|
||||
kind: ScheduleKind::PrepaidExpense,
|
||||
total: usd("46116860184273879.03"),
|
||||
start: d(1, 1),
|
||||
months: 6,
|
||||
balance_account: b.prepaid,
|
||||
pl_account: b.insurance,
|
||||
})
|
||||
.unwrap();
|
||||
let before = b.l.clone();
|
||||
assert!(b.l.post_schedule_month(id, d(1, 31)).is_err());
|
||||
assert!(
|
||||
b.l.differs_from(&before).is_empty(),
|
||||
"{:?}",
|
||||
b.l.differs_from(&before)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user