use std::path::PathBuf;
use agentstatedeveloper_core::{
AsgIndexStore, AsgLedgerStore, Author, AuthorKind, Engine, IndexStore, LedgerEntry, LedgerKind,
LedgerStore, Position, Symbol, SymbolKind,
thinking::{DEFAULT_CONFIDENCE_FLOOR, gather_prior_thinking},
};
fn fresh_engine() -> (tempfile::TempDir, PathBuf, Engine) {
let tmp = tempfile::tempdir().expect("tempdir");
let db_path = tmp.path().join(".asd-state.db");
let engine = Engine::open_sqlite(&db_path).expect("open sqlite engine");
(tmp, db_path, engine)
}
fn put_sym(engine: &Engine, qname: &str) -> String {
let index = AsgIndexStore::from_engine(engine);
let sym = Symbol {
symbol_id: format!("sym_{}", qname.replace('.', "_")),
symbol_fp: "fp".into(),
qname: qname.into(),
language: "python".into(),
kind: SymbolKind::Function,
file: "src/x.py".into(),
start: Position { line: 1, col: 0 },
end: Position { line: 5, col: 0 },
signature: None,
doc: None,
};
index.put_symbol(&engine.ref_name, &sym, "t").expect("put");
sym.symbol_id
}
fn det_id(intent: &str, qname: &str, content: &str) -> String {
let key = format!("think:{intent}:{qname}:{content}");
let h = blake3::hash(key.as_bytes()).to_hex();
let short: String = h.chars().take(24).collect();
format!("led_think_{short}")
}
fn append_think(
engine: &Engine,
sym_id: &str,
kind: LedgerKind,
entry_id: &str,
summary: &str,
conf: Option<f64>,
body: Option<&str>,
tags: Vec<String>,
) {
let ledger = AsgLedgerStore::from_engine(engine);
let mut entry = LedgerEntry::new(
sym_id,
kind,
summary,
Author {
kind: AuthorKind::Agent,
id: "asd-think".into(),
},
);
entry.entry_id = entry_id.to_string();
entry.confidence = conf;
entry.body = body.map(str::to_string);
entry.tags = tags;
ledger
.append_entry(&engine.ref_name, &entry, "asd-think")
.expect("append");
}
#[test]
fn end_to_end_thinking_surfaces_in_prior_thinking_projection() {
let (_tmp, _db, engine) = fresh_engine();
let qn = "pkg.target".to_string();
let sid = put_sym(&engine, &qn);
append_think(
&engine,
&sid,
LedgerKind::Hypothesis,
&det_id("hypothesis", &qn, "strong claim"),
"strong claim",
Some(0.7),
None,
vec!["source:asd-think".into()],
);
append_think(
&engine,
&sid,
LedgerKind::Hypothesis,
&det_id("hypothesis", &qn, "weak guess"),
"weak guess",
Some(0.1),
None,
vec!["source:asd-think".into()],
);
append_think(
&engine,
&sid,
LedgerKind::MentalModel,
&det_id("model", "audio-pipeline", "in→mix→out"),
"audio-pipeline: in→mix→out",
None,
Some(r#"{"symbols":["pkg.target","pkg.other"],"name":"audio-pipeline"}"#),
vec!["source:asd-think".into()],
);
append_think(
&engine,
&sid,
LedgerKind::FailedAttempt,
&det_id("failed", &qn, "caching"),
"failed: caching — because broke under reload",
None,
Some(r#"{"tried":"caching","because":"broke under reload"}"#),
vec!["source:asd-think".into()],
);
append_think(
&engine,
&sid,
LedgerKind::OpenQuestion,
&det_id("question", &qn, "what is 4096?"),
"what is 4096?",
None,
None,
vec!["source:asd-think".into()],
);
let pt = gather_prior_thinking(&engine, &[qn.clone()], DEFAULT_CONFIDENCE_FLOOR);
let v = pt.entries;
let o = v.as_object().expect("non-null projection");
let hyps = o["hypotheses"].as_array().expect("hypotheses");
assert_eq!(hyps.len(), 1, "below-floor hypothesis must be filtered out");
assert_eq!(hyps[0]["confidence"].as_f64(), Some(0.7));
let mm = o["mental_models"].as_array().expect("mental_models");
assert_eq!(mm.len(), 1);
assert_eq!(mm[0]["name"].as_str(), Some("audio-pipeline"));
assert_eq!(mm[0]["symbols"].as_array().unwrap().len(), 2);
let fa = o["failed_attempts"].as_array().expect("failed_attempts");
assert_eq!(fa[0]["tried"].as_str(), Some("caching"));
assert_eq!(fa[0]["because"].as_str(), Some("broke under reload"));
let oq = o["open_questions"].as_array().expect("open_questions");
assert_eq!(oq[0]["question"].as_str(), Some("what is 4096?"));
}
#[test]
fn deterministic_entry_id_is_idempotent_on_replay() {
let (_tmp, _db, engine) = fresh_engine();
let qn = "pkg.idem".to_string();
let sid = put_sym(&engine, &qn);
let id = det_id("hypothesis", &qn, "same claim");
append_think(
&engine,
&sid,
LedgerKind::Hypothesis,
&id,
"same claim",
Some(0.5),
None,
vec!["source:asd-think".into()],
);
append_think(
&engine,
&sid,
LedgerKind::Hypothesis,
&id,
"same claim",
Some(0.5),
None,
vec!["source:asd-think".into()],
);
let ledger = AsgLedgerStore::from_engine(&engine);
let entries = ledger.list_entries(&engine.ref_name, &sid).expect("list");
let hyps: Vec<_> = entries
.iter()
.filter(|e| e.kind == LedgerKind::Hypothesis)
.collect();
assert_eq!(
hyps.len(),
1,
"deterministic id must collapse re-runs to a single row"
);
}
#[test]
fn gather_prior_thinking_returns_null_when_only_non_thinking_kinds_exist() {
let (_tmp, _db, engine) = fresh_engine();
let qn = "pkg.other".to_string();
let sid = put_sym(&engine, &qn);
append_think(
&engine,
&sid,
LedgerKind::Decision,
"led_dec_1",
"decided X",
None,
None,
vec![],
);
append_think(
&engine,
&sid,
LedgerKind::Mapping,
"led_map_1",
"covers Z",
None,
None,
vec![],
);
let pt = gather_prior_thinking(&engine, &[qn], DEFAULT_CONFIDENCE_FLOOR);
assert_eq!(pt.entries, serde_json::Value::Null);
}