Found in the M5b device check on straylight: the self-test asked about each of the 33 tags and each neutralised form, 66 requests, one connection each, and inferproxy's token bucket (burst 10) refused the ninth, so loopd would not start. Now it asks about all the tags at once, one per line, and then all their neutralised forms: the answer to the first must hold each tag's recorded token in order, the second none of them. Each tag is a special token, split out before anything else, so the lines between cannot change it; and a neutralised form is only dangerous as a tag's own token, so that is what the second answer must not hold. The errors name a tag by its id, as before. The fake tokenizer reads text with several tags in it; the tests cover each tag as ordinary tokens, as another listed or unlisted token, a duplicated token in the middle and at the end, a form holding each tag, and both requests failing. Spec section 8 and the runbook entry follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
129 lines
3.8 KiB
Rust
129 lines
3.8 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, tokenizer};
|
|
|
|
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.respond("/tokenize", tokenizer);
|
|
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"
|
|
);
|
|
assert_eq!(
|
|
server.requests_to("/tokenize").len(),
|
|
2,
|
|
"the tag check ran: the tags, then their neutralised forms"
|
|
);
|
|
}
|
|
|
|
#[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"));
|
|
}
|