use super::*;
use crate::{
Adapter, BodyCapture, Classification, ComparisonRun, ComparisonSummary, RunInput,
TargetObservation,
};
use chrono::{TimeZone, Utc};
use std::collections::BTreeMap;
use tempfile::tempdir;
fn body() -> BodyCapture {
BodyCapture {
size_bytes: 0,
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
preview: String::new(),
truncated: false,
}
}
fn target(latency_ms: u128) -> TargetObservation {
TargetObservation {
status: Some(0),
headers: BTreeMap::new(),
body: body(),
stderr: None,
latency_ms,
error: None,
}
}
fn run(
path: impl Into<String>,
timestamp_seconds: i64,
classification: Classification,
secondary: bool,
) -> ComparisonRun {
let path = path.into();
ComparisonRun {
id: Uuid::new_v4(),
timestamp: Utc.timestamp_opt(timestamp_seconds, 0).unwrap(),
adapter: Adapter::Http,
input: RunInput::Http {
method: "GET".to_string(),
path,
query: None,
},
request_headers: BTreeMap::new(),
request_body: body(),
primary: target(10),
candidate: target(20),
secondary: secondary.then(|| target(30)),
comparison: ComparisonSummary {
classification,
..Default::default()
},
}
}
#[tokio::test]
async fn load_creates_parent_directory() {
let dir = tempdir().unwrap();
let path = dir.path().join("nested").join("http-runs.jsonl");
let _storage = Storage::load(path.clone()).await.unwrap();
assert!(path.parent().unwrap().exists());
}
#[tokio::test]
async fn run_writer_creates_parent_directory() {
let dir = tempdir().unwrap();
let path = dir.path().join("nested").join("cli-runs.jsonl");
let writer = RunWriter::open(path.clone()).await.unwrap();
writer
.append(&run("writer", 1, Classification::Match, false))
.await
.unwrap();
writer.flush().await.unwrap();
assert!(path.exists());
}
#[tokio::test]
async fn run_writer_appends_without_loading_existing_files() {
let dir = tempdir().unwrap();
let path = dir.path().join("cli-runs.jsonl");
std::fs::write(dir.path().join("corrupt.jsonl"), "not-json\n").unwrap();
let writer = RunWriter::open(path.clone()).await.unwrap();
writer
.append(&run("writer", 1, Classification::Match, false))
.await
.unwrap();
writer.flush().await.unwrap();
let lines = std::fs::read_to_string(path).unwrap();
assert_eq!(lines.lines().count(), 1);
}
#[tokio::test]
async fn load_skips_empty_and_corrupt_jsonl_lines() {
let dir = tempdir().unwrap();
let path = dir.path().join("http-runs.jsonl");
let valid = serde_json::to_string(&run("valid", 1, Classification::Match, false)).unwrap();
std::fs::write(&path, format!("\n{valid}\nnot-json\n\n")).unwrap();
let storage = Storage::load(path).await.unwrap();
let runs = storage.list().await;
assert_eq!(runs.len(), 1);
assert!(matches!(
runs[0].input,
RunInput::Http { ref path, .. } if path == "valid"
));
}
#[tokio::test]
async fn list_returns_newest_first() {
let dir = tempdir().unwrap();
let storage = Storage::load(dir.path().join("http-runs.jsonl"))
.await
.unwrap();
let first = run("first", 1, Classification::Match, false);
let second = run("second", 2, Classification::SuspiciousDifference, false);
storage.insert(first).await.unwrap();
storage.insert(second).await.unwrap();
let runs = storage.list().await;
assert!(matches!(
runs[0].input,
RunInput::Http { ref path, .. } if path == "second"
));
assert!(matches!(
runs[1].input,
RunInput::Http { ref path, .. } if path == "first"
));
}
#[tokio::test]
async fn load_merges_jsonl_files_in_same_directory() {
let dir = tempdir().unwrap();
let http_path = dir.path().join("http-runs.jsonl");
let cli_path = dir.path().join("cli-runs.jsonl");
std::fs::write(
&http_path,
format!(
"{}\n",
serde_json::to_string(&run("http", 1, Classification::Match, false)).unwrap()
),
)
.unwrap();
std::fs::write(
&cli_path,
format!(
"{}\n",
serde_json::to_string(&run("cli", 2, Classification::ReferenceNoise, true)).unwrap()
),
)
.unwrap();
let storage = Storage::load(http_path).await.unwrap();
let stats = storage.stats().await;
assert_eq!(stats.total_runs, 2);
assert_eq!(stats.matches, 1);
assert_eq!(stats.reference_noise, 1);
}
#[tokio::test]
async fn storage_load_still_scans_directory_for_admin_views() {
let dir = tempdir().unwrap();
let http_path = dir.path().join("http-runs.jsonl");
let cli_path = dir.path().join("cli-runs.jsonl");
std::fs::write(
&http_path,
format!(
"{}\n",
serde_json::to_string(&run("http", 1, Classification::Match, false)).unwrap()
),
)
.unwrap();
std::fs::write(
&cli_path,
format!(
"{}\n",
serde_json::to_string(&run("cli", 2, Classification::SuspiciousDifference, false))
.unwrap()
),
)
.unwrap();
let storage = Storage::load(http_path).await.unwrap();
let stats = storage.stats().await;
assert_eq!(stats.total_runs, 2);
assert_eq!(stats.matches, 1);
assert_eq!(stats.suspicious_differences, 1);
}
#[tokio::test]
async fn stats_limits_latest_runs_to_20() {
let dir = tempdir().unwrap();
let storage = Storage::load(dir.path().join("http-runs.jsonl"))
.await
.unwrap();
for index in 0..25 {
storage
.insert(run(
format!("run-{index}"),
index,
Classification::Match,
false,
))
.await
.unwrap();
}
let stats = storage.stats().await;
assert_eq!(stats.total_runs, 25);
assert_eq!(stats.latest_runs.len(), 20);
assert!(matches!(
stats.latest_runs[0].input,
RunInput::Http { ref path, .. } if path == "run-24"
));
assert!(matches!(
stats.latest_runs[19].input,
RunInput::Http { ref path, .. } if path == "run-5"
));
}
#[tokio::test]
async fn stats_handles_missing_secondary_latencies() {
let dir = tempdir().unwrap();
let storage = Storage::load(dir.path().join("http-runs.jsonl"))
.await
.unwrap();
storage
.insert(run("primary-candidate", 1, Classification::Match, false))
.await
.unwrap();
let stats = storage.stats().await;
assert_eq!(stats.total_runs, 1);
assert_eq!(stats.latency.primary_avg_ms, 10.0);
assert_eq!(stats.latency.candidate_avg_ms, 20.0);
assert_eq!(stats.latency.secondary_avg_ms, None);
}