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>
123 lines
3.6 KiB
Rust
123 lines
3.6 KiB
Rust
//! Tests for the `loopd serve` command. Do not edit.
|
|
|
|
mod support;
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::process::{Command, Stdio};
|
|
use std::time::Duration;
|
|
use support::{FakeServer, Home, Reply};
|
|
|
|
fn config_file(home: &Home, server: &FakeServer, expect_slots: u32) -> std::path::PathBuf {
|
|
let text = format!(
|
|
r#"
|
|
[infer]
|
|
socket = "{}"
|
|
model = "test-model"
|
|
[slots]
|
|
main = 0
|
|
background = 1
|
|
[expect]
|
|
template_sha256 = "f55f52930aa8bf44ab5cb85f99370fcc3c56e9a85640b812086d5330bce5d86b"
|
|
n_ctx = 131072
|
|
slots = {expect_slots}
|
|
[limits]
|
|
poll_ms = 40
|
|
liveness_ms = 500
|
|
retry_backoff_ms = [10]
|
|
[paths]
|
|
home = "{}"
|
|
"#,
|
|
server.socket.display(),
|
|
home.dir.display()
|
|
);
|
|
let path = home.dir.join("config.toml");
|
|
std::fs::write(&path, text).unwrap();
|
|
path
|
|
}
|
|
|
|
fn healthy_routes(server: &FakeServer) {
|
|
server.route("/props", vec![Reply::fixture("props")]);
|
|
server.route(
|
|
"/v1/chat/completions",
|
|
vec![
|
|
Reply::fixture("tool_call"),
|
|
Reply::fixture("turn1"),
|
|
Reply::fixture("turn2"),
|
|
Reply::fixture("plain"),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serve_runs_the_self_test_then_binds_the_socket_with_mode_0600() {
|
|
let home = Home::new();
|
|
let server = FakeServer::start();
|
|
healthy_routes(&server);
|
|
let config = config_file(&home, &server, 2);
|
|
let socket = home.dir.join("run").join("loop").join("loop.sock");
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_loopd"))
|
|
.args(["serve", "--config"])
|
|
.arg(&config)
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let mut up = false;
|
|
for _ in 0..200 {
|
|
if socket.exists() {
|
|
up = true;
|
|
break;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
let mode = std::fs::metadata(&socket).map(|m| m.permissions().mode() & 0o777);
|
|
let _ = child.kill();
|
|
let output = child.wait_with_output().unwrap();
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(up, "the socket never appeared; stderr: {stderr}");
|
|
assert_eq!(mode.unwrap(), 0o600, "{stderr}");
|
|
assert!(stderr.contains("selftest: ok"), "{stderr}");
|
|
assert!(stderr.contains("serving on"), "{stderr}");
|
|
assert_eq!(
|
|
server.requests_to("/v1/chat/completions").len(),
|
|
3,
|
|
"the three self-test completions ran"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serve_refuses_to_start_when_the_self_test_fails() {
|
|
let home = Home::new();
|
|
let server = FakeServer::start();
|
|
healthy_routes(&server);
|
|
let config = config_file(&home, &server, 3); // the fixture reports 2 slots
|
|
let output = Command::new(env!("CARGO_BIN_EXE_loopd"))
|
|
.args(["serve", "--config"])
|
|
.arg(&config)
|
|
.output()
|
|
.unwrap();
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert_eq!(output.status.code(), Some(1), "{stderr}");
|
|
assert!(stderr.contains("selftest: FAILED"), "{stderr}");
|
|
assert!(stderr.contains("slot count"), "{stderr}");
|
|
assert!(
|
|
!home.dir.join("run").join("loop").join("loop.sock").exists(),
|
|
"no socket was left behind"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn usage_and_a_bad_config_are_reported() {
|
|
let output = Command::new(env!("CARGO_BIN_EXE_loopd"))
|
|
.arg("dance")
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(2));
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("usage"));
|
|
let output = Command::new(env!("CARGO_BIN_EXE_loopd"))
|
|
.args(["serve", "--config", "/nonexistent/config.toml"])
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("config.toml"));
|
|
}
|