proto becomes bxcore, the one shared crate (M5b task 00)
Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
//! Tests for audit and session log records against JSONL fixtures. Do not edit these or the fixtures.
|
||||
|
||||
use bxcore::{
|
||||
ApprovalAnswer, AuditEvent, AuditRecord, CallId, DataClass, DecisionRecord, DenyReason, Epoch,
|
||||
Hash32, LogRecord, ResultStatus, SessionId, Timestamp, ToolCall,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::fmt::Debug;
|
||||
|
||||
const SEQ_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
||||
const REV_HEX: &str = "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100";
|
||||
|
||||
fn ts(s: &str) -> Timestamp {
|
||||
Timestamp::parse(s).unwrap()
|
||||
}
|
||||
|
||||
/// Line `i` of the fixture must decode to `want[i]`, and `want[i]` must encode to exactly that line.
|
||||
fn check<T: Serialize + DeserializeOwned + PartialEq + Debug>(name: &str, want: &[T]) {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/records/{name}",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"));
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
assert_eq!(lines.len(), want.len(), "{name}: number of lines");
|
||||
for (i, (line, want)) in lines.iter().zip(want).enumerate() {
|
||||
let got: T = serde_json::from_str(line).unwrap_or_else(|e| panic!("{name}:{}: {e}", i + 1));
|
||||
assert_eq!(&got, want, "{name}:{}: decoded value", i + 1);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(want).unwrap(),
|
||||
line,
|
||||
"{name}:{}: encoded bytes",
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn sid(s: &str) -> SessionId {
|
||||
SessionId::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn record(seq: u64, time: &str, prev: Hash32, event: AuditEvent) -> AuditRecord {
|
||||
AuditRecord {
|
||||
seq,
|
||||
time: ts(time),
|
||||
prev,
|
||||
event,
|
||||
}
|
||||
}
|
||||
|
||||
/// One line per event variant, every `DecisionRecord` variant, and every option both set and unset.
|
||||
#[test]
|
||||
fn audit_records() {
|
||||
let seq_hash = Hash32::from_hex(SEQ_HEX).unwrap();
|
||||
let rev_hash = Hash32::from_hex(REV_HEX).unwrap();
|
||||
let want = [
|
||||
record(
|
||||
0,
|
||||
"2026-09-17T08:05:00.000Z",
|
||||
Hash32::ZERO,
|
||||
AuditEvent::Decision {
|
||||
session: sid("mm-thread-42"),
|
||||
call: CallId(1),
|
||||
tool: "read_file".to_string(),
|
||||
arguments: r#"{"path":"/etc/hosts"}"#.to_string(),
|
||||
outcome: DecisionRecord::Allowed {},
|
||||
grant: Some("read-etc".to_string()),
|
||||
grant_sha256: Some(seq_hash),
|
||||
taint: DataClass::Private,
|
||||
untrusted: false,
|
||||
},
|
||||
),
|
||||
record(
|
||||
1,
|
||||
"2026-09-17T08:05:00.500Z",
|
||||
seq_hash,
|
||||
AuditEvent::Result {
|
||||
session: sid("mm-thread-42"),
|
||||
call: CallId(1),
|
||||
decision: 0,
|
||||
status: ResultStatus::Result,
|
||||
class: DataClass::Secret,
|
||||
untrusted: true,
|
||||
truncated: true,
|
||||
bytes: 65536,
|
||||
sha256: rev_hash,
|
||||
taint_after: DataClass::Secret,
|
||||
},
|
||||
),
|
||||
record(
|
||||
2,
|
||||
"2026-09-17T08:05:01.250Z",
|
||||
rev_hash,
|
||||
AuditEvent::Decision {
|
||||
session: sid("mm-thread-42"),
|
||||
call: CallId(2),
|
||||
tool: "shell".to_string(),
|
||||
arguments: r#"{"command":"df -h"}"#.to_string(),
|
||||
outcome: DecisionRecord::Ask {},
|
||||
grant: Some("shell-ask".to_string()),
|
||||
grant_sha256: Some(rev_hash),
|
||||
taint: DataClass::Secret,
|
||||
untrusted: true,
|
||||
},
|
||||
),
|
||||
record(
|
||||
3,
|
||||
"2026-09-17T08:06:00.000Z",
|
||||
seq_hash,
|
||||
AuditEvent::Approval {
|
||||
session: sid("mm-thread-42"),
|
||||
call: CallId(2),
|
||||
decision: 2,
|
||||
answer: ApprovalAnswer::Approved,
|
||||
by: Some("u8f3k2".to_string()),
|
||||
post: Some("p9x7".to_string()),
|
||||
reason: None,
|
||||
outcome: DecisionRecord::Allowed {},
|
||||
grant: Some("shell-auto".to_string()),
|
||||
grant_sha256: Some(seq_hash),
|
||||
taint: DataClass::Secret,
|
||||
untrusted: true,
|
||||
},
|
||||
),
|
||||
record(
|
||||
4,
|
||||
"2026-09-17T08:06:00.100Z",
|
||||
rev_hash,
|
||||
AuditEvent::Result {
|
||||
session: sid("mm-thread-42"),
|
||||
call: CallId(2),
|
||||
decision: 2,
|
||||
status: ResultStatus::Failed,
|
||||
class: DataClass::Private,
|
||||
untrusted: false,
|
||||
truncated: false,
|
||||
bytes: 27,
|
||||
sha256: seq_hash,
|
||||
taint_after: DataClass::Secret,
|
||||
},
|
||||
),
|
||||
record(
|
||||
5,
|
||||
"2026-09-17T08:07:00.000Z",
|
||||
seq_hash,
|
||||
AuditEvent::Decision {
|
||||
session: sid("cron-morning"),
|
||||
call: CallId(1),
|
||||
tool: "rm_rf".to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
outcome: DecisionRecord::Denied {
|
||||
reason: DenyReason::NoGrant,
|
||||
},
|
||||
grant: None,
|
||||
grant_sha256: None,
|
||||
taint: DataClass::Private,
|
||||
untrusted: false,
|
||||
},
|
||||
),
|
||||
record(
|
||||
6,
|
||||
"2026-09-17T08:08:00.000Z",
|
||||
rev_hash,
|
||||
AuditEvent::Approval {
|
||||
session: sid("cron-morning"),
|
||||
call: CallId(3),
|
||||
decision: 4,
|
||||
answer: ApprovalAnswer::Refused,
|
||||
by: Some("bxctl".to_string()),
|
||||
post: None,
|
||||
reason: Some("not \"now\"".to_string()),
|
||||
outcome: DecisionRecord::Denied {
|
||||
reason: DenyReason::ApprovalRefused,
|
||||
},
|
||||
grant: None,
|
||||
grant_sha256: None,
|
||||
taint: DataClass::Private,
|
||||
untrusted: false,
|
||||
},
|
||||
),
|
||||
record(
|
||||
7,
|
||||
"2026-09-17T08:23:00.000Z",
|
||||
seq_hash,
|
||||
AuditEvent::Approval {
|
||||
session: sid("cron-morning"),
|
||||
call: CallId(4),
|
||||
decision: 5,
|
||||
answer: ApprovalAnswer::Expired,
|
||||
by: None,
|
||||
post: None,
|
||||
reason: None,
|
||||
outcome: DecisionRecord::Denied {
|
||||
reason: DenyReason::ApprovalExpired,
|
||||
},
|
||||
grant: None,
|
||||
grant_sha256: None,
|
||||
taint: DataClass::Public,
|
||||
untrusted: false,
|
||||
},
|
||||
),
|
||||
record(
|
||||
8,
|
||||
"2026-09-18T00:00:00.000Z",
|
||||
rev_hash,
|
||||
AuditEvent::Recovery {
|
||||
torn_bytes: 117,
|
||||
torn_sha256: seq_hash,
|
||||
},
|
||||
),
|
||||
record(
|
||||
9,
|
||||
"2026-09-18T00:00:01.000Z",
|
||||
seq_hash,
|
||||
AuditEvent::AcceptedBreak {
|
||||
file: "2026-09-17.jsonl".to_string(),
|
||||
line: 7,
|
||||
last_good: rev_hash,
|
||||
},
|
||||
),
|
||||
];
|
||||
check("audit.jsonl", &want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_log_records() {
|
||||
let want = [
|
||||
LogRecord::SessionStart {
|
||||
time: ts("2026-09-17T08:05:00.000Z"),
|
||||
session: SessionId::new("mm-thread-42").unwrap(),
|
||||
epoch: Epoch(0),
|
||||
slot: 0,
|
||||
baseline: Hash32::from_hex(SEQ_HEX).unwrap(),
|
||||
},
|
||||
LogRecord::User {
|
||||
time: ts("2026-09-17T08:05:01.000Z"),
|
||||
content: "What is in /etc/hosts?".to_string(),
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts("2026-09-17T08:05:03.000Z"),
|
||||
content: None,
|
||||
reasoning_content: Some("The user wants a file.\nI will read it.".to_string()),
|
||||
tool_calls: vec![ToolCall {
|
||||
id: "call_a1".to_string(),
|
||||
name: "read_file".to_string(),
|
||||
arguments: r#"{"path":"/etc/hosts"}"#.to_string(),
|
||||
}],
|
||||
},
|
||||
LogRecord::ToolResult {
|
||||
time: ts("2026-09-17T08:05:04.000Z"),
|
||||
call: CallId(1),
|
||||
tool_call_id: "call_a1".to_string(),
|
||||
content: "127.0.0.1 localhost\n".to_string(),
|
||||
class: DataClass::Private,
|
||||
untrusted: true,
|
||||
truncated: false,
|
||||
},
|
||||
LogRecord::Assistant {
|
||||
time: ts("2026-09-17T08:05:06.000Z"),
|
||||
content: Some("It maps localhost to 127.0.0.1.".to_string()),
|
||||
reasoning_content: None,
|
||||
tool_calls: vec![],
|
||||
},
|
||||
LogRecord::CacheLoss {
|
||||
time: ts("2026-09-17T09:00:00.000Z"),
|
||||
expected: 30695,
|
||||
got: 0,
|
||||
},
|
||||
LogRecord::EpochEnd {
|
||||
time: ts("2026-09-17T12:00:00.000Z"),
|
||||
next: Epoch(1),
|
||||
summary: "Looked at /etc/hosts.".to_string(),
|
||||
},
|
||||
];
|
||||
check("session.jsonl", &want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_and_types_are_rejected() {
|
||||
let user = r#"{"type":"user","time":"2026-09-17T08:05:01.000Z","content":"hi"}"#;
|
||||
assert!(serde_json::from_str::<LogRecord>(user).is_ok());
|
||||
let extra = user.replacen("\"content\"", "\"role\":\"user\",\"content\"", 1);
|
||||
assert!(serde_json::from_str::<LogRecord>(&extra).is_err());
|
||||
let unknown_type = user.replacen("\"user\"", "\"system\"", 1);
|
||||
assert!(serde_json::from_str::<LogRecord>(&unknown_type).is_err());
|
||||
|
||||
// A variant with no fields must reject an unknown key too. A serde unit variant would not.
|
||||
for decision in [r#"{"outcome":"allowed"}"#, r#"{"outcome":"ask"}"#] {
|
||||
assert!(serde_json::from_str::<DecisionRecord>(decision).is_ok());
|
||||
let extra = decision.replacen('}', ",\"why\":\"\"}", 1);
|
||||
assert!(
|
||||
serde_json::from_str::<DecisionRecord>(&extra).is_err(),
|
||||
"{extra}"
|
||||
);
|
||||
}
|
||||
let denied = r#"{"outcome":"denied","reason":"no_grant"}"#;
|
||||
assert!(serde_json::from_str::<DecisionRecord>(denied).is_ok());
|
||||
let extra = denied.replacen("\"reason\"", "\"grant\":null,\"reason\"", 1);
|
||||
assert!(serde_json::from_str::<DecisionRecord>(&extra).is_err());
|
||||
let approved = r#"{"outcome":"approved"}"#;
|
||||
assert!(serde_json::from_str::<DecisionRecord>(approved).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user