mcp: tests for the settings beside the books, year earnings (open periods, last year's partial close), year 1, a failed flush; drop by-class P&L's dead placeholder guard; record config, MCP, GUI mutation scores
check / check (push) Canceled after 0s

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-14 20:45:49 -06:00
1 parent ca3a279219
commit efa370dfa3
4 files changed
+112 -5

No files matched your search

+40 -1
View File
@@ -3676,7 +3676,7 @@ impl BooksServer {
} }
for line in entry.lines() { for line in entry.lines() {
let account = match ledger.chart().get(line.account) { let account = match ledger.chart().get(line.account) {
Some(a) if !a.placeholder => a, Some(a) => a,
_ => continue, _ => continue,
}; };
let signed = match line.side { let signed = match line.side {
@@ -4589,6 +4589,7 @@ mod tests {
fn periods_are_a_year_and_a_month() { fn periods_are_a_year_and_a_month() {
assert_eq!(parse_period_mcp("2026-01"), Ok((2026, 1))); assert_eq!(parse_period_mcp("2026-01"), Ok((2026, 1)));
assert_eq!(parse_period_mcp("2026-12"), Ok((2026, 12))); assert_eq!(parse_period_mcp("2026-12"), Ok((2026, 12)));
assert_eq!(parse_period_mcp("0001-01"), Ok((1, 1)));
for bad in [ for bad in [
"2026-13", "2026-13",
"2026-00", "2026-00",
@@ -4713,6 +4714,44 @@ mod tests {
); );
} }
/// Only a closed period moves where the unclosed part starts: an open
/// one dated before as_of doesn't, and a close that ended before the
/// fiscal year began leaves the year starting on its first day (last
/// December's income is never this year's).
#[test]
fn year_earnings_count_only_closes_and_never_before_the_year() {
let mut s = store("earnings-open");
let cash = s
.add_account("1000", "Cash", AccountType::Asset, None, false)
.unwrap();
let sales = s
.add_account("4000", "Sales", AccountType::Revenue, None, false)
.unwrap();
post(&mut s, d(2025, 12, 15), cash, sales, "999.00");
post(&mut s, d(2026, 1, 10), cash, sales, "1000.00");
s.create_period("2026-01", d(2026, 1, 1), d(2026, 1, 31))
.unwrap();
let server = BooksServer::new(s, ReceiptPolicy::default());
let as_of = d(2026, 2, 28);
let thousand = Decimal::from_str("1000.00").unwrap();
{
let store = server.lock().unwrap();
let e = server.year_earnings(store.ledger(), as_of).unwrap();
assert_eq!(e.unclosed, thousand, "an open January closes nothing");
}
{
let mut store = server.lock().unwrap();
let to_nov = store
.create_period("2025-01..11", d(2025, 1, 1), d(2025, 11, 30))
.unwrap();
store.close_period(to_nov).unwrap();
}
let store = server.lock().unwrap();
let e = server.year_earnings(store.ledger(), as_of).unwrap();
assert_eq!(e.net_income_ytd, thousand);
assert_eq!(e.unclosed, thousand, "December 2025 is not this year's");
}
/// Unlinking a receipt is refused only from a posted, non-reversal /// Unlinking a receipt is refused only from a posted, non-reversal
/// entry the policy covers (crate::policy). /// entry the policy covers (crate::policy).
#[test] #[test]
+39
View File
@@ -194,4 +194,43 @@ mod tests {
); );
assert_eq!(classify(" "), Line::Skip); assert_eq!(classify(" "), Line::Skip);
} }
/// Counts the lines written to it; every flush fails.
struct FailingFlush(std::sync::Arc<std::sync::atomic::AtomicUsize>);
impl AsyncWrite for FailingFlush {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
let lines = buf.iter().filter(|&&b| b == b'\n').count();
self.0.fetch_add(lines, std::sync::atomic::Ordering::SeqCst);
std::task::Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Err(std::io::Error::other("closed")))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
}
/// A client that can't take a flushed reply is gone: the writer stops
/// after the first, rather than writing on into a dead channel.
#[tokio::test]
async fn a_failed_flush_stops_the_writer() {
let written = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let input = std::io::Cursor::new(b"not json\nnor this\n".to_vec());
let (rmcp_in, rmcp_out, writer) = spawn(input, FailingFlush(written.clone()));
drop((rmcp_in, rmcp_out));
writer.await.unwrap();
assert_eq!(written.load(std::sync::atomic::Ordering::SeqCst), 1);
}
} }
+25
View File
@@ -116,6 +116,31 @@ fn the_settings_beside_the_books_are_read() {
); );
} }
/// The cash flow roles in wanderbooks.toml beside the data folder are
/// read without --books-toml: its cash account lets cash_flow run with no
/// cash_accounts of its own.
#[test]
fn the_cash_roles_beside_the_books_are_read() {
let dir = temp("roles");
std::fs::write(
dir.join("wanderbooks.toml"),
"[books]\ncash_accounts = [\"1000\"]\n",
)
.unwrap();
let data = dir.join("books-data");
let mut store = ledger_store::Store::create(&data, ledger_core::Currency::USD).unwrap();
store
.add_account("1000", "Cash", ledger_core::AccountType::Asset, None, false)
.unwrap();
drop(store);
let reply = one_call(
Command::new(BIN).arg("--data-dir").arg(&data),
"books.cash_flow",
serde_json::json!({"start": "2026-01-01", "end": "2026-03-31"}),
);
assert!(reply.contains("\"isError\":false"), "{reply}");
}
/// A writer that keeps the books is a conflict: the server gives up with /// A writer that keeps the books is a conflict: the server gives up with
/// an error (after its ten-second wait), never hangs. /// an error (after its ten-second wait), never hangs.
#[test] #[test]
+8 -4
View File
@@ -3,9 +3,10 @@
How wanderbooks knows its books are right. Strongest oracle first: a 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. 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 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 comes from are in `docs/spec/`. Every crate is covered: `ledger-core`, the
the store in full; the import crate in progress; MCP, config, and GUI store, import, config, and MCP in full; the GUI where it computes and
next (gap list). writes numbers (its view model and formatting) and in its read-only
promise.
## Layers ## Layers
@@ -30,6 +31,9 @@ next (gap list).
| 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 | | 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 |
| Mutation (store) | can the tests fail? | `ledger-store` (2026-10-13: 357 caught + 1 timeout of 365 viable, from 82%; three of its seven survivors then killed; four equivalent left: `sort_value`'s two arms, `OpenFlags` `|` vs `^`, the long form of a sweepless close) | `just mutants-store` | ~12 min | weekly | | Mutation (store) | can the tests fail? | `ledger-store` (2026-10-13: 357 caught + 1 timeout of 365 viable, from 82%; three of its seven survivors then killed; four equivalent left: `sort_value`'s two arms, `OpenFlags` `|` vs `^`, the long form of a sweepless close) | `just mutants-store` | ~12 min | weekly |
| Mutation (import) | can the tests fail? | `ledger-import` (2026-10-13: from 91 of 171 (53%) to 208 of 239 viable on the full run; the 31 survivors (XLSX layout, borders, alignment, exact-text grouping, IIF transliteration, tax-summary optional sections, day-equals-month dates) then killed by targeted tests, re-run clean but one: a blank amount cell written unbordered on an unruled row, invisible, so equivalent) | `just mutants-import` | ~1 h | weekly | | Mutation (import) | can the tests fail? | `ledger-import` (2026-10-13: from 91 of 171 (53%) to 208 of 239 viable on the full run; the 31 survivors (XLSX layout, borders, alignment, exact-text grouping, IIF transliteration, tax-summary optional sections, day-equals-month dates) then killed by targeted tests, re-run clean but one: a blank amount cell written unbordered on an unruled row, invisible, so equivalent) | `just mutants-import` | ~1 h | weekly |
| Mutation (config) | can the tests fail? | `ledger-config` (2026-10-14: 55 caught of 55 viable) | `just mutants-config` | minutes | weekly |
| Mutation (MCP) | can the tests fail? | `ledger-mcp` (2026-10-14: from 197 caught + 5 timeouts of 328 viable (62%) to 314 caught + 2 timeouts of 331; of its 15 survivors, five then killed by targeted tests (the settings file beside the books, year earnings with an open period and with last year's partial close, year 1, a failed flush), one dead guard removed (`profit_and_loss_by_class` skipping placeholders, which never take postings), nine equivalent: `money` rescaling an amount already at two places, the lock wait's deadline instant, `year_earnings` starting the day the year starts either way, the bank import's rehearsal sides (a line's side can't make a draft refuse) and its zero-amount side (zero rows are dropped by the parser), a disposal's zero gain adding zero, and the CPA package index summing debits instead of credits (entries balance)) | `just mutants-mcp` | ~1 h | weekly |
| Mutation (GUI) | can the tests fail? | `ledger-gui` view model and formatting (2026-10-14: 140 caught of 176 viable on the full run, then 92 of 99 on a targeted rerun of the money paths after new tests; of its 7 survivors, one then killed by a test (burn over exactly one month, confirmed by a rerun of `compute_kpis`'s 48 mutants), two dead arms removed (`parties`' bill arms: the ledger tags bill entries with their vendor, which wins), four equivalent: an entry's debits vs its credits (they balance), the three-month burn boundary (both paths give 3), burn over zero covered months (only when the first entry is after the window, which then holds no loss), and `compact`'s sign test at zero (zero prints "$0" unsigned either way)) | `just mutants-gui` | ~2 h | weekly |
CI: `.github/workflows/check.yml` runs `just check` on every push; CI: `.github/workflows/check.yml` runs `just check` on every push;
`scheduled.yml` runs proofs, the oracle, fuzzing, and mutation weekly. `scheduled.yml` runs proofs, the oracle, fuzzing, and mutation weekly.
@@ -113,4 +117,4 @@ B-14) are the first.
| ASC paragraphs not yet read | DONE (2026-10-14) | read in the ASC basic view (`docs/spec/sources/fasb-asc.md`): all confirm the rules; one citation fixed (230-10-45-14(b), not (2)) | | ASC paragraphs not yet read | DONE (2026-10-14) | read in the ASC basic view (`docs/spec/sources/fasb-asc.md`): all confirm the rules; one citation fixed (230-10-45-14(b), not (2)) |
| GnuCash's own close | OPEN | the oracle books closes as ordinary entries; comparing GnuCash's Close Book needs its Scheme API | | GnuCash's own close | OPEN | the oracle books closes as ordinary entries; comparing GnuCash's Close Book needs its Scheme API |
| Import: spec-blind suite, footing properties, mutation | DONE | sources vendored (Intuit bank CSV and IIF, ECMA-376/Excel numbers, IRS form lines); specs on every module; tests/spec_import.rs (spec-blind, footing and round-trip properties, XLSX read back from its XML, layout and styles); fuzz_import_csv; templates checked against the engine's control accounts; mutation one equivalent left | | Import: spec-blind suite, footing properties, mutation | DONE | sources vendored (Intuit bank CSV and IIF, ECMA-376/Excel numbers, IRS form lines); specs on every module; tests/spec_import.rs (spec-blind, footing and round-trip properties, XLSX read back from its XML, layout and styles); fuzz_import_csv; templates checked against the engine's control accounts; mutation one equivalent left |
| MCP, config, GUI crates | NEXT | the same playbook | | MCP, config, GUI crates | DONE (2026-10-14) | the same playbook: sourced module specs (config: Pub 538, Treas. Reg. 301.7701-3; MCP: the MCP tool spec and JSON-RPC 2.0); spec-blind suites `spec_config.rs`, `spec_mcp.rs`, `spec_gui.rs`; the MCP oracle `oracle_mcp.rs` (every export against ledger-core); startup, HTTP concurrency, and the viewer's read-only promise (`read_only.rs`) tested; `fuzz_config_parse`; mutation above |