M5b plan: task 01, the invalid-arguments detail

Task file and 13 given files (2 new: the wire fixture with a detail and
brokerd/tests/invalid_detail.rs; 11 new versions of existing tests, most
only adding `detail: None`).

Checked against a reference in a scratch worktree with its own target/,
on titan (rustc 1.98.1, clippy 0.1.98): make gate prints gate: ok with
956 tests (944 + 12 new); /tmp has no new entries after the gate. All 21
quoted before-blocks occur once in master, and applying the edits to
master gives the reference byte for byte. The given files are the
reference's, formatted, none over 500 lines; no chat-template tag in
the plan.

Before the edits, strict's envelopes_reject_unknown_keys_at_every_depth
fails and ten test targets do not compile, as step 2 lists.

Mutations: 21 one-line breaks of the reference; 20 caught by the given
tests. The one not caught (dropping `default` from the detail attribute)
cannot change behaviour: serde reads a missing Option field as None.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 02:35:35 -07:00
co-authored by Claude Opus 5.5
parent 56b969d55b
commit fa5bcae503
14 changed files with 4338 additions and 0 deletions
@@ -0,0 +1 @@
{"v":1,"id":9,"final":true,"msg":{"kind":"tool_response","body":{"status":"denied","reason":"invalid_arguments","detail":"invalid argument shape: unknown field `file`, expected `text`"}}}
@@ -0,0 +1,151 @@
//! Every JSON object in every fixture must reject an unknown key. Do not edit.
//!
//! The other test files check unknown fields in a few hand-picked places. This one checks all of
//! them: it walks each fixture, adds one unknown key to one object at a time, at every depth, and
//! requires that the result no longer decodes.
use bxcore::{AuditRecord, Envelope, Grant, LogRecord};
use serde::de::DeserializeOwned;
use serde_json::Value;
/// Every copy of `value` that has exactly one extra key in exactly one object.
fn with_one_unknown_key(value: &Value) -> Vec<Value> {
let mut out = Vec::new();
match value {
Value::Object(map) => {
let mut extended = map.clone();
extended.insert("zz_unknown".to_string(), Value::Bool(true));
out.push(Value::Object(extended));
for (key, child) in map {
for changed in with_one_unknown_key(child) {
let mut copy = map.clone();
copy.insert(key.clone(), changed);
out.push(Value::Object(copy));
}
}
}
Value::Array(items) => {
for (i, child) in items.iter().enumerate() {
for changed in with_one_unknown_key(child) {
let mut copy = items.clone();
copy[i] = changed;
out.push(Value::Array(copy));
}
}
}
_ => {}
}
out
}
/// Returns how many variations were tried, so callers can check the walk reached nested objects.
fn check<T: DeserializeOwned>(what: &str, text: &str) -> usize {
let value: Value = serde_json::from_str(text).unwrap_or_else(|e| panic!("{what}: {e}"));
assert!(
serde_json::from_value::<T>(value.clone()).is_ok(),
"{what}: fixture must decode"
);
let variations = with_one_unknown_key(&value);
for changed in &variations {
assert!(
serde_json::from_value::<T>(changed.clone()).is_err(),
"{what}: accepted an unknown key: {changed}"
);
}
variations.len()
}
fn fixture(path: &str) -> String {
let full = format!("{}/tests/fixtures/{path}", env!("CARGO_MANIFEST_DIR"));
std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full}: {e}"))
}
#[test]
fn envelopes_reject_unknown_keys_at_every_depth() {
for name in [
"tool_request.json",
"tool_response_pending.json",
"tool_response_result.json",
"tool_response_failed.json",
"tool_response_denied.json",
"tool_response_denied_detail.json",
"error.json",
"turn.json",
"turn_event_tool_result.json",
"turn_event_content.json",
"turn_event_retrying.json",
"turn_done.json",
"error_session_full.json",
"approvals.json",
"approval_list.json",
"approval_list_empty.json",
"approve.json",
"approve_result_allowed.json",
"approve_result_denied.json",
"refuse.json",
"refuse_no_reason.json",
"ok.json",
"check_grants.json",
"grants_report.json",
"grants_report_ok.json",
"error_forbidden.json",
"error_no_such_approval.json",
"turn_event_approval_pending.json",
"turn_event_tool_denied.json",
] {
// Envelope, msg and body: three objects. Some bodies hold more: turn_done a usage
// object, approval_list one item, approve_result an outcome, grants_report two problems.
let want = match name {
"turn_done.json" | "approval_list.json" => 4,
"approve_result_allowed.json" | "approve_result_denied.json" => 4,
"grants_report.json" => 5,
_ => 3,
};
assert_eq!(
check::<Envelope>(name, &fixture(&format!("wire/{name}"))),
want,
"{name}"
);
}
}
#[test]
fn audit_records_reject_unknown_keys_at_every_depth() {
for (i, line) in fixture("records/audit.jsonl").lines().enumerate() {
// The record and its event: two objects. Decision and approval events also hold an
// outcome object.
let want = if line.contains("\"outcome\"") { 3 } else { 2 };
assert_eq!(
check::<AuditRecord>(&format!("audit.jsonl:{}", i + 1), line),
want
);
}
}
#[test]
fn log_records_reject_unknown_keys_at_every_depth() {
let mut tried = 0;
for (i, line) in fixture("records/session.jsonl").lines().enumerate() {
tried += check::<LogRecord>(&format!("session.jsonl:{}", i + 1), line);
}
// Seven records, plus the one tool call inside the first assistant record.
assert_eq!(tried, 8);
}
#[test]
fn usage_log_records_reject_unknown_keys_at_every_depth() {
let mut tried = 0;
for (i, line) in fixture("records/session_usage.jsonl").lines().enumerate() {
tried += check::<LogRecord>(&format!("session_usage.jsonl:{}", i + 1), line);
}
// Seven records, plus the one tool call inside the first assistant record.
assert_eq!(tried, 8);
}
#[test]
fn grants_reject_unknown_keys_at_every_depth() {
let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap();
let text = serde_json::to_string(&grant).unwrap();
// The grant and its constraints: two objects.
assert_eq!(check::<Grant>("full.toml as JSON", &text), 2);
}
@@ -0,0 +1,219 @@
//! Tests for IPC messages against byte-exact fixtures. Do not edit these or the fixtures.
use bxcore::{
CallId, DataClass, DenyReason, Envelope, ErrorCode, Message, SessionId, Timestamp, ToolRequest,
ToolResponse, WireError,
};
fn fixture(name: &str) -> String {
let path = format!("{}/tests/fixtures/wire/{name}", env!("CARGO_MANIFEST_DIR"));
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"));
text.trim_end_matches('\n').to_string()
}
/// The fixture must decode to `want`, and `want` must encode to exactly the fixture's bytes.
fn check(name: &str, want: Envelope) {
let text = fixture(name);
let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(got, want, "{name}: decoded value");
assert_eq!(
serde_json::to_string(&want).unwrap(),
text,
"{name}: encoded bytes"
);
}
fn response(id: u64, r#final: bool, body: ToolResponse) -> Envelope {
Envelope {
v: 1,
id,
r#final,
msg: Message::ToolResponse(body),
}
}
#[test]
fn tool_request() {
let body = ToolRequest {
session: SessionId::new("mm-thread-42").unwrap(),
call: CallId(3),
tool: "read_file".to_string(),
arguments: r#"{"path":"/etc/hosts"}"#.to_string(),
};
check(
"tool_request.json",
Envelope {
v: 1,
id: 7,
r#final: true,
msg: Message::ToolRequest(body),
},
);
}
#[test]
fn tool_response_pending() {
let body = ToolResponse::PendingApproval {
approval: 41,
expires: Timestamp::parse("2026-09-17T08:35:00.000Z").unwrap(),
};
check("tool_response_pending.json", response(7, false, body));
}
#[test]
fn an_approval_id_is_a_number_not_a_string() {
let good = fixture("tool_response_pending.json");
let bad = good.replacen("\"approval\":41", "\"approval\":\"41\"", 1);
assert_ne!(good, bad);
assert!(serde_json::from_str::<Envelope>(&good).is_ok());
assert!(serde_json::from_str::<Envelope>(&bad).is_err());
let negative = good.replacen("\"approval\":41", "\"approval\":-1", 1);
assert!(serde_json::from_str::<Envelope>(&negative).is_err());
}
#[test]
fn tool_response_result() {
let body = ToolResponse::Result {
content: "127.0.0.1 localhost\n".to_string(),
class: DataClass::Private,
untrusted: true,
truncated: false,
};
check("tool_response_result.json", response(7, true, body));
}
#[test]
fn tool_response_failed() {
let body = ToolResponse::Failed {
message: "exit status 2".to_string(),
};
check("tool_response_failed.json", response(8, true, body));
}
#[test]
fn tool_response_denied() {
let body = ToolResponse::Denied {
reason: DenyReason::TaintTooHigh,
detail: None,
};
check("tool_response_denied.json", response(9, true, body));
}
/// A detail is written only when there is one, so the fixture above has no `detail` key.
#[test]
fn tool_response_denied_with_a_detail() {
let body = ToolResponse::Denied {
reason: DenyReason::InvalidArguments,
detail: Some("invalid argument shape: unknown field `file`, expected `text`".to_string()),
};
check("tool_response_denied_detail.json", response(9, true, body));
}
#[test]
fn a_detail_must_be_a_string() {
let good = fixture("tool_response_denied_detail.json");
assert!(serde_json::from_str::<Envelope>(&good).is_ok());
let at = good.find("\"detail\":").unwrap();
for value in ["7", "[\"a\"]", "{}", "true"] {
let bad = format!("{}\"detail\":{value}}}}}}}", &good[..at]);
assert!(
serde_json::from_str::<Envelope>(&bad).is_err(),
"accepted {bad}"
);
}
}
#[test]
fn error_message() {
let body = WireError {
code: ErrorCode::BadVersion,
detail: "expected 1".to_string(),
};
check(
"error.json",
Envelope {
v: 1,
id: 0,
r#final: true,
msg: Message::Error(body),
},
);
}
#[test]
fn deny_reasons_and_error_codes_are_snake_case() {
let reasons = [
(DenyReason::NoGrant, "no_grant"),
(DenyReason::GrantExpired, "grant_expired"),
(DenyReason::TaintTooHigh, "taint_too_high"),
(DenyReason::DeniedByGrant, "denied_by_grant"),
(DenyReason::ApprovalRefused, "approval_refused"),
(DenyReason::ApprovalExpired, "approval_expired"),
(DenyReason::GrantsInvalid, "grants_invalid"),
(DenyReason::AuditUnavailable, "audit_unavailable"),
(DenyReason::InvalidArguments, "invalid_arguments"),
(DenyReason::StateUnreadable, "state_unreadable"),
];
for (value, text) in reasons {
assert_eq!(
serde_json::to_string(&value).unwrap(),
format!("\"{text}\"")
);
}
let codes = [
(ErrorCode::BadFrame, "bad_frame"),
(ErrorCode::BadVersion, "bad_version"),
(ErrorCode::BadMessage, "bad_message"),
(ErrorCode::Internal, "internal"),
(ErrorCode::Forbidden, "forbidden"),
(ErrorCode::NoSuchApproval, "no_such_approval"),
];
for (value, text) in codes {
assert_eq!(
serde_json::to_string(&value).unwrap(),
format!("\"{text}\"")
);
}
}
#[test]
fn unknown_and_missing_fields_are_rejected() {
let good = fixture("tool_request.json");
assert!(serde_json::from_str::<Envelope>(&good).is_ok());
let bad = [
// extra field in the envelope
good.replacen("{\"v\":1,", "{\"v\":1,\"extra\":0,", 1),
// extra field beside kind and body
good.replacen(
"\"kind\":\"tool_request\",",
"\"kind\":\"tool_request\",\"x\":1,",
1,
),
// extra field in the body
good.replacen("\"call\":3,", "\"call\":3,\"priority\":9,", 1),
// missing field in the body
good.replacen("\"call\":3,", "", 1),
// missing `final`
good.replacen("\"final\":true,", "", 1),
// unknown kind
good.replacen("tool_request", "tool_demand", 1),
// invalid session id inside a message
good.replacen("mm-thread-42", "../../etc", 1),
];
for text in bad {
assert!(
serde_json::from_str::<Envelope>(&text).is_err(),
"accepted {text}"
);
}
}
#[test]
fn unknown_field_in_a_response_variant_is_rejected() {
let good = fixture("tool_response_denied.json");
let bad = good.replacen("\"reason\":", "\"note\":\"x\",\"reason\":", 1);
assert!(serde_json::from_str::<Envelope>(&good).is_ok());
assert!(serde_json::from_str::<Envelope>(&bad).is_err());
let bad_status = good.replacen("denied", "refused", 1);
assert!(serde_json::from_str::<Envelope>(&bad_status).is_err());
}