use std::time::Duration;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use crate::config::LlmConfig;
use crate::llm::cache::Cache;
use crate::llm::chain::ProviderChain;
use crate::llm::client::LlmClient;
pub(crate) fn cfg_for(server: &MockServer, model: &str, max_retries: u32) -> LlmConfig {
LlmConfig {
enabled: true,
endpoint: Some(format!("{}/v1", server.uri())),
model: Some(model.to_owned()),
api_key: Some("not-needed".to_owned()),
max_retries,
..LlmConfig::default()
}
}
pub(crate) fn sse(parts: &[&str]) -> String {
sse_chunks(parts, "\"stop\"")
}
pub(crate) fn sse_finishing_with(parts: &[&str], finish: &str) -> String {
sse_chunks(parts, &format!("\"{finish}\""))
}
pub(crate) fn sse_without_finish_reason(parts: &[&str]) -> String {
sse_chunks(parts, "null")
}
fn sse_chunks(parts: &[&str], last: &str) -> String {
assert!(
!parts.is_empty(),
"an SSE fixture needs a final chunk to carry its finish reason"
);
let mut out = String::new();
for (i, part) in parts.iter().enumerate() {
let finish = if i + 1 == parts.len() { last } else { "null" };
out.push_str(&format!(
"data: {{\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"m\",\"choices\":[{{\"index\":0,\"delta\":{{\"content\":{}}},\"finish_reason\":{finish}}}]}}\n\n",
serde_json::to_string(part).expect("string serializes")
));
}
out.push_str("data: [DONE]\n\n");
out
}
pub(crate) async fn server_without_finish_reason(parts: &[&str]) -> MockServer {
sse_server(sse_without_finish_reason(parts)).await
}
pub(crate) async fn server_finishing_with(parts: &[&str], finish: &str) -> MockServer {
sse_server(sse_finishing_with(parts, finish)).await
}
pub(crate) async fn request_count(server: &MockServer) -> usize {
server
.received_requests()
.await
.expect("the mock server must be recording requests")
.len()
}
pub(crate) fn fast_retry_client(cfg: &LlmConfig) -> LlmClient {
let mut client = LlmClient::new(cfg).expect("client builds");
client.retry_config.initial_delay = Duration::from_millis(10);
client.retry_config.max_delay = Duration::from_millis(50);
client.retry_config.jitter_factor = 0.0;
client
}
pub(crate) async fn server_returning(parts: &[&str]) -> MockServer {
sse_server(sse(parts)).await
}
async fn sse_server(body: String) -> MockServer {
let server = MockServer::start().await;
mount_sse(
&server,
ResponseTemplate::new(200).set_body_raw(body, "text/event-stream"),
)
.await;
server
}
pub(crate) async fn server_failing_with(status: u16) -> MockServer {
let server = MockServer::start().await;
mount_sse(&server, ResponseTemplate::new(status)).await;
server
}
pub(crate) async fn mount_sse(server: &MockServer, template: ResponseTemplate) {
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(template)
.mount(server)
.await;
}
pub(crate) async fn json_server(route: &str, status: u16, body: &str) -> MockServer {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(route))
.respond_with(
ResponseTemplate::new(status)
.set_body_string(body)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
server
}
pub(crate) fn fast_retry_chain(cfgs: &[LlmConfig]) -> ProviderChain {
let refs: Vec<&LlmConfig> = cfgs.iter().collect();
let mut chain = ProviderChain::new(&refs).expect("chain builds from valid configs");
for provider in &mut chain.providers {
if let Some(client) = provider.backend.http_mut() {
client.retry_config.initial_delay = Duration::from_millis(10);
client.retry_config.max_delay = Duration::from_millis(50);
client.retry_config.jitter_factor = 0.0;
}
}
chain
}
pub(crate) fn temp_cache() -> (Cache, tempfile::TempDir) {
let dir = tempfile::TempDir::new().expect("temp dir");
let cache = Cache::new(dir.path().to_path_buf(), 30, 1024 * 1024);
(cache, dir)
}
pub(crate) fn write_executable(path: &std::path::Path, contents: impl AsRef<str>) {
#[cfg(unix)]
{
let status = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(r#"printf '%s' "$2" > "$1" && chmod +x "$1""#)
.arg("sh") .arg(path)
.arg(contents.as_ref())
.status()
.expect("the writer process must start");
assert!(
status.success(),
"writing the executable {} failed: {status}",
path.display()
);
}
#[cfg(not(unix))]
std::fs::write(path, contents.as_ref()).expect("writing the executable must succeed");
}
#[cfg(unix)]
pub(crate) fn probe_and_stop_process(pid: &str) -> bool {
std::process::Command::new("/bin/kill")
.arg(pid)
.status()
.expect("stop grandchild")
.success()
}
pub(crate) fn two_level_tree_size(root: &std::path::Path) -> u64 {
std::fs::read_dir(root)
.into_iter()
.flatten()
.flatten()
.filter_map(|directory| std::fs::read_dir(directory.path()).ok())
.flatten()
.flatten()
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.map(|metadata| metadata.len())
.sum()
}
pub(crate) fn make_executable(path: &std::path::Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)
.expect("the file must exist before its mode is changed")
.permissions();
perms.set_mode(perms.mode() | 0o111);
std::fs::set_permissions(path, perms).expect("setting the executable bit must succeed");
}
#[cfg(not(unix))]
let _ = path;
}
pub(crate) fn write_drep_toml(dir: &std::path::Path, endpoint: &str) {
let body = format!(
r#"[[llm]]
enabled = true
endpoint = "{endpoint}"
model = "m"
api_key = "not-needed"
max_retries = 1
"#
);
std::fs::write(dir.join("drep.toml"), body).expect("drep.toml");
}
pub(crate) fn write_site_policy_body(dir: &std::path::Path, body: &str) -> std::path::PathBuf {
let path = dir.join("site.toml");
std::fs::write(&path, body).expect("write site policy");
path
}
pub(crate) fn write_site_policy(dir: &std::path::Path, markers: &[&str]) -> std::path::PathBuf {
let quoted: Vec<String> = markers.iter().map(|marker| format!("{marker:?}")).collect();
write_site_policy_body(dir, &format!("refuse_markers = [{}]\n", quoted.join(", ")))
}
pub(crate) fn git(dir: &std::path::Path) -> std::process::Command {
let mut command = std::process::Command::new("git");
command
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_QUARANTINE_PATH")
.current_dir(dir);
command
}
pub(crate) fn git_init(dir: &std::path::Path) {
git_must(dir, &["init", "--initial-branch=main"]);
for (key, value) in [
("user.email", "test@example.com"),
("user.name", "test"),
("core.hooksPath", ""),
("commit.gpgsign", "false"),
] {
git_must(dir, &["config", "--local", key, value]);
}
}
pub(crate) fn git_unresolvable(dir: &std::path::Path) {
std::fs::write(dir.join(".git"), "not a gitfile\n").expect("unresolvable .git");
}
pub(crate) fn git_add(dir: &std::path::Path, path: &str) {
git_must(dir, &["add", "--", path]);
}
pub(crate) fn git_commit_all(dir: &std::path::Path, message: &str) {
git_must(dir, &["add", "--all"]);
git_must(dir, &["commit", "--quiet", "--no-verify", "-m", message]);
}
pub(crate) fn git_output(dir: &std::path::Path, args: &[&str]) -> String {
let output = git(dir)
.args(args)
.output()
.unwrap_or_else(|err| panic!("git {} must run: {err}", args.join(" ")));
assert!(
output.status.success(),
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.expect("git output is utf8")
.trim()
.to_owned()
}
fn git_must(dir: &std::path::Path, args: &[&str]) {
git_output(dir, args);
}
pub(crate) fn assert_executable(path: &std::path::Path) {
assert!(
crate::languages::runner::is_executable(path),
"{} must be executable - git ignores a non-executable hook silently",
path.display()
);
}
pub(crate) fn clear_executable(path: &std::path::Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)
.expect("the file must exist before its mode is changed")
.permissions();
perms.set_mode(perms.mode() & !0o111);
std::fs::set_permissions(path, perms).expect("clearing the executable bit must succeed");
}
#[cfg(not(unix))]
let _ = path;
}
pub(crate) const MODELS_DEV_DOCUMENT: &str = r#"{
"kimi-for-coding": {
"id": "kimi-for-coding",
"name": "Kimi For Coding",
"api": "https://api.kimi.com/coding/v1",
"models": {
"k3": {
"id": "k3",
"name": "Kimi K3",
"reasoning": true,
"temperature": false,
"limit": { "context": 262144, "output": 131072 }
},
"kimi-for-coding": {
"id": "kimi-for-coding",
"temperature": false,
"limit": { "context": 262144, "output": 32768 }
}
}
},
"zai-coding-plan": {
"id": "zai-coding-plan",
"api": "https://api.z.ai/api/coding/paas/v4",
"models": {
"glm-5.3": {
"id": "glm-5.3",
"temperature": false,
"limit": { "context": 204800, "output": 131072 }
},
"glm-5.2": {
"id": "glm-5.2",
"temperature": true,
"limit": { "context": 204800, "output": 131072 }
}
}
},
"openai": {
"id": "openai",
"api": null,
"models": {
"gpt-5.6-sol": {
"id": "gpt-5.6-sol",
"temperature": false,
"limit": { "context": 400000, "output": 128000 }
}
}
},
"blank-endpoint": {
"id": "blank-endpoint",
"api": " ",
"models": { "nowhere": { "id": "nowhere", "temperature": false } }
},
"quiet-vendor": {
"id": "quiet-vendor",
"api": "https://quiet.example/v1",
"models": { "unspecified": { "id": "unspecified" } }
}
}"#;