#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use serde_json::{Value, json};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
pub const COUNT_FIXTURE: &str = env!("CARGO_BIN_EXE_salvor-mcp-count-fixture");
pub fn salvor(store: &Path) -> assert_cmd::Command {
let mut command = assert_cmd::Command::cargo_bin("salvor").expect("salvor binary builds");
command.arg("--store").arg(store).env("RUST_LOG", "warn");
command
}
pub async fn run_salvor(store: &Path, args: &[&str]) -> std::process::Output {
let store = store.to_owned();
let args: Vec<String> = args.iter().map(|a| (*a).to_owned()).collect();
tokio::task::spawn_blocking(move || {
let mut command = salvor(&store);
command.args(&args);
command.output().expect("salvor runs")
})
.await
.expect("blocking task joins")
}
pub fn text_response(text: &str, input_tokens: u64, output_tokens: u64) -> Value {
json!({
"id": "msg_text",
"model": "test-model",
"role": "assistant",
"content": [{"type": "text", "text": text}],
"stop_reason": "end_turn",
"usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}
})
}
pub fn tool_use_response(
tool_use_id: &str,
tool: &str,
input: Value,
input_tokens: u64,
output_tokens: u64,
) -> Value {
json!({
"id": format!("msg_tool_{tool_use_id}"),
"model": "test-model",
"role": "assistant",
"content": [{"type": "tool_use", "id": tool_use_id, "name": tool, "input": input}],
"stop_reason": "tool_use",
"usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}
})
}
pub struct GateModel {
script: Vec<(usize, Value)>,
gated_count: Option<usize>,
released: Arc<AtomicBool>,
delay: Duration,
}
impl GateModel {
pub async fn mount(script: Vec<(usize, Value)>) -> MockServer {
Self::mount_gated(
script,
None,
Arc::new(AtomicBool::new(true)),
Duration::ZERO,
)
.await
}
pub async fn mount_gated(
script: Vec<(usize, Value)>,
gated_count: Option<usize>,
released: Arc<AtomicBool>,
delay: Duration,
) -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/messages"))
.respond_with(Self {
script,
gated_count,
released,
delay,
})
.mount(&server)
.await;
server
}
}
impl Respond for GateModel {
fn respond(&self, request: &Request) -> ResponseTemplate {
let body: Value = match serde_json::from_slice(&request.body) {
Ok(body) => body,
Err(_) => return ResponseTemplate::new(400),
};
let count = body
.get("messages")
.and_then(Value::as_array)
.map_or(0, Vec::len);
for (expected, response) in &self.script {
if *expected == count {
let mut template = ResponseTemplate::new(200).set_body_json(response.clone());
if self.gated_count == Some(count) && !self.released.load(Ordering::SeqCst) {
template = template.set_delay(self.delay);
}
return template;
}
}
ResponseTemplate::new(500).set_body_json(json!({
"error": {"type": "test_script", "message": format!("no scripted response for {count} messages")}
}))
}
}
pub fn write_agent(dir: &Path, model_uri: &str, count_file: &Path, extra: &str) -> PathBuf {
let toml = format!(
"model = \"test-model\"\n\
system_prompt = \"You are a test agent.\"\n\
\n\
[llm]\n\
base_url = \"{model_uri}\"\n\
max_retries = 0\n\
\n\
[[mcp_servers]]\n\
command = \"{fixture}\"\n\
args = [\"{count}\"]\n\
effect_overrides = {{ record = \"write\" }}\n\
{extra}\n",
fixture = COUNT_FIXTURE,
count = count_file.display(),
);
let path = dir.join("agent.toml");
std::fs::write(&path, toml).expect("write agent toml");
path
}
pub fn count_lines(count_file: &Path) -> usize {
std::fs::read_to_string(count_file)
.map(|text| text.lines().count())
.unwrap_or(0)
}