235 lines
6.9 KiB
Rust
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");
|
|
}
|
|
}
|