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
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:
1 parent
ca3a279219
commit
efa370dfa3
4 files changed
+112
-5
No files matched your search
@@ -3676,7 +3676,7 @@ impl BooksServer {
|
||||
}
|
||||
for line in entry.lines() {
|
||||
let account = match ledger.chart().get(line.account) {
|
||||
Some(a) if !a.placeholder => a,
|
||||
Some(a) => a,
|
||||
_ => continue,
|
||||
};
|
||||
let signed = match line.side {
|
||||
@@ -4589,6 +4589,7 @@ mod tests {
|
||||
fn periods_are_a_year_and_a_month() {
|
||||
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("0001-01"), Ok((1, 1)));
|
||||
for bad in [
|
||||
"2026-13",
|
||||
"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
|
||||
/// entry the policy covers (crate::policy).
|
||||
#[test]
|
||||
|
||||
@@ -194,4 +194,43 @@ mod tests {
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// an error (after its ten-second wait), never hangs.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user