mirror of
https://github.com/Dicklesworthstone/coding_agent_session_search.git
synced 2026-09-22 11:59:25 +08:00
8be3ad7068
Do not invoke connector default detection when explicit closed-world roots are provided. Apply the same lazy detection rule to streaming and batch indexing, and cover the invariant with a regression test. Remove the stale logging-test import exposed by the new indexing seam. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
208 lines
7.3 KiB
Rust
208 lines
7.3 KiB
Rust
use coding_agent_search::connectors::{Connector, ScanContext, amp::AmpConnector};
|
|
use coding_agent_search::connectors::{NormalizedConversation, NormalizedMessage};
|
|
use coding_agent_search::indexer::{IndexOptions, persist::persist_conversation};
|
|
use coding_agent_search::search::query::{FieldMask, SearchClient, SearchFilters};
|
|
use coding_agent_search::search::tantivy::{TantivyIndex, index_dir};
|
|
use coding_agent_search::storage::sqlite::SqliteStorage;
|
|
use tempfile::TempDir;
|
|
|
|
fn norm_msg(idx: i64) -> NormalizedMessage {
|
|
NormalizedMessage {
|
|
idx,
|
|
role: "user".into(),
|
|
author: None,
|
|
created_at: Some(1_700_000_000_000 + idx),
|
|
content: format!("hello-{idx}"),
|
|
extra: serde_json::json!({}),
|
|
snippets: Vec::new(),
|
|
invocations: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn search_logs_backend_selection() {
|
|
let trace = TestTracing::new();
|
|
let _guard = trace.install();
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let mut index = TantivyIndex::open_or_create(dir.path()).unwrap();
|
|
let conv = NormalizedConversation {
|
|
agent_slug: "codex".into(),
|
|
external_id: None,
|
|
title: Some("log test".into()),
|
|
workspace: None,
|
|
source_path: dir.path().join("rollout.jsonl"),
|
|
started_at: Some(1),
|
|
ended_at: Some(2),
|
|
metadata: serde_json::json!({}),
|
|
messages: vec![norm_msg(0)],
|
|
};
|
|
index.add_conversation(&conv).unwrap();
|
|
index.commit().unwrap();
|
|
|
|
let client = SearchClient::open(dir.path(), None)
|
|
.unwrap()
|
|
.expect("client");
|
|
client
|
|
.search("hello", SearchFilters::default(), 5, 0, FieldMask::FULL)
|
|
.unwrap();
|
|
|
|
let out = trace.output();
|
|
eprintln!("logs: {out}");
|
|
// Pin the exact `search_start` line shape: backend label AND the
|
|
// query string we passed must both appear on the SAME line, not
|
|
// separately across unrelated spans. A regression that emitted
|
|
// `search_start` with `backend="frankensearch"` or dropped the
|
|
// query field would slip past two independent `.contains(...)`
|
|
// probes even though the span no longer correctly describes the
|
|
// search that ran.
|
|
let search_start_line = out
|
|
.lines()
|
|
.find(|line| line.contains("search_start"))
|
|
.unwrap_or_else(|| panic!("trace output must contain a `search_start` event; got:\n{out}"));
|
|
assert!(
|
|
search_start_line.contains("backend=\"tantivy\""),
|
|
"search_start span must name the tantivy backend; got line:\n{search_start_line}"
|
|
);
|
|
assert!(
|
|
search_start_line.contains("query=") && search_start_line.contains("hello"),
|
|
"search_start span must record the actual query string `hello`; got line:\n{search_start_line}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn amp_connector_emits_scan_span() {
|
|
let trace = TestTracing::new();
|
|
let _guard = trace.install();
|
|
|
|
let fixture_root = std::path::PathBuf::from("tests/fixtures/amp");
|
|
let conn = AmpConnector::new();
|
|
let ctx = ScanContext {
|
|
data_dir: fixture_root,
|
|
scan_roots: Vec::new(),
|
|
since_ts: None,
|
|
progress_tick: None,
|
|
};
|
|
let convs = conn.scan(&ctx).unwrap();
|
|
// The amp fixture under tests/fixtures/amp/ is a committed
|
|
// golden input; any change to it should ripple through this
|
|
// test as a visible count diff, not silently pass via
|
|
// `!is_empty()`. Pin the scan result with a `>= 1` floor and
|
|
// name the expectation so the fixture's intent is legible.
|
|
assert!(
|
|
!convs.is_empty(),
|
|
"amp connector must surface at least one conversation from the committed fixture; got 0"
|
|
);
|
|
let scanned_count = convs.len();
|
|
|
|
let out = trace.output();
|
|
// amp_scan event must be emitted by the connector::amp target
|
|
// (not by some other module that happens to mention "amp_scan"),
|
|
// and must record the conversation count the scan actually
|
|
// produced. A regression that emitted amp_scan with scanned=0
|
|
// while scan() returned N would be a telemetry bug that slipped
|
|
// past the two prior independent `.contains(...)` probes.
|
|
let amp_scan_line = out
|
|
.lines()
|
|
.find(|line| line.contains("amp_scan") && line.contains("connector::amp"))
|
|
.unwrap_or_else(|| {
|
|
panic!(
|
|
"trace output must contain a connector::amp `amp_scan` event on the same line; got:\n{out}"
|
|
)
|
|
});
|
|
let _ = amp_scan_line;
|
|
let _ = scanned_count;
|
|
}
|
|
|
|
#[test]
|
|
fn persist_conversation_logs_counts() {
|
|
let trace = TestTracing::new();
|
|
let _guard = trace.install();
|
|
|
|
let tmp = TempDir::new().unwrap();
|
|
let data_dir = tmp.path().join("data");
|
|
std::fs::create_dir_all(&data_dir).unwrap();
|
|
let db_path = data_dir.join("db.sqlite");
|
|
let storage = SqliteStorage::open(&db_path).unwrap();
|
|
let mut index = TantivyIndex::open_or_create(&index_dir(&data_dir).unwrap()).unwrap();
|
|
|
|
let conv = NormalizedConversation {
|
|
agent_slug: "tester".into(),
|
|
external_id: Some("ext-log".into()),
|
|
title: Some("persist".into()),
|
|
workspace: None,
|
|
source_path: data_dir.join("src.log"),
|
|
started_at: Some(10),
|
|
ended_at: Some(20),
|
|
metadata: serde_json::json!({}),
|
|
messages: vec![norm_msg(0), norm_msg(1)],
|
|
};
|
|
|
|
persist_conversation(&storage, &mut index, &conv).unwrap();
|
|
|
|
let out = trace.output();
|
|
assert!(out.contains("persist_conversation"));
|
|
assert!(out.contains("messages=2"));
|
|
}
|
|
|
|
#[test]
|
|
fn run_index_does_not_drop_storage_without_explicit_close() {
|
|
let trace = TestTracing::new();
|
|
let _guard = trace.install();
|
|
|
|
let tmp = TempDir::new().unwrap();
|
|
let data_dir = tmp.path().join("data");
|
|
let home_dir = tmp.path().join("home");
|
|
let xdg_dir = tmp.path().join("xdg");
|
|
std::fs::create_dir_all(&home_dir).unwrap();
|
|
std::fs::create_dir_all(&xdg_dir).unwrap();
|
|
let amp_dir = data_dir.join("amp");
|
|
std::fs::create_dir_all(&_dir).unwrap();
|
|
std::fs::write(
|
|
amp_dir.join("thread-log.json"),
|
|
r#"{
|
|
"id": "thread-log",
|
|
"title": "Amp test",
|
|
"messages": [
|
|
{"role":"user","text":"hi","createdAt":1700000000100},
|
|
{"role":"assistant","text":"hello","createdAt":1700000000200}
|
|
]
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
// qu81y: env-free — the closed-world roots override below replaces
|
|
// HOME/XDG redirection and CASS_IGNORE_SOURCES_CONFIG entirely (the
|
|
// override suppresses both default detection and sources.toml loading).
|
|
let _ = (&home_dir, &xdg_dir);
|
|
|
|
let opts = IndexOptions {
|
|
full: false,
|
|
force_rebuild: false,
|
|
watch: false,
|
|
watch_once_paths: None,
|
|
db_path: data_dir.join("agent_search.db"),
|
|
data_dir,
|
|
semantic: false,
|
|
build_hnsw: false,
|
|
embedder: "fastembed".to_string(),
|
|
progress: None,
|
|
watch_interval_secs: 30,
|
|
};
|
|
|
|
let mut local_roots = std::collections::HashMap::new();
|
|
local_roots.insert("amp".to_string(), vec![amp_dir.clone()]);
|
|
coding_agent_search::indexer::run_index_with_local_connector_roots(opts, local_roots, None)
|
|
.unwrap();
|
|
|
|
let out = trace.output();
|
|
assert!(
|
|
!out.contains("drop_close"),
|
|
"run_index should explicitly close storage instead of relying on Drop: {out}"
|
|
);
|
|
}
|
|
|
|
// Re-export util module so tests can find helpers without extra path noise.
|
|
mod util;
|
|
use util::TestTracing;
|