use semlith::Semlith;
use std::path::Path;
fn row<'a>(
client: &'a str,
query: &'a str,
excerpt: i64,
whole: i64,
) -> semlith::store::NewRetrieval<'a> {
semlith::store::NewRetrieval {
client,
session: "test",
tool: "search",
query,
hits: 1,
micros: 4200,
excerpt_tokens: excerpt,
whole_file_tokens: whole,
stale_hits: 0,
tokenizer: semlith::ledger::CHARS4,
query_id: "",
}
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn nothing_is_recorded_unless_recording_was_asked_for() {
let (corpus, store) = corpus();
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
s.search("sourdough", 3).unwrap();
assert_eq!(
rows(s.db()),
0,
"a search recorded a row without anyone switching recording on"
);
assert!(semlith::store::ledger_break(s.db()).unwrap().is_none());
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn one_recorded_retrieval_is_one_row_the_cli_prints() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let s = Semlith::open(store.path(), None).unwrap();
semlith::store::record_retrieval(s.db(), &row("claude-code", "sourdough starter", 180, 4000))
.unwrap();
assert_eq!(rows(s.db()), 1);
let (queries, clients, excerpt, whole) = semlith::store::ledger_totals(s.db()).unwrap();
assert_eq!((queries, clients, excerpt, whole), (1, 1, 180, 4000));
drop(s);
let printed = cli(store.path(), &["ledger", "--last", "5"]);
assert!(printed.contains("sourdough starter"), "{printed}");
assert!(printed.contains("claude-code"), "{printed}");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn editing_a_row_breaks_the_chain_and_the_break_is_found() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let s = Semlith::open(store.path(), None).unwrap();
for i in 0..4 {
semlith::store::record_retrieval(s.db(), &row("agent", &format!("query {i}"), 5, 50))
.unwrap();
}
assert!(
semlith::store::ledger_break(s.db()).unwrap().is_none(),
"an untouched ledger must verify"
);
semlith::store::read_only(s.db(), false).unwrap();
s.db()
.execute(
"UPDATE retrievals SET query = 'something else' WHERE id = 2",
[],
)
.unwrap();
semlith::store::read_only(s.db(), true).unwrap();
assert_eq!(
semlith::store::ledger_break(s.db()).unwrap(),
Some(2),
"the edited row was not detected"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn an_empty_ledger_explains_itself() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let printed = cli(store.path(), &["ledger"]);
assert!(printed.contains("nothing recorded"), "{printed}");
assert!(
printed.contains("ever leaves this machine"),
"the empty case should still say where the data would live: {printed}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn an_older_store_gains_an_empty_ledger_and_keeps_its_format() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
{
let db = rusqlite::Connection::open(store.path().join("store.db")).unwrap();
db.execute_batch("DROP TABLE retrievals; DROP TABLE edges; DROP TABLE symbols;")
.unwrap();
}
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
assert_eq!(rows(s.db()), 0, "the ledger came back empty");
assert_eq!(
semlith::store::format(s.db()).unwrap(),
2,
"opening a store must not move its format version"
);
assert!(
!s.search("sourdough", 3).unwrap().is_empty(),
"the store still searches with an empty graph and an empty ledger"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_search_and_a_graph_call_over_stdio_are_recorded_under_the_client_name() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let script = [
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"harness","version":"1"}}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"semlith_search","arguments":{"query":"sourdough","k":3}}}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"semlith_neighbors","arguments":{"name":"take"}}}"#,
]
.join("\n");
let mut fleet = semlith::fleet::Fleet::open(&[store.path().to_path_buf()]).unwrap();
let mut out = Vec::new();
semlith::mcp::serve(&mut fleet, script.as_bytes(), &mut out).unwrap();
drop(fleet);
let s = Semlith::open(store.path(), None).unwrap();
let rows = semlith::store::retrievals(s.db(), 10).unwrap();
assert!(
!rows.is_empty(),
"an agent's retrievals over stdio were recorded as nothing, which is the \
defect this release exists to fix"
);
assert!(
rows.iter().all(|r| r.client == "harness"),
"the ledger records the name the client gave itself: {rows:?}"
);
assert!(
semlith::store::ledger_break(s.db()).unwrap().is_none(),
"the chain must verify over rows written by the new formula"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_search_from_the_command_line_is_recorded_as_cli() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
cli(store.path(), &["search", "sourdough", "--k", "3"]);
let s = Semlith::open(store.path(), None).unwrap();
let rows = semlith::store::retrievals(s.db(), 10).unwrap();
assert!(
rows.iter().any(|r| r.client == "cli"),
"the command line is a client like any other: {rows:?}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_recorded_retrieval_says_the_model_counted_it() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let mut fleet = semlith::fleet::Fleet::open(&[store.path().to_path_buf()]).unwrap();
fleet.quiet = true;
let hits = fleet
.search_filtered("sourdough", 3, &semlith::filter::Filter::default())
.unwrap();
assert!(!hits.is_empty(), "the fixture corpus should answer this");
semlith::ledger::search(
&fleet,
&semlith::ledger::Who {
client: "harness",
session: "harness",
},
"sourdough",
&hits,
std::time::Duration::from_millis(1),
);
drop(fleet);
let s = Semlith::open(store.path(), None).unwrap();
let labels: Vec<String> = s
.db()
.prepare("SELECT tokenizer FROM retrievals")
.unwrap()
.query_map([], |r| r.get(0))
.unwrap()
.map(|r| r.unwrap())
.collect();
assert!(!labels.is_empty(), "nothing was recorded");
assert!(
labels.iter().all(|l| l == semlith::ledger::MODEL),
"the ledger fell back to an estimate while claiming to count: {labels:?}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_store_holding_rows_of_both_formulas_verifies_and_still_catches_an_edit() {
let (corpus, store) = corpus();
{
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.path().to_path_buf()], |_, _| {})
.unwrap();
}
let s = Semlith::open(store.path(), None).unwrap();
semlith::store::record_legacy_retrieval(s.db(), "claude-code", "old one", 10, 100).unwrap();
semlith::store::record_legacy_retrieval(s.db(), "claude-code", "old two", 10, 100).unwrap();
semlith::store::record_retrieval(s.db(), &row("cursor", "new one", 20, 200)).unwrap();
semlith::store::record_retrieval(s.db(), &row("cursor", "new two", 20, 200)).unwrap();
assert_eq!(rows(s.db()), 4);
assert!(
semlith::store::ledger_break(s.db()).unwrap().is_none(),
"a store written by two releases must verify across the boundary"
);
semlith::store::read_only(s.db(), false).unwrap();
s.db()
.execute("UPDATE retrievals SET query = 'tampered' WHERE id = 1", [])
.unwrap();
semlith::store::read_only(s.db(), true).unwrap();
assert_eq!(
semlith::store::ledger_break(s.db()).unwrap(),
Some(1),
"an edit to a pre-0.15.0 row was not detected"
);
semlith::store::read_only(s.db(), false).unwrap();
s.db()
.execute("UPDATE retrievals SET query = 'old one' WHERE id = 1", [])
.unwrap();
s.db()
.execute("UPDATE retrievals SET stale_hits = 9 WHERE id = 3", [])
.unwrap();
semlith::store::read_only(s.db(), true).unwrap();
assert_eq!(
semlith::store::ledger_break(s.db()).unwrap(),
Some(3),
"an edit to one of the new columns was not detected, which would mean the \
chain does not actually cover them"
);
}
fn corpus() -> (tempfile::TempDir, tempfile::TempDir) {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
std::fs::write(
corpus.path().join("bread.md"),
"Sourdough starter needs flour and water and a warm shelf.",
)
.unwrap();
std::fs::write(
corpus.path().join("lock.rs"),
"fn hold() {}\nfn take() { hold(); }\n",
)
.unwrap();
(corpus, store)
}
fn rows(db: &rusqlite::Connection) -> i64 {
db.query_row("SELECT COUNT(*) FROM retrievals", [], |r| r.get(0))
.unwrap()
}
fn cli(store: &Path, args: &[&str]) -> String {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_semlith"))
.arg("--store")
.arg(store)
.args(args)
.output()
.expect("the binary runs");
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
)
}
#[test]
fn rows_sharing_a_query_id_count_as_one_retrieval() {
let dir = tempfile::tempdir().unwrap();
let s = Semlith::open(dir.path(), None).unwrap();
let shared = "one-search";
for (excerpt, whole) in [(10, 100), (20, 200)] {
let mut r = row("portal", "sourdough", excerpt, whole);
r.query_id = shared;
semlith::store::record_retrieval(s.db(), &r).unwrap();
}
semlith::store::record_retrieval(s.db(), &row("cli", "rye", 5, 50)).unwrap();
let (queries, _, excerpt, whole) = semlith::store::ledger_totals(s.db()).unwrap();
assert_eq!(queries, 2, "two searches, three rows");
assert_eq!((excerpt, whole), (35, 350));
assert_eq!(semlith::store::ledger_break(s.db()).unwrap(), None);
let with_id = semlith::store::retrievals(s.db(), 10).unwrap();
assert!(
with_id.iter().any(|row| row.query_id == shared),
"the id is still stored and read back, it is only left out of the hash"
);
}