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>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
//! Tests for the session store. Do not edit.
|
||||
|
||||
mod support;
|
||||
|
||||
use loopd::baseline::Baseline;
|
||||
use loopd::session::{Session, SessionError};
|
||||
use proto::{CallId, DataClass, Epoch, LogRecord, SessionId, Timestamp};
|
||||
use support::Home;
|
||||
|
||||
fn baseline() -> Baseline {
|
||||
Baseline {
|
||||
system: "sys".to_string(),
|
||||
tools: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn id(s: &str) -> SessionId {
|
||||
SessionId::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn ts() -> Timestamp {
|
||||
Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_writes_the_baseline_file_and_the_start_record() {
|
||||
let home = Home::new();
|
||||
let s = Session::create(&home.dir, id("a"), baseline(), 3).unwrap();
|
||||
assert_eq!(s.dir(), home.dir.join("sessions").join("a"));
|
||||
assert_eq!(
|
||||
Baseline::from_json(&home.read("sessions/a/0.baseline.json")).unwrap(),
|
||||
baseline()
|
||||
);
|
||||
let records = home.records("a");
|
||||
assert_eq!(records.len(), 1);
|
||||
match &records[0] {
|
||||
LogRecord::SessionStart {
|
||||
session,
|
||||
epoch,
|
||||
slot,
|
||||
baseline: hash,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(session, &id("a"));
|
||||
assert_eq!(*epoch, Epoch(0));
|
||||
assert_eq!(*slot, 3);
|
||||
assert_eq!(*hash, baseline().hash().unwrap());
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
assert_eq!(s.records(), &records[..]);
|
||||
assert!(matches!(
|
||||
Session::create(&home.dir, id("a"), baseline(), 0),
|
||||
Err(SessionError::Exists(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_is_visible_on_disk_at_once_and_after_reopen() {
|
||||
let home = Home::new();
|
||||
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "hi\nthere \"quoted\" caf\u{e9}".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
home.records("a").len(),
|
||||
2,
|
||||
"written and synced before append returns"
|
||||
);
|
||||
drop(s);
|
||||
let s = Session::open(&home.dir, id("a")).unwrap();
|
||||
assert_eq!(s.records().len(), 2);
|
||||
assert_eq!(s.baseline(), &baseline());
|
||||
assert!(
|
||||
matches!(s.records()[1], LogRecord::User { ref content, .. } if content.contains("caf\u{e9}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_uses_the_baseline_file_not_system_md() {
|
||||
let home = Home::new();
|
||||
let s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
|
||||
drop(s);
|
||||
home.write("system.md", "A different prompt.\n");
|
||||
let s = Session::open(&home.dir, id("a")).unwrap();
|
||||
assert_eq!(s.baseline().system, "sys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_a_missing_session_is_not_found() {
|
||||
let home = Home::new();
|
||||
assert!(matches!(
|
||||
Session::open(&home.dir, id("nope")),
|
||||
Err(SessionError::NotFound(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_torn_log_is_refused_with_the_line_number() {
|
||||
let home = Home::new();
|
||||
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "hi".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
drop(s);
|
||||
let path = "sessions/a/0.jsonl";
|
||||
let good = home.read(path);
|
||||
// A last line cut in the middle.
|
||||
home.write(path, &good[..good.len() - 5]);
|
||||
match Session::open(&home.dir, id("a")).map(|_| ()) {
|
||||
Err(SessionError::Torn { line, .. }) => assert_eq!(line, 2),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// A complete line that is not a record.
|
||||
home.write(path, &format!("{good}{{\"type\":\"zz\"}}\n"));
|
||||
match Session::open(&home.dir, id("a")).map(|_| ()) {
|
||||
Err(SessionError::Torn { line, path, .. }) => {
|
||||
assert_eq!(line, 3);
|
||||
assert!(path.ends_with("0.jsonl"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let e: Box<dyn std::error::Error> =
|
||||
Box::new(Session::open(&home.dir, id("a")).map(|_| ()).unwrap_err());
|
||||
assert!(e.to_string().contains("0.jsonl:3"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_ids_continue_across_a_reopen() {
|
||||
let home = Home::new();
|
||||
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
|
||||
assert_eq!(s.next_call(), CallId(1));
|
||||
assert_eq!(s.next_call(), CallId(2));
|
||||
s.append(LogRecord::ToolResult {
|
||||
time: ts(),
|
||||
call: CallId(2),
|
||||
tool_call_id: "x".to_string(),
|
||||
content: String::new(),
|
||||
class: DataClass::Public,
|
||||
untrusted: false,
|
||||
truncated: false,
|
||||
})
|
||||
.unwrap();
|
||||
drop(s);
|
||||
let mut s = Session::open(&home.dir, id("a")).unwrap();
|
||||
assert_eq!(
|
||||
s.next_call(),
|
||||
CallId(3),
|
||||
"one more than the highest call in the log"
|
||||
);
|
||||
let mut fresh = Session::create(&home.dir, id("b"), baseline(), 0).unwrap();
|
||||
assert_eq!(fresh.next_call(), CallId(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_usage_is_the_latest_usage_record() {
|
||||
let home = Home::new();
|
||||
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
|
||||
assert_eq!(s.last_usage(), None);
|
||||
s.append(LogRecord::Usage {
|
||||
time: ts(),
|
||||
cache_n: 1,
|
||||
prompt_n: 2,
|
||||
predicted_n: 3,
|
||||
reasoning_tokens: 0,
|
||||
thinking_capped: false,
|
||||
})
|
||||
.unwrap();
|
||||
s.append(LogRecord::User {
|
||||
time: ts(),
|
||||
content: "x".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
s.append(LogRecord::Usage {
|
||||
time: ts(),
|
||||
cache_n: 6,
|
||||
prompt_n: 7,
|
||||
predicted_n: 8,
|
||||
reasoning_tokens: 4,
|
||||
thinking_capped: true,
|
||||
})
|
||||
.unwrap();
|
||||
let u = s.last_usage().unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
u.cache_n,
|
||||
u.prompt_n,
|
||||
u.predicted_n,
|
||||
u.reasoning_tokens,
|
||||
u.thinking_capped
|
||||
),
|
||||
(6, 7, 8, 4, true)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user