use super::{Expected, Issuer};
use crate::assistant::{config::Seat, permission::Decision, tools::Identity};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde_json::{Value, json};
use tokio::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _, BufReader};
fn token(expires: u64) -> anyhow::Result<String> {
Ok(format!(
"e30.{}.c2ln",
URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json!({"exp":expires}))?)
))
}
async fn fixture(
responses: Vec<(&'static str, u16, Value)>,
expires: u64,
) -> anyhow::Result<(Issuer, tokio::task::JoinHandle<anyhow::Result<()>>)> {
use std::os::unix::fs::PermissionsExt as _;
let directory = tempfile::Builder::new()
.permissions(std::fs::Permissions::from_mode(0o700))
.tempdir()?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let origin = format!("http://{}", listener.local_addr()?);
let worker = tokio::spawn(async move {
for (expected, status, body) in responses {
let mut peer = BufReader::new(listener.accept().await?.0);
let mut line = String::new();
peer.read_line(&mut line).await?;
anyhow::ensure!(
line.starts_with(expected),
"watch request order differs: expected {expected}, observed {}",
line.trim()
);
let mut length = 0;
loop {
line.clear();
peer.read_line(&mut line).await?;
if line == "\r\n" {
break;
}
if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") {
length = value.trim().parse()?;
}
}
let mut received = vec![0; length];
peer.read_exact(&mut received).await?;
let bytes = serde_json::to_vec(&body)?;
let header = format!(
"HTTP/1.1 {status} Fixture\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
bytes.len()
);
peer.get_mut().write_all(header.as_bytes()).await?;
peer.get_mut().write_all(&bytes).await?;
peer.get_mut().shutdown().await?;
}
Ok(())
});
let seat: Seat = serde_json::from_value(
json!({"harness":{"id":"codex","home":directory.path(),"program":"/bin/true","args":[]},"door_socket":"/tmp/unused","serve_config":"/tmp/unused","issuer":origin,"store":"fixture"}),
)?;
let identity = Identity {
agent: "agent".into(),
launch: serde_json::to_string(&json!({"headers":{"lys-agent-pass":"fixture-proof"}}))?,
pass: token(expires)?,
};
Ok((Issuer::new(&seat, &identity)?, worker))
}
fn now() -> anyhow::Result<u64> {
Ok(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs())
}
fn draft(state: &str) -> Value {
json!({"id":"draft","state":state,"holder":"agent","target":{"kind":"haematite.database","id":"fixture","action":"agent_permission"},"request_digest":"digest","decided_by":"person","decided_at":1})
}
#[tokio::test]
async fn round4_2_all_real_lys_draft_states_are_understood() -> anyhow::Result<()> {
for state in [
"waiting",
"approved",
"refused",
"executed",
"refused_on_execution",
] {
let (issuer, worker) = fixture(
vec![("GET /api/product-drafts/draft ", 200, draft(state))],
now()? + 3600,
)
.await?;
let answer = issuer
.draft_answer(
"draft",
&Expected {
digest: "digest".into(),
},
)
.await;
worker.await??;
let answer = answer?;
if state == "waiting" {
assert!(answer.is_none());
continue;
}
let answer = answer.ok_or_else(|| anyhow::anyhow!("terminal state unanswered"))?;
assert_eq!(answer.person, "person");
assert!(match state {
"approved" => matches!(answer.decision, Decision::Approve),
"executed" => matches!(answer.decision, Decision::Cancelled),
_ => matches!(answer.decision, Decision::Refuse),
});
}
Ok(())
}
#[tokio::test]
async fn round4_3_near_expiry_watch_pass_is_renewed_before_reading() -> anyhow::Result<()> {
let expires = now()? + 3600;
let (issuer, worker) = fixture(
vec![
(
"POST /api/agents/agent/pass ",
200,
json!({"pass":token(expires)?,"audience":"haematite","expires_at":expires}),
),
("GET /api/product-drafts/draft ", 200, draft("waiting")),
],
now()? + 1,
)
.await?;
let answer = issuer
.draft_answer(
"draft",
&Expected {
digest: "digest".into(),
},
)
.await;
let served = worker.await?;
served?;
assert!(answer?.is_none());
Ok(())
}
#[tokio::test]
async fn round4_3_watch_refusal_reports_its_http_status() -> anyhow::Result<()> {
let (issuer, worker) = fixture(
vec![(
"GET /api/product-drafts/draft ",
403,
json!({"refusal":"fixture"}),
)],
now()? + 3600,
)
.await?;
let result = issuer
.draft_answer(
"draft",
&Expected {
digest: "digest".into(),
},
)
.await;
worker.await??;
let error = result
.err()
.ok_or_else(|| anyhow::anyhow!("HTTP refusal accepted"))?;
assert!(error.to_string().contains("HTTP 403"), "{error}");
Ok(())
}