P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
check / check (push) Canceled after 0s

books.rename_account; account names trimmed, bounded, refused when a
sibling's or TIN-shaped; books.show refuses what its target ignores. A
report asked for without dates covers this fiscal year so far, everywhere
(one rule for tools, descriptions and the viewer; the IIF and CPA package
keep their own dates). policy.require_tax_category warns or refuses; a
Form 4797 gain is shown apart on every return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 10:41:24 -06:00
1 parent 8255ac2103
commit 9ab003d0d1
69 files changed
+8063 -570

No files matched your search

+159
View File
@@ -1553,3 +1553,162 @@ fn old_book_answers_every_tool(dir: &Path, per_tool: usize) -> Vec<String> {
let _ = std::fs::remove_dir_all(&p.base);
failures
}
/// P3 (defects 8, 15, 16): renames, refused report ranges and tax
/// categories at creation, at their edges, are each answered (a result or
/// a refusal, never a stop), and the books reopen after them. A new name
/// is one line of at most 200 characters (ledger_core::account::
/// MAX_ACCOUNT_NAME): 200 is taken and read back whole; 201, a
/// 100,000-character one, a line break or a tab is refused.
#[test]
fn hostile_renames_ranges_and_categories_are_answered() {
let p = places("p3");
let mut m = Mcp::start(&p);
fixture(&mut m, &p);
let refused = |r: &Value| r["isError"] == json!(true);
for (tool, args, refuse) in [
(
"books.rename_account",
json!({"account": "6300", "name": ""}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": " "}),
true,
),
(
"books.rename_account",
json!({"account": "9999", "name": "X"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "insurance"}),
false,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Contract labor"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Policy 123456789"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Policy ٠١٢٣٤٥٦٧٨"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Risk", "reason": "x".repeat(2_001)}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Risk", "reason": "\u{0}"}),
true,
),
(
"books.show",
json!({"account": "1000", "start": "9999-12-31"}),
true,
),
(
"books.show",
json!({"report": "profit_and_loss", "start": "9999-12-31"}),
true,
),
(
"books.show",
json!({"report": "profit_and_loss", "end": "0001-01-01"}),
false,
),
(
"books.show",
json!({"report": "trial_balance", "end": "9999-12-31"}),
false,
),
(
"books.profit_and_loss",
json!({"start": "9999-12-31"}),
true,
),
("books.profit_and_loss", json!({"end": "0001-01-01"}), false),
(
"books.profit_and_loss_by_class",
json!({"start": "2026-12-31", "end": "2026-01-01"}),
true,
),
(
"books.cash_flow",
json!({"start": "9999-12-31", "cash_accounts": ["1000"]}),
true,
),
("books.ar_aging", json!({}), false),
("books.ap_aging", json!({"as_of": "9999-12-31"}), false),
(
"books.add_account",
json!({"code": "4100", "name": "Fees", "account_type": "revenue", "tax_category": "meals"}),
true,
),
(
"books.add_account",
json!({"code": "4101", "name": "Fees", "account_type": "revenue", "deductible_percent": "1e9"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "N".repeat(201)}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "N".repeat(100_000)}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Risk\nPolicy"}),
true,
),
(
"books.rename_account",
json!({"account": "6300", "name": "Risk\tPolicy"}),
true,
),
(
"books.add_account",
json!({"code": "6310", "name": "X".repeat(201), "account_type": "expense"}),
true,
),
(
"books.add_account",
json!({"code": "6311", "name": "Two\nlines", "account_type": "expense"}),
true,
),
] {
let r = pinned(&mut m, tool, args.clone());
assert_eq!(refused(&r), refuse, "{tool} {}: {r}", truncated(&args));
}
// A name of 200 characters: taken, and read back whole.
let long = "N".repeat(200);
let r = pinned(
&mut m,
"books.rename_account",
json!({"account": "1000", "name": long}),
);
assert!(!refused(&r), "{}", truncated(&r));
let chart = m.ok("books.chart", json!({}));
assert!(
chart["accounts"]
.as_array()
.unwrap()
.iter()
.any(|a| a["code"] == "1000" && a["name"].as_str().unwrap() == long)
);
reopens(m, &p, "P3's hostile inputs");
}