#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)]
mod common;
use chrono::{DateTime, Utc};
use common::blz_cmd;
use serde_json::Value;
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn seed_source(
tmp: &tempfile::TempDir,
server: &MockServer,
alias: &str,
doc: &str,
) -> anyhow::Result<()> {
let url = format!("{}/llms.txt", server.uri());
Mock::given(method("HEAD"))
.and(path("/llms.txt"))
.respond_with(
ResponseTemplate::new(200).insert_header("content-length", doc.len().to_string()),
)
.mount(server)
.await;
Mock::given(method("GET"))
.and(path("/llms.txt"))
.respond_with(ResponseTemplate::new(200).set_body_string(doc))
.mount(server)
.await;
blz_cmd()
.env("BLZ_DATA_DIR", tmp.path())
.args(["add", alias, url.as_str(), "-y"])
.assert()
.success();
Ok(())
}
fn run_json(tmp: &tempfile::TempDir, args: &[&str]) -> anyhow::Result<Value> {
let stdout = blz_cmd()
.env("BLZ_DATA_DIR", tmp.path())
.args(args)
.assert()
.success()
.get_output()
.stdout
.clone();
Ok(serde_json::from_slice(&stdout)?)
}
fn assert_search_payload_schema(payload: &Value) {
for key in [
"query",
"page",
"limit",
"totalResults",
"totalPages",
"totalLinesSearched",
"searchTimeMs",
"sources",
"results",
] {
assert!(payload.get(key).is_some(), "missing top-level key: {key}");
}
assert!(
payload.get("total_hits").is_some(),
"missing compatibility key: total_hits"
);
assert!(
payload.get("execution_time_ms").is_some(),
"missing compatibility key: execution_time_ms"
);
assert_eq!(
payload.get("total_hits"),
payload.get("totalResults"),
"compat total_hits should mirror totalResults"
);
assert_eq!(
payload.get("execution_time_ms"),
payload.get("searchTimeMs"),
"compat execution_time_ms should mirror searchTimeMs"
);
}
fn assert_result_schema(result: &Value) {
for key in [
"source",
"file",
"headingPath",
"lines",
"snippet",
"scorePercentage",
"sourceUrl",
"checksum",
"anchor",
"fetchedAt",
"isStale",
] {
assert!(result.get(key).is_some(), "missing result key: {key}");
}
assert!(
result["score"].is_number(),
"score should be present and numeric"
);
let sp = result
.get("scorePercentage")
.and_then(Value::as_f64)
.expect("scorePercentage number");
assert!(
(0.0..=100.0).contains(&sp),
"scorePercentage out of range: {sp}"
);
}
#[tokio::test]
async fn search_json_schema_contains_expected_fields() -> anyhow::Result<()> {
let tmp = tempdir()?;
let server = MockServer::start().await;
let doc = "# Title\n\n## A\nalpha beta gamma\n\n## B\nbravo charlie\n";
seed_source(&tmp, &server, "e2e", doc).await?;
let payload = run_json(&tmp, &["search", "alpha", "--source", "e2e", "-f", "json"])?;
assert_search_payload_schema(&payload);
let results = payload
.get("results")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(!results.is_empty(), "expected at least one result");
let first = &results[0];
assert_result_schema(first);
let fetched_at = first
.get("fetchedAt")
.and_then(Value::as_str)
.expect("fetchedAt string present");
let parsed: DateTime<Utc> = fetched_at
.parse()
.expect("fetchedAt should parse as RFC3339 timestamp");
let now = Utc::now();
assert!(
parsed <= now,
"fetchedAt should not be in the future: {parsed} > {now}"
);
let is_stale = first
.get("isStale")
.and_then(Value::as_bool)
.expect("isStale boolean present");
assert!(!is_stale, "freshly added source should not be stale");
Ok(())
}
#[tokio::test]
async fn search_marks_stale_results_when_metadata_is_old() -> anyhow::Result<()> {
let tmp = tempdir()?;
let server = MockServer::start().await;
let doc = "# Title\n\n## Section\ncontent line one\ncontent line two\n";
seed_source(&tmp, &server, "aging", doc).await?;
let metadata_path = tmp
.path()
.join("sources")
.join("aging")
.join("metadata.json");
let mut metadata: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&metadata_path)?)?;
let old_timestamp = (Utc::now() - chrono::Duration::days(60)).to_rfc3339();
if let Value::Object(ref mut map) = metadata {
map.insert("fetched_at".to_string(), Value::String(old_timestamp));
}
std::fs::write(&metadata_path, serde_json::to_string_pretty(&metadata)?)?;
let v = run_json(
&tmp,
&["search", "content", "--source", "aging", "-f", "json"],
)?;
let result = v
.get("results")
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.cloned()
.expect("expected single result");
let is_stale = result
.get("isStale")
.and_then(Value::as_bool)
.expect("isStale boolean present");
assert!(is_stale, "stale metadata should yield isStale = true");
Ok(())
}