Files
boxmaker/docs/plans/M2b/files/crates/loopd/tests/baseline.rs
T
kyleandClaude Fable 5.1 e156975649 Add M2b plan: ten tasks, tests, recordings and the first system prompt
The tasks build the agent loop on M2a's client: channel messages and
the usage record in proto, four config tables, the tool port and
registry with find_tool and call_tool, the baseline and replay, the
session store, the turn loop with its limits and the append-only
property test, the channel server, loopd serve, bxctl chat, and the
device checks including a four-turn conversation with a restart.

Checked against a private reference implementation: the gate passes
after every task in order, the new suites pass under CPU load, and the
reference passes make verify-device on straylight with no cache loss.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 17:19:02 -07:00

235 lines
6.9 KiB
Rust

//! Tests for the baseline and the replay function. Do not edit.
mod support;
use loopd::baseline::{Baseline, messages};
use loopd::llama::ChatMessage;
use loopd::tools::Registry;
use proto::{CallId, DataClass, Epoch, Hash32, LogRecord, SessionId, Timestamp, ToolCall};
use std::path::Path;
use support::Home;
fn ts() -> Timestamp {
Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap()
}
#[test]
fn assembles_system_prompt_and_core_schemas() {
let home = Home::new();
let cfg = home.config(Path::new("/tmp/unused.sock"));
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
assert_eq!(
b.system, "You are Boxmaker, a test agent.",
"trailing newline trimmed"
);
let names: Vec<&str> = b.tools.iter().map(|t| t.name.as_str()).collect();
assert_eq!(
names,
["clock", "find_tool", "call_tool"],
"core tools, then the two meta-tools; not echo"
);
}
#[test]
fn core_memory_is_appended_when_present_and_only_then() {
let home = Home::new();
let cfg = home.config(Path::new("/tmp/unused.sock"));
home.write("memory/core.md", "The owner likes cork.\n\n");
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
assert_eq!(
b.system,
"You are Boxmaker, a test agent.\n\nThe owner likes cork."
);
home.write("memory/core.md", " \n");
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
assert_eq!(
b.system, "You are Boxmaker, a test agent.",
"an empty core file adds nothing"
);
}
#[test]
fn a_missing_system_prompt_is_an_error_naming_the_file() {
let home = Home::new();
let mut cfg = home.config(Path::new("/tmp/unused.sock"));
cfg.baseline.system = home.dir.join("nope.md");
let e = Baseline::assemble(&cfg, &Registry::m2b()).unwrap_err();
assert!(e.to_string().contains("nope.md"), "{e}");
}
#[test]
fn json_round_trip_and_hash() {
let home = Home::new();
let cfg = home.config(Path::new("/tmp/unused.sock"));
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
let text = b.to_json().unwrap();
assert_eq!(Baseline::from_json(&text).unwrap(), b);
assert_eq!(
b.hash().unwrap(),
proto::sha256(text.as_bytes()).unwrap(),
"the hash is of the JSON as written"
);
assert_ne!(b.hash().unwrap(), Hash32::ZERO);
let mut other = b.clone();
other.system.push('!');
assert_ne!(other.hash().unwrap(), b.hash().unwrap());
assert!(
Baseline::from_json(&text.replacen("\"system\"", "\"zz\":1,\"system\"", 1)).is_err(),
"unknown keys are rejected"
);
}
#[test]
fn replay_keeps_only_messages_in_order_and_unchanged() {
let b = Baseline {
system: "sys".to_string(),
tools: vec![],
};
let call = ToolCall {
id: "c1".to_string(),
name: "clock".to_string(),
arguments: "{}".to_string(),
};
let records = vec![
LogRecord::SessionStart {
time: ts(),
session: SessionId::new("s").unwrap(),
epoch: Epoch(0),
slot: 0,
baseline: Hash32::ZERO,
},
LogRecord::User {
time: ts(),
content: "hi".to_string(),
},
LogRecord::Assistant {
time: ts(),
content: None,
reasoning_content: Some("think".to_string()),
tool_calls: vec![call.clone()],
},
LogRecord::Usage {
time: ts(),
cache_n: 1,
prompt_n: 2,
predicted_n: 3,
reasoning_tokens: 1,
thinking_capped: false,
},
LogRecord::ToolResult {
time: ts(),
call: CallId(1),
tool_call_id: "c1".to_string(),
content: "noon".to_string(),
class: DataClass::Public,
untrusted: false,
truncated: false,
},
LogRecord::CacheLoss {
time: ts(),
expected: 10,
got: 0,
},
LogRecord::Assistant {
time: ts(),
content: Some("It is noon.".to_string()),
reasoning_content: None,
tool_calls: vec![],
},
LogRecord::EpochEnd {
time: ts(),
next: Epoch(1),
summary: "x".to_string(),
},
];
let want = vec![
ChatMessage::System {
content: "sys".to_string(),
},
ChatMessage::User {
content: "hi".to_string(),
},
ChatMessage::Assistant {
content: None,
reasoning_content: Some("think".to_string()),
tool_calls: vec![call],
},
ChatMessage::Tool {
tool_call_id: "c1".to_string(),
content: "noon".to_string(),
},
ChatMessage::Assistant {
content: Some("It is noon.".to_string()),
reasoning_content: None,
tool_calls: vec![],
},
];
assert_eq!(messages(&b, &records), want);
assert_eq!(
messages(&b, &[]),
vec![ChatMessage::System {
content: "sys".to_string()
}]
);
}
/// Replaying a prefix of the log gives a prefix of the messages: the function never reorders
/// or rewrites. Checked for every prefix of a generated log.
#[test]
fn replay_of_a_prefix_is_a_prefix() {
let b = Baseline {
system: "sys".to_string(),
tools: vec![],
};
let mut records = Vec::new();
let mut seed: u64 = 0x9e3779b97f4a7c15;
let mut next = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
for i in 0..200u64 {
let r = match next() % 5 {
0 => LogRecord::User {
time: ts(),
content: format!("u{i}"),
},
1 => LogRecord::Assistant {
time: ts(),
content: Some(format!("a{i}")),
reasoning_content: None,
tool_calls: vec![],
},
2 => LogRecord::ToolResult {
time: ts(),
call: CallId(i),
tool_call_id: format!("t{i}"),
content: "x".repeat((next() % 50) as usize),
class: DataClass::Private,
untrusted: true,
truncated: false,
},
3 => LogRecord::Usage {
time: ts(),
cache_n: i,
prompt_n: 1,
predicted_n: 1,
reasoning_tokens: 0,
thinking_capped: false,
},
_ => LogRecord::CacheLoss {
time: ts(),
expected: i,
got: 0,
},
};
records.push(r);
}
let whole = messages(&b, &records);
for n in 0..=records.len() {
let part = messages(&b, &records[..n]);
assert_eq!(whole[..part.len()], part[..], "prefix of {n} records");
}
}