use std::{
env, fs,
io::{BufRead, BufReader, Read, Write},
net::{TcpListener, TcpStream},
path::{Path, PathBuf},
process::{Command, Output},
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
thread::{self, JoinHandle},
};
pub(crate) fn run(config: &Path, args: &[&str], embedding_url: &str) -> Output {
Command::new(bin())
.env("CLAWGALLERY_CONFIG_DIR", config)
.env("CLAWGALLERY_VDR_EMBEDDING_URL", embedding_url)
.env_remove("OPENAI_API_KEY")
.env("CODEX_HOME", config.join("codex-home"))
.args(args)
.output()
.expect("clawgallery command should run")
}
pub(crate) fn assert_success(output: Output) -> String {
if !output.status.success() {
panic!(
"command failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
String::from_utf8_lossy(&output.stdout).to_string()
}
pub(crate) struct FakeEmbeddingServer {
url: String,
requests: Arc<AtomicUsize>,
_statuses: Arc<Mutex<Vec<u16>>>,
_handle: JoinHandle<()>,
}
impl FakeEmbeddingServer {
pub(crate) fn start() -> Self {
Self::start_with_mode(false)
}
pub(crate) fn start_with_response_model(model: &'static str) -> Self {
Self::start_with_mode_and_response_model(false, Some(model))
}
pub(crate) fn start_with_statuses(statuses: Vec<u16>) -> Self {
Self::start_with_mode_response_model_and_statuses(false, None, statuses)
}
#[allow(dead_code)]
pub(crate) fn start_multivector() -> Self {
Self::start_with_mode_and_response_model(true, None)
}
fn start_with_mode(multivector: bool) -> Self {
Self::start_with_mode_and_response_model(multivector, None)
}
fn start_with_mode_and_response_model(
multivector: bool,
response_model: Option<&'static str>,
) -> Self {
Self::start_with_mode_response_model_and_statuses(multivector, response_model, Vec::new())
}
fn start_with_mode_response_model_and_statuses(
multivector: bool,
response_model: Option<&'static str>,
statuses: Vec<u16>,
) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind fake embedding server");
let url = format!(
"http://{}",
listener.local_addr().expect("fake server local addr")
);
let requests = Arc::new(AtomicUsize::new(0));
let request_count = Arc::clone(&requests);
let statuses = Arc::new(Mutex::new(statuses));
let response_statuses = Arc::clone(&statuses);
let handle = thread::spawn(move || {
for stream in listener.incoming().flatten() {
request_count.fetch_add(1, Ordering::SeqCst);
let status = {
let mut statuses = response_statuses.lock().expect("status sequence lock");
if statuses.is_empty() {
200
} else {
statuses.remove(0)
}
};
handle_request(stream, multivector, response_model, status);
}
});
Self {
url,
requests,
_statuses: statuses,
_handle: handle,
}
}
pub(crate) fn url(&self) -> &str {
&self.url
}
pub(crate) fn request_count(&self) -> usize {
self.requests.load(Ordering::SeqCst)
}
}
pub(crate) fn write_caption(
config: &Path,
image_id: &str,
image_path: &Path,
title: &str,
description: &str,
) {
let caption = serde_json::json!({
"image_id": image_id,
"path": image_path,
"title": title,
"description": description,
"model": "test",
"provider": "test",
"created_at": "2026-06-05T00:00:00Z",
"filename_meaningful": false
});
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(config.join("captions.jsonl"))
.expect("open captions");
writeln!(file, "{caption}").expect("write caption");
}
pub(crate) fn image_id_for(config: &Path, name: &str) -> (String, PathBuf) {
let raw = fs::read_to_string(config.join("images.jsonl")).expect("images jsonl");
raw.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<serde_json::Value>(line).expect("image record"))
.find_map(|record| {
let path = PathBuf::from(record["path"].as_str()?);
path.ends_with(name)
.then(|| (record["id"].as_str().expect("image id").to_string(), path))
})
.expect("image record for name")
}
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_clawgallery"))
}
fn handle_request(
mut stream: TcpStream,
multivector: bool,
response_model: Option<&'static str>,
status: u16,
) {
let mut reader = BufReader::new(stream.try_clone().expect("clone stream"));
let mut content_len = 0_usize;
loop {
let mut line = String::new();
if reader.read_line(&mut line).expect("read header") == 0 {
return;
}
if line == "\r\n" {
break;
}
if let Some((name, value)) = line.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
content_len = value.trim().parse().expect("content length");
}
}
let mut body = vec![0_u8; content_len];
reader.read_exact(&mut body).expect("read request body");
let request: serde_json::Value = serde_json::from_slice(&body).expect("json request");
if status != 200 {
let body = format!("{{\"error\":\"status {status}\"}}");
let reply = format!(
"HTTP/1.1 {status} Test Status\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
stream.write_all(reply.as_bytes()).expect("write response");
return;
}
let inputs = request["inputs"].as_array().expect("inputs array");
let model = request["model"].as_str().unwrap_or("test-model");
let response_model = response_model.unwrap_or(model);
let dimensions = request["dimensions"].as_u64().unwrap_or(4) as usize;
let sparse = request.get("format").and_then(|value| value.as_str()) == Some("sparse")
|| model.contains("vsplade")
|| model.contains("v-splade");
let response = if sparse {
let embeddings: Vec<_> = inputs
.iter()
.map(|input| sparse_for(input, dimensions))
.collect();
serde_json::json!({
"model": response_model,
"dimensions": dimensions,
"format": "sparse",
"embeddings": embeddings
})
} else if multivector {
let embeddings: Vec<_> = inputs
.iter()
.map(|input| multivector_for(input, dimensions))
.collect();
serde_json::json!({
"model": response_model,
"dimensions": dimensions,
"embeddings": embeddings
})
} else {
let embeddings: Vec<_> = inputs
.iter()
.map(|input| embedding_for(input, dimensions))
.collect();
serde_json::json!({
"model": response_model,
"dimensions": dimensions,
"embeddings": embeddings
})
};
let body = response.to_string();
let reply = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
stream.write_all(reply.as_bytes()).expect("write response");
}
fn pad_dims(mut row: Vec<f32>, dimensions: usize) -> Vec<f32> {
row.resize(dimensions.max(1), 0.0);
row
}
fn embedding_for(input: &serde_json::Value, dimensions: usize) -> Vec<f32> {
let haystack = input["value"].as_str().unwrap_or_default().to_lowercase();
let row = if haystack.contains("dog") || haystack.contains("puppy") {
vec![1.0, 0.0, 0.0, 0.0]
} else if haystack.contains("cat") || haystack.contains("kitten") {
vec![0.0, 1.0, 0.0, 0.0]
} else if haystack.contains("new") || haystack.contains("fresh") {
vec![0.0, 0.0, 1.0, 0.0]
} else {
vec![0.0, 0.0, 0.0, 1.0]
};
pad_dims(row, dimensions)
}
fn sparse_for(input: &serde_json::Value, dimensions: usize) -> serde_json::Value {
let haystack = input["value"].as_str().unwrap_or_default().to_lowercase();
let (index, value) = if haystack.contains("dog") || haystack.contains("puppy") {
(1_u32, 2.0_f32)
} else if haystack.contains("cat") || haystack.contains("kitten") {
(2, 2.0)
} else if haystack.contains("login") {
(3, 2.0)
} else {
(0, 0.1)
};
let index = index.min(dimensions.saturating_sub(1) as u32);
serde_json::json!({"indices": [index], "values": [value]})
}
fn multivector_for(input: &serde_json::Value, dimensions: usize) -> Vec<Vec<f32>> {
let haystack = input["value"].as_str().unwrap_or_default().to_lowercase();
let mut rows: Vec<Vec<f32>> = haystack
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|word| !word.is_empty())
.map(|word| {
let row = if word.contains("dog") || word.contains("puppy") {
vec![1.0, 0.0, 0.0, 0.0]
} else if word.contains("cat") || word.contains("kitten") {
vec![0.0, 1.0, 0.0, 0.0]
} else if word.contains("new") || word.contains("fresh") {
vec![0.0, 0.0, 1.0, 0.0]
} else {
vec![0.0, 0.0, 0.0, 1.0]
};
pad_dims(row, dimensions)
})
.collect();
if rows.is_empty() {
rows.push(pad_dims(vec![0.0, 0.0, 0.0, 1.0], dimensions));
}
rows
}