metrale_bench/benchmarks/agentic/
preflight.rs1use std::time::Duration;
12
13use anyhow::{Result, bail};
14use serde_json::json;
15
16use crate::http;
17use crate::plugin::PluginHandle;
18
19pub const SANITY_PROMPT: &str = "What is 2+2? Respond with just the number.";
21
22pub fn answered(text: &str, reasoning: &str) -> bool {
26 [text, reasoning].iter().any(|channel| {
27 channel.match_indices('4').any(|(index, _)| {
28 let before = channel[..index].chars().next_back();
29 let after = channel[index + 1..].chars().next();
30 let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_';
31 !before.is_some_and(identifier) && !after.is_some_and(identifier)
32 })
33 })
34}
35
36pub async fn sanity_check(handle: &PluginHandle, timeout: Duration) -> Result<()> {
39 let target = handle.target();
40 let body = json!({
41 "model": target.model,
42 "messages": [{"role": "user", "content": SANITY_PROMPT}],
43 "chat_template_kwargs": {"enable_thinking": false},
46 "max_tokens": 80,
47 "temperature": 0.0,
48 "stream": true,
49 });
50 let outcome = http::chat_stream(target, &body, timeout).await?;
51 if outcome.text.trim().is_empty() && outcome.reasoning.trim().is_empty() {
52 bail!(
53 "warm-up: {} returned no parseable response",
54 target.base_url
55 );
56 }
57 if !answered(&outcome.text, &outcome.reasoning) {
58 bail!(
59 "warm-up: {} did not answer '4' to 2+2 — catastrophic regression, halting: {:?}",
60 target.base_url,
61 crate::benchmarks::one_line(format!("{} {}", outcome.text, outcome.reasoning))
62 );
63 }
64 Ok(())
65}
66
67#[cfg(test)]
68mod tests {
69 use std::sync::Arc;
70 use std::sync::atomic::AtomicBool;
71
72 use tokio::io::{AsyncReadExt, AsyncWriteExt};
73 use tokio::net::TcpListener;
74
75 use super::*;
76 use crate::artifacts::ArtifactStore;
77 use crate::plugin::TargetEndpoint;
78
79 async fn reasoning_server() -> (u16, tokio::sync::oneshot::Receiver<serde_json::Value>) {
80 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
81 let port = listener.local_addr().unwrap().port();
82 let (sent, request) = tokio::sync::oneshot::channel();
83 tokio::spawn(async move {
84 let (mut socket, _) = listener.accept().await.unwrap();
85 let mut bytes = Vec::new();
86 let mut buf = [0u8; 4096];
87 let (header_end, length) = loop {
88 let n = socket.read(&mut buf).await.unwrap();
89 assert!(n > 0, "client closed before sending its request");
90 bytes.extend_from_slice(&buf[..n]);
91 let Some(header_end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") else {
92 continue;
93 };
94 let headers = String::from_utf8_lossy(&bytes[..header_end]);
95 let length = headers
96 .lines()
97 .find_map(|line| {
98 line.to_ascii_lowercase()
99 .strip_prefix("content-length: ")
100 .and_then(|value| value.parse::<usize>().ok())
101 })
102 .expect("request must declare content length");
103 if bytes.len() >= header_end + 4 + length {
104 break (header_end, length);
105 }
106 };
107 let body = serde_json::from_slice(&bytes[header_end + 4..header_end + 4 + length])
108 .expect("request body must be JSON");
109 sent.send(body).unwrap();
110
111 let body = "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"2+2 is 4\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n";
112 socket
113 .write_all(
114 format!(
115 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
116 body.len()
117 )
118 .as_bytes(),
119 )
120 .await
121 .unwrap();
122 });
123 (port, request)
124 }
125
126 #[test]
127 fn the_prompt_is_the_harness_prompt() {
128 let harness = include_str!("../../../../../bench/fp8_dgx2_drift/harness/run_tier.sh");
129 let request = harness
130 .lines()
131 .find_map(|line| {
132 line.trim_start()
133 .strip_prefix("-d '")
134 .and_then(|body| body.strip_suffix("' 2>&1)"))
135 })
136 .expect("run_tier.sh must carry the warm-up request as JSON");
137 let body: serde_json::Value =
138 serde_json::from_str(request).expect("the harness warm-up request must be valid JSON");
139 assert_eq!(body["messages"][0]["content"], SANITY_PROMPT);
140 }
141
142 #[test]
143 fn either_channel_may_carry_the_answer() {
144 assert!(answered("4", ""));
145 assert!(answered("", "2+2 is 4"));
146 assert!(answered("The answer is 4.", ""));
147 assert!(!answered("", ""));
148 assert!(!answered("five", "let me think"));
149 assert!(!answered("42", ""), "42 is not the answer to 2+2");
150 assert!(
151 !answered("status 404", ""),
152 "an error code is not an answer"
153 );
154 assert!(
155 !answered("4ever", ""),
156 "a digit inside a word is not an answer"
157 );
158 }
159
160 #[test]
161 fn the_harness_uses_the_same_answer_boundary() {
162 let harness = include_str!("../../../../../bench/fp8_dgx2_drift/harness/run_tier.sh");
163 assert!(
164 harness.contains("grep -Eq '(^|[^[:alnum:]_])4([^[:alnum:]_]|$)'"),
165 "the shell and Rust preflights must reject 42 and 404 alike"
166 );
167 }
168
169 #[tokio::test]
170 async fn sanity_check_sends_the_declared_request_and_reads_reasoning() {
171 let (port, request) = reasoning_server().await;
172 let (events, receiver) = std::sync::mpsc::channel();
173 let handle = PluginHandle::new(
174 1,
175 TargetEndpoint::local(port, "test-model"),
176 ArtifactStore::with_root(std::env::temp_dir().join("metrale-preflight-test")),
177 events,
178 Arc::new(AtomicBool::new(false)),
179 );
180
181 sanity_check(&handle, Duration::from_secs(2))
182 .await
183 .expect("a standalone 4 in reasoning must pass");
184 drop(receiver);
185 let body = request.await.expect("server must observe the request");
186 assert_eq!(body["model"], "test-model");
187 assert_eq!(
188 body["messages"],
189 json!([{"role": "user", "content": SANITY_PROMPT}])
190 );
191 assert_eq!(
192 body["chat_template_kwargs"],
193 json!({"enable_thinking": false})
194 );
195 assert_eq!(body["max_tokens"], 80);
196 assert_eq!(body["temperature"], 0.0);
197 assert_eq!(body["stream"], true);
198 }
199}