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 older = semlith::store::SHARDED_FORMAT;
{
let db = rusqlite::Connection::open(store.path().join("store.db")).unwrap();
db.execute_batch("DROP TABLE retrievals; DROP TABLE edges; DROP TABLE symbols;")
.unwrap();
db.execute(
"UPDATE meta SET v = ?1 WHERE k = ?2",
rusqlite::params![older.to_string(), semlith::store::FORMAT_KEY],
)
.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(),
older,
"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 fleet = semlith::fleet::Fleet::open(&[store.path().to_path_buf()]).unwrap();
let labels = fleet.labels().join(", ");
let fleet = std::sync::Mutex::new(fleet);
let mut out = Vec::new();
semlith::mcp::serve(&fleet, &labels, 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"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_raw_read_is_recorded_uncredited_and_does_not_break_the_chain() {
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();
drop(s);
let fleet = semlith::fleet::Fleet::open(&[store.path().to_path_buf()]).unwrap();
let held = corpus.path().join("bread.md").display().to_string();
assert!(
semlith::ledger::raw_read(&fleet, "claude-code", "s1", &held),
"a read of an indexed file must be recorded"
);
assert!(
!semlith::ledger::raw_read(&fleet, "claude-code", "s1", "/nowhere/x.rs"),
"a file no store holds must not be recorded against a store that does"
);
drop(fleet);
let s = Semlith::open(store.path(), None).unwrap();
let rows = semlith::store::retrievals(s.db(), 10).unwrap();
let raw: Vec<_> = rows.iter().filter(|r| r.query == held).collect();
assert_eq!(raw.len(), 1, "one raw read is one row: {rows:?}");
assert_eq!(raw[0].hits, 0, "a raw read retrieved nothing from semlith");
assert!(
raw[0].whole_file_tokens > 0,
"the row must say what reading it whole cost: {:?}",
raw[0]
);
let savings = semlith::store::ledger_savings(s.db()).unwrap();
assert_eq!(
savings.credited, 0,
"a raw read must never be credited with a saving"
);
assert_eq!(
savings.total, 1,
"it is still a retrieval, and a denominator"
);
assert_eq!(savings.coverage(), 0, "nothing was covered");
assert!(
semlith::store::ledger_break(s.db()).unwrap().is_none(),
"the new row kind broke the hash chain"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn the_ledger_states_its_saving_with_coverage_refunds_and_tier() {
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();
drop(s);
let fleet = semlith::fleet::Fleet::open(&[store.path().to_path_buf()]).unwrap();
let held = corpus.path().join("bread.md").display().to_string();
assert!(semlith::ledger::raw_read(
&fleet,
"claude-code",
"s1",
&held
));
drop(fleet);
let s = Semlith::open(store.path(), None).unwrap();
let savings = semlith::store::ledger_savings(s.db()).unwrap();
let misses = semlith::store::ledger_misses(s.db()).unwrap();
drop(s);
for args in [vec!["ledger"], vec!["ledger", "--verify"]] {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_semlith"))
.args(&args)
.arg("-s")
.arg(store.path())
.output()
.expect("running semlith ledger");
let said = String::from_utf8_lossy(&out.stdout).to_string();
for wanted in [
format!("coverage {} %", savings.coverage()),
format!("tier {}", savings.tier()),
format!("refunds {}", misses.refunds),
format!("zero-hit {}", misses.zero_hit),
] {
assert!(
said.contains(&wanted),
"`semlith {}` does not state {wanted:?}:\n{said}",
args.join(" ")
);
}
assert!(
said.contains("clients: claude-code"),
"`semlith {}` does not name the clients:\n{said}",
args.join(" ")
);
assert!(
said.contains("counted with the store's tokenizer"),
"the token helper is missing:\n{said}"
);
}
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn the_ledger_groups_retrievals_into_sessions() {
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();
let write = |client: &str, session: &str, query: &str, hits: i64, excerpt, whole| {
let mut r = row(client, query, excerpt, whole);
r.session = session;
r.hits = hits;
semlith::store::record_retrieval(s.db(), &r).unwrap();
};
write("claude-code", "s1", "how does the lock work", 3, 100, 4_000);
write("claude-code", "s1", "who calls acquire", 2, 150, 2_000);
write("claude-code", "s1", "nothing at all", 0, 40, 900);
write("cursor", "s2", "where is the parser", 1, 90, 1_200);
let sessions = semlith::store::ledger_sessions(s.db(), 50).unwrap();
assert_eq!(sessions.len(), 2, "two sessions: {sessions:?}");
let one = sessions
.iter()
.find(|row| row.session == "s1")
.expect("the claude-code session");
assert_eq!(one.client, "claude-code");
assert_eq!(one.retrievals, 3);
assert_eq!(one.zero_hit, 1, "a zero-hit read is recorded, not dropped");
assert_eq!(one.net, (4_000 - 100) + (2_000 - 150));
assert_eq!(
one.tier(),
"modelled",
"rows counted by the four-character fallback are modelled"
);
let two = sessions
.iter()
.find(|row| row.session == "s2")
.expect("the cursor session");
assert_eq!(two.retrievals, 1);
assert_eq!(two.net, 1_200 - 90);
}
#[test]
fn the_ledger_page_can_filter_sort_page_and_export_its_sessions() {
const APP_JS: &str = include_str!("../src/portal/app.js");
for wanted in [
"function ledgerSessions(",
"Filter by client",
"Filter by tier",
"cost at ",
"w-sessions",
"semlith-sessions",
] {
assert!(
APP_JS.contains(wanted),
"the sessions table is missing {wanted:?}"
);
}
let block = APP_JS
.split("function ledgerSessions(")
.nth(1)
.expect("ledgerSessions exists");
assert!(
block.contains("dataTable({"),
"the table is not a dataTable"
);
assert!(block.contains("perPage:"), "the table does not page");
assert!(block.contains("sort:"), "the table does not sort");
assert!(
block.contains("exportRows(") && block.contains("shown()"),
"export must write the filtered rows, not every row"
);
for format in ["Markdown", "CSV", "JSON"] {
assert!(block.contains(format), "no {format} export");
}
}
#[test]
fn session_replay_is_off_until_the_privacy_page_turns_it_on() {
let fresh = semlith::home::Settings::default();
assert_eq!(fresh.session_replay, None);
assert!(!fresh.session_replay.unwrap_or(false));
let empty = tempfile::tempdir().unwrap();
let found = semlith::replay::read(empty.path(), 10).unwrap();
assert!(found.sessions.is_empty());
assert_eq!(found.client, "claude-code");
}
#[test]
fn a_transcript_marks_each_answer_with_what_the_agent_did_next() {
let home = tempfile::tempdir().unwrap();
let project = home.path().join("-Users-someone-work");
std::fs::create_dir_all(&project).unwrap();
let line = |name: &str| {
format!(
r#"{{"type":"assistant","message":{{"content":[{{"type":"tool_use","name":"{name}"}}]}}}}"#
)
};
let transcript = [
line("mcp__semlith__semlith_search"),
line("Read"),
line("mcp__semlith__semlith_impact"),
line("Edit"),
line("mcp__semlith__semlith_search"),
line("Grep"),
r#"{"type":"user","message":{"content":"plain text"}}"#.to_string(),
]
.join("\n");
std::fs::write(project.join("abc123.jsonl"), transcript).unwrap();
let found = semlith::replay::read(home.path(), 10).unwrap();
assert_eq!(found.sessions.len(), 1, "{found:?}");
let session = &found.sessions[0];
assert_eq!(session.id, "abc123");
assert_eq!(session.answers, 3);
assert_eq!(session.refund, 1, "a whole-file read after an answer");
assert_eq!(session.sufficed, 1, "an edit after an answer");
assert_eq!(session.miss, 1, "a grep after an answer");
assert_eq!(session.unknown, 0);
}