Hand over the M3a plan: 22 tasks, their files, and the check record
Task files, the files they copy in (byte-identical to the reference on m3a-ref), each area's check record, and a README with the per-task table of what each check exposed. The handoff note is done with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
//! Tests for how `bxctl chat` prints what the model wrote and what `brokerd` denied. Do not edit.
|
||||
|
||||
use bxctl::chat::Printer;
|
||||
use proto::{DataClass, DenyReason, Timestamp, TurnEvent};
|
||||
|
||||
const BACKSLASH: char = '\\';
|
||||
|
||||
/// The escape for one code point, built and never spelled out (see `tests/escape.rs`).
|
||||
fn esc(code: u32) -> String {
|
||||
format!("{BACKSLASH}u{code:04x}")
|
||||
}
|
||||
|
||||
fn print(printer: &mut Printer, events: &[TurnEvent]) -> String {
|
||||
let mut out = Vec::new();
|
||||
for e in events {
|
||||
printer.event(&mut out, e).unwrap();
|
||||
}
|
||||
printer.end_reasoning(&mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
fn denied(name: &str, reason: DenyReason) -> TurnEvent {
|
||||
TurnEvent::ToolDenied {
|
||||
name: name.to_string(),
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_denial_shows_its_reason_by_its_wire_name() {
|
||||
let mut p = Printer::new(true, false);
|
||||
assert_eq!(
|
||||
print(&mut p, &[denied("read_file", DenyReason::NoGrant)]),
|
||||
"[denied read_file: no_grant]\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// Walks all ten reasons: the three that mean the harness is refusing to work carry their
|
||||
/// runbook entry on the next line, and the other seven carry nothing.
|
||||
#[test]
|
||||
fn the_three_fail_closed_denials_point_to_the_runbook_and_no_other_does() {
|
||||
let cases = [
|
||||
(DenyReason::NoGrant, "no_grant", None),
|
||||
(DenyReason::GrantExpired, "grant_expired", None),
|
||||
(DenyReason::TaintTooHigh, "taint_too_high", None),
|
||||
(DenyReason::DeniedByGrant, "denied_by_grant", None),
|
||||
(DenyReason::ApprovalRefused, "approval_refused", None),
|
||||
(DenyReason::ApprovalExpired, "approval_expired", None),
|
||||
(DenyReason::InvalidArguments, "invalid_arguments", None),
|
||||
(
|
||||
DenyReason::GrantsInvalid,
|
||||
"grants_invalid",
|
||||
Some("see docs/runbook.md#grants-invalid"),
|
||||
),
|
||||
(
|
||||
DenyReason::AuditUnavailable,
|
||||
"audit_unavailable",
|
||||
Some("see docs/runbook.md#audit-unavailable"),
|
||||
),
|
||||
(
|
||||
DenyReason::StateUnreadable,
|
||||
"state_unreadable",
|
||||
Some("see docs/runbook.md#broker-state-damaged"),
|
||||
),
|
||||
];
|
||||
for (reason, name, pointer) in cases {
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(&mut p, &[denied("shell", reason)]);
|
||||
let want = match pointer {
|
||||
Some(pointer) => format!("[denied shell: {name}]\n{pointer}\n"),
|
||||
None => format!("[denied shell: {name}]\n"),
|
||||
};
|
||||
assert_eq!(got, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_denial_ends_an_open_reasoning_block_first() {
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[
|
||||
TurnEvent::Reasoning {
|
||||
text: "hm".to_string(),
|
||||
},
|
||||
denied("shell", DenyReason::NoGrant),
|
||||
],
|
||||
);
|
||||
assert_eq!(got, "\x1b[2mhm\x1b[0m\n[denied shell: no_grant]\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_and_content_are_printed_as_data() {
|
||||
let hostile = "a\x1b[8mb\x07c\rd";
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[
|
||||
TurnEvent::Reasoning {
|
||||
text: hostile.to_string(),
|
||||
},
|
||||
TurnEvent::Content {
|
||||
text: hostile.to_string(),
|
||||
},
|
||||
],
|
||||
);
|
||||
let shown = format!("a{}[8mb{}c{}d", esc(0x1b), esc(0x07), esc(0x0d));
|
||||
// The only escape sequences left are the printer's own: dim on, dim off.
|
||||
assert_eq!(got, format!("\x1b[2m{shown}\x1b[0m\n{shown}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invisible_and_direction_changing_characters_are_shown() {
|
||||
let rlo = char::from_u32(0x202e).unwrap();
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[TurnEvent::Content {
|
||||
text: format!("see {rlo}txt.exe"),
|
||||
}],
|
||||
);
|
||||
assert_eq!(got, format!("see {}txt.exe", esc(0x202e)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newlines_and_tabs_in_model_text_pass_through() {
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[TurnEvent::Content {
|
||||
text: "one\n\ttwo\n".to_string(),
|
||||
}],
|
||||
);
|
||||
assert_eq!(got, "one\n\ttwo\n");
|
||||
}
|
||||
|
||||
/// The model chooses tool names too: every place a name is printed escapes it.
|
||||
#[test]
|
||||
fn tool_names_are_printed_as_data_everywhere() {
|
||||
let name = "sh\x1b[2Jell";
|
||||
let shown = format!("sh{}[2Jell", esc(0x1b));
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[
|
||||
TurnEvent::ToolCallStarted {
|
||||
name: name.to_string(),
|
||||
},
|
||||
TurnEvent::ToolResult {
|
||||
name: name.to_string(),
|
||||
class: DataClass::Public,
|
||||
truncated: false,
|
||||
},
|
||||
TurnEvent::ToolResult {
|
||||
name: name.to_string(),
|
||||
class: DataClass::Private,
|
||||
truncated: true,
|
||||
},
|
||||
denied(name, DenyReason::NoGrant),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
got,
|
||||
format!(
|
||||
"[tool {shown}]\n[{shown}: Public]\n[{shown}: Private, truncated]\n\
|
||||
[denied {shown}: no_grant]\n"
|
||||
)
|
||||
);
|
||||
assert!(!got.contains('\x1b'));
|
||||
}
|
||||
|
||||
/// The printer shows nothing for a pending approval: the block comes from `brokerd`, through
|
||||
/// `handle_pending`. It still ends an open reasoning block, so the block is not dimmed.
|
||||
#[test]
|
||||
fn a_pending_approval_prints_nothing_itself_but_ends_the_reasoning_block() {
|
||||
let pending = TurnEvent::ApprovalPending {
|
||||
approval: 41,
|
||||
tool: "read_file".to_string(),
|
||||
expires: Timestamp::parse("2026-09-18T12:15:00.000Z").unwrap(),
|
||||
};
|
||||
let mut p = Printer::new(true, false);
|
||||
assert_eq!(print(&mut p, std::slice::from_ref(&pending)), "");
|
||||
let mut p = Printer::new(true, false);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[
|
||||
TurnEvent::Reasoning {
|
||||
text: "hm".to_string(),
|
||||
},
|
||||
pending,
|
||||
],
|
||||
);
|
||||
assert_eq!(got, "\x1b[2mhm\x1b[0m\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_mode_prints_the_new_events_as_json_lines() {
|
||||
let mut p = Printer::new(true, true);
|
||||
let got = print(
|
||||
&mut p,
|
||||
&[
|
||||
TurnEvent::ApprovalPending {
|
||||
approval: 41,
|
||||
tool: "read_file".to_string(),
|
||||
expires: Timestamp::parse("2026-09-18T12:15:00.000Z").unwrap(),
|
||||
},
|
||||
denied("shell", DenyReason::GrantsInvalid),
|
||||
],
|
||||
);
|
||||
let lines: Vec<serde_json::Value> = got
|
||||
.lines()
|
||||
.map(|l| serde_json::from_str(l).unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
serde_json::json!({"event": "approval_pending", "approval": 41,
|
||||
"tool": "read_file", "expires": "2026-09-18T12:15:00.000Z"}),
|
||||
serde_json::json!({"event": "tool_denied", "name": "shell",
|
||||
"reason": "grants_invalid"}),
|
||||
]
|
||||
);
|
||||
assert!(!got.contains("runbook"), "json mode adds no prose");
|
||||
}
|
||||
Reference in New Issue
Block a user