use super::assistant_test_support::*;
use serde_json::json;
#[path = "assistant_boundary_revision_tests.rs"]
mod revision;
use std::{
io::{BufRead, BufReader},
os::unix::{fs::PermissionsExt, net::UnixStream},
time::Duration,
};
#[test]
fn valid_hello_is_welcomed_and_socket_is_private() -> Result {
let f = Fixture::new(json!({}))?;
let seat = f.seat("agent-pass", "wards")?;
assert_eq!(
std::fs::metadata(f.files.path().join("assistant.sock"))?
.permissions()
.mode()
& 0o777,
0o600
);
drop(seat);
Ok(())
}
#[test]
fn wrong_audience_is_refused_by_name() -> Result {
let f = Fixture::new(json!({}))?;
for (pass, code) in [
("wrong-audience", "wrong_audience"),
("wrong-agent", "assistant_agent_mismatch"),
("unowned-agent", "contract_refused"),
] {
assert!(
f.seat(pass, "wards")
.err()
.ok_or_else(|| anyhow::anyhow!("welcomed wrong audience"))?
.to_string()
.contains(code)
);
}
Ok(())
}
#[test]
fn second_live_seat_is_refused_seat_taken() -> Result {
let f = Fixture::new(json!({}))?;
let first = f.seat("agent-pass", "wards")?;
assert!(
f.seat("agent-pass", "wards")
.err()
.ok_or_else(|| anyhow::anyhow!("duplicate admitted"))?
.to_string()
.contains("seat_taken")
);
drop(first);
Ok(())
}
#[test]
fn session_person_and_context_reach_seat_and_ack_returns_202() -> Result {
let f = Fixture::new(json!({}))?;
let read = ack(f.seat("agent-pass", "wards")?);
check(f.answer("POST","",&json!({"text":"ward census","person":"intruder","context":{"store":"wards","branch":"main","screen":"records","selection":["patient"]}})),202,"id")?;
let op = read.recv_timeout(Duration::from_secs(5))?;
assert_eq!(op["op"]["person"], PERSON);
assert_eq!(op["op"]["text"], "ward census");
assert_eq!(op["op"]["context"]["selection"], json!(["patient"]));
Ok(())
}
#[test]
fn missing_seat_answers_start_route_and_lys_link() -> Result {
let f = Fixture::new(json!({}))?;
let text = written(f.answer("POST", "", &json!({"text":"hello"})))?;
assert!(
text.starts_with("HTTP/1.1 409 ")
&& text.contains("assistant_not_started")
&& text.contains("/api/assistant/wards/start")
&& text.contains("https://lys.example"),
"{text}"
);
Ok(())
}
#[test]
fn zero_two_or_malformed_operations_are_refused() -> Result {
let f = Fixture::new(json!({}))?;
for body in [
json!({}),
json!({"text":"hello","cancel":true}),
json!({"cancel":false}),
json!({"text":7}),
json!({"session":{"list":true,"new":true}}),
json!({"config":{"option":"model"}}),
json!({"command":{"name":"","input":""}}),
] {
check(f.answer("POST", "", &body), 400, "assistant_op_invalid")?;
}
Ok(())
}
#[test]
fn cross_origin_post_is_refused_before_session() -> Result {
let f = Fixture::new(json!({}))?;
let mut r = f.request("POST", "", &json!({"text":"hello"}));
r.headers = vec![("origin".into(), "https://evil.example".into())];
check(
super::route::answer(&f.door, &f.site, &r, crate::serve::pass::now()),
403,
"origin_refused",
)
}
#[test]
fn seat_refusal_reaches_request_by_name() -> Result {
let f = Fixture::new(json!({}))?;
let mut seat = f.seat("agent-pass", "wards")?;
let worker = std::thread::spawn(move || -> Result {
let op = seat.read()?;
seat.send(&json!({"refused":{"id":op["op"]["id"],"code":"prompt_queue_full","message":"queue full"}}))
});
check(
f.answer("POST", "", &json!({"text":"hello"})),
409,
"prompt_queue_full",
)?;
worker.join().map_err(|_| anyhow::anyhow!("seat panic"))??;
Ok(())
}
#[test]
fn frame_bound_refuses_count_plus_one() -> Result {
let f = Fixture::new(json!({"frame_bytes":256}))?;
let mut stream = UnixStream::connect(f.files.path().join("assistant.sock"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
send(&mut stream, &json!({"hello":"x".repeat(244)}))?;
let mut line = String::new();
BufReader::new(stream).read_line(&mut line)?;
assert!(line.contains("frame_bytes=256; count=257"), "{line}");
Ok(())
}
#[test]
fn person_and_store_seat_bounds_name_count() -> Result {
for (bounds, pass, store, code) in [
(
json!({"max_per_person":1}),
"agent-pass",
"other",
"max_per_person",
),
(
json!({"max_per_store":1}),
"other-person",
"wards",
"max_per_store",
),
(
json!({"door_max_connections":1}),
"other-person",
"wards",
"max_connections",
),
] {
let f = Fixture::new(bounds)?;
let first = f.seat("agent-pass", "wards")?;
let error = if code == "max_connections" {
let stream = UnixStream::connect(f.files.path().join("assistant.sock"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut refused = String::new();
BufReader::new(stream).read_line(&mut refused)?;
refused
} else {
f.seat(pass, store)
.err()
.ok_or_else(|| anyhow::anyhow!("bound admitted"))?
.to_string()
};
assert!(error.contains(&format!("{code}=1; count=1")), "{error}");
drop(first);
}
Ok(())
}
#[test]
fn active_socket_and_regular_file_are_never_replaced() -> Result {
let mut f = Fixture::new(json!({}))?;
let before = std::fs::metadata(f.files.path().join("assistant.sock"))?;
let second = std::os::unix::net::UnixListener::bind(f.files.path().join("assistant.sock"));
assert!(second.is_err());
assert_eq!(
before.file_type(),
std::fs::metadata(f.files.path().join("assistant.sock"))?.file_type()
);
let active = super::assistant_socket::Listener::start(&f.door, &f.config)
.err()
.ok_or_else(|| anyhow::anyhow!("active listener replaced"))?;
assert_eq!(active.code, "assistant_socket_listening");
let regular = f.files.path().join("regular");
std::fs::write(®ular, b"fixture bytes")?;
f.config
.assistant
.as_mut()
.ok_or_else(|| anyhow::anyhow!("assistant config missing"))?
.socket = regular.clone();
let refused = super::assistant_socket::Listener::start(&f.door, &f.config)
.err()
.ok_or_else(|| anyhow::anyhow!("regular file replaced"))?;
assert_eq!(refused.code, "assistant_socket_exists");
assert_eq!(std::fs::read(®ular)?, b"fixture bytes");
let failed = f.files.path().join("failed.sock");
f.config
.assistant
.as_mut()
.ok_or_else(|| anyhow::anyhow!("assistant config missing"))?
.socket = failed.clone();
let refused = super::assistant_socket::Listener::start(&f.door, &f.config)
.err()
.ok_or_else(|| anyhow::anyhow!("second socket attached"))?;
assert_eq!(refused.code, "assistant_already_listening");
assert!(!failed.exists(), "failed startup left its socket file");
let stale = f.files.path().join("stale.sock");
drop(std::os::unix::net::UnixListener::bind(&stale)?);
f.config
.assistant
.as_mut()
.ok_or_else(|| anyhow::anyhow!("assistant config missing"))?
.socket = stale.clone();
let fresh = std::sync::Arc::new(super::signin::Door::new(Lys {
decisions: std::sync::atomic::AtomicUsize::new(0),
..Lys::default()
}));
let restarted = super::assistant_socket::Listener::start(&fresh, &f.config)?;
assert_eq!(
std::fs::metadata(&stale)?.permissions().mode() & 0o777,
0o600
);
drop(restarted);
Ok(())
}
#[test]
fn outstanding_op_bound_names_bound_and_count() -> Result {
let f = Fixture::new(json!({"max_per_person":1}))?;
let mut seat = f.seat("agent-pass", "wards")?;
let request = f.request("POST", "", &json!({"text":"first"}));
let (door, site) = (
std::sync::Arc::clone(&f.door),
std::sync::Arc::clone(&f.site),
);
let post = std::thread::spawn(move || {
written(super::route::answer(
&door,
&site,
&request,
crate::serve::pass::now(),
))
});
let op = seat.read()?;
let refused = written(f.answer("POST", "", &json!({"text":"second"})))?;
assert!(refused.contains("max_per_person=1; count=1"), "{refused}");
seat.send(&json!({"ack":{"id":op["op"]["id"]}}))?;
assert!(
post.join()
.map_err(|_| anyhow::anyhow!("post panic"))??
.starts_with("HTTP/1.1 202 ")
);
Ok(())
}