use inferd_daemon::endpoint::bind_tcp;
use inferd_daemon::lifecycle::{AcceptContext, serve_tcp, wait_for_ready};
use inferd_daemon::logx::{LogxLayer, LogxWriter};
use inferd_daemon::router::Router;
use inferd_engine::mock::{Mock, MockConfig};
use inferd_proto::{Message, Request, Response, Role, write_frame};
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tracing_subscriber::layer::SubscriberExt;
async fn boot_daemon(
log_dir: &Path,
mock_config: MockConfig,
) -> (
String,
tokio::sync::oneshot::Sender<()>,
tokio::task::JoinHandle<()>,
tracing::subscriber::DefaultGuard,
) {
let writer = Arc::new(LogxWriter::open(log_dir, "inferd", 16 * 1024 * 1024).unwrap());
let layer = LogxLayer::new(writer);
let subscriber = tracing_subscriber::registry().with(layer);
let guard = tracing::subscriber::set_default(subscriber);
let mock = Arc::new(Mock::with_config(mock_config));
let router = Arc::new(Router::new(vec![mock]));
wait_for_ready(&router, Duration::from_secs(2))
.await
.expect("backend ready");
let listener = bind_tcp("127.0.0.1:0").await.expect("bind tcp");
let addr = listener.local_addr().unwrap().to_string();
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let handle = tokio::spawn(async move {
let _ = serve_tcp(listener, router, AcceptContext::default(), shutdown_rx).await;
});
(addr, shutdown_tx, handle, guard)
}
async fn send_request(addr: &str, req: &Request) -> Vec<Response> {
let mut stream = TcpStream::connect(addr).await.expect("connect");
let mut buf = Vec::new();
write_frame(&mut buf, req).expect("write frame");
stream.write_all(&buf).await.unwrap();
stream.flush().await.unwrap();
let mut reader = BufReader::new(stream);
let mut frames = Vec::new();
loop {
let mut line = Vec::new();
let n = reader.read_until(b'\n', &mut line).await.expect("read");
if n == 0 {
break;
}
let resp: Response = serde_json::from_slice(&line).expect("decode");
let terminal = resp.is_terminal();
frames.push(resp);
if terminal {
break;
}
}
frames
}
fn read_log(dir: &Path) -> Vec<Value> {
let path = dir.join("inferd.ndjson");
let raw = std::fs::read_to_string(&path).unwrap_or_default();
raw.lines()
.filter(|l| !l.is_empty())
.map(|l| serde_json::from_str(l).expect("ndjson parse"))
.collect()
}
#[tokio::test]
async fn request_done_record_shape() {
let dir = tempfile::tempdir().unwrap();
let (addr, shutdown, handle, _guard) = boot_daemon(
dir.path(),
MockConfig {
tokens: vec!["alpha".into(), "beta".into()],
..Default::default()
},
)
.await;
let frames = send_request(
&addr,
&Request {
id: "log-r1".into(),
messages: vec![Message {
role: Role::User,
content: "hi".into(),
}],
..default_request()
},
)
.await;
assert_eq!(frames.len(), 3, "2 tokens + 1 done");
let _ = shutdown.send(());
let _ = tokio::time::timeout(Duration::from_secs(1), handle).await;
let records = read_log(dir.path());
let request_done = records
.iter()
.find(|r| r.get("msg").and_then(|m| m.as_str()) == Some("request_done"))
.expect("expected a request_done record in the log");
assert_eq!(request_done["req_id"], "log-r1");
assert_eq!(request_done["backend"], "mock");
assert_eq!(request_done["completion_tokens"], 2);
assert!(request_done.get("t").and_then(|v| v.as_str()).is_some());
assert_eq!(request_done["level"], "info");
assert!(request_done.get("component").is_some());
}
#[tokio::test]
async fn injected_credential_does_not_leak_into_log() {
let dir = tempfile::tempdir().unwrap();
let (addr, shutdown, handle, _guard) = boot_daemon(dir.path(), MockConfig::default()).await;
let secret = format!("{}-{}", "sk", "1234567890abcdefghij");
let req_id = format!("req-with-secret-{secret}");
let _ = send_request(
&addr,
&Request {
id: req_id.clone(),
messages: vec![Message {
role: Role::User,
content: "hi".into(),
}],
..default_request()
},
)
.await;
let _ = shutdown.send(());
let _ = tokio::time::timeout(Duration::from_secs(1), handle).await;
let raw = std::fs::read_to_string(dir.path().join("inferd.ndjson")).unwrap();
assert!(
!raw.contains(&secret),
"secret leaked into activity log: {raw}"
);
assert!(
raw.contains("[REDACTED"),
"expected redaction marker in: {raw}"
);
}
fn default_request() -> Request {
Request {
id: String::new(),
messages: vec![],
temperature: None,
top_p: None,
top_k: None,
max_tokens: None,
stream: None,
image_token_budget: None,
grammar: String::new(),
}
}