use super::*;
use crate::template::RealGit;
use crate::workspace::{config_ref, fixture, proposal::proposal_ref, repo_git};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::Cursor;
use tempfile::TempDir;
pub(super) struct StubEnv(pub(super) HashMap<&'static str, OsString>);
impl EnvLookup for StubEnv {
fn get(&self, key: &str) -> Option<OsString> {
self.0.get(key).cloned()
}
}
pub(super) fn caller(home: &Path, ws: &Path, agent: &str) -> StubEnv {
let mut m = HashMap::new();
m.insert(ENV_CONV_REPO, ws.as_os_str().to_owned());
m.insert(ENV_CONV_BRANCH, OsString::from(agent));
m.insert(
super::super::harness::ENV_LITANY_HOME,
home.as_os_str().to_owned(),
);
StubEnv(m)
}
pub(super) const AGENT: &str = "20260101-a1";
pub(super) fn fixture() -> (TempDir, PathBuf, PathBuf) {
let holder = TempDir::new().unwrap();
let home = holder.path().join("home");
std::fs::create_dir_all(&home).unwrap();
let roots = crate::harness_root::Roots {
config: home.clone(),
data: home.clone(),
};
let ws = fixture::workspace_under(&roots);
fixture::spawn_root(&ws, AGENT);
(holder, home, ws)
}
pub(super) fn remember(home: &Path, ws: &Path, fact: &str) -> Result<String, Error> {
let env = caller(home, ws, AGENT);
let mut out: Vec<u8> = Vec::new();
let input = serde_json::to_vec(&serde_json::json!({ "fact": fact })).unwrap();
run_with(&mut Cursor::new(input), &mut out, &env, &RealGit::new())?;
Ok(String::from_utf8(out).unwrap())
}
pub(super) fn facts(ws: &Path, refname: &str) -> Option<String> {
let spec = format!("{refname}:{}", crate::facts::FILE);
RealGit::new()
.run_capture(&repo_git(ws), &["show", &spec])
.ok()
}
#[test]
fn a_fact_is_staged_as_a_proposal_and_no_lineage_moves() {
let (_h, home, ws) = fixture();
let git = RealGit::new();
let before = git
.run_capture(&repo_git(&ws), &["rev-parse", &config_ref("default")])
.unwrap();
let out = remember(&home, &ws, "raw observations are kept for 45 days").unwrap();
assert!(out.contains("\"status\":\"proposed\""), "{out}");
assert!(out.contains(AGENT), "{out}");
assert!(out.contains("--accept"), "{out}");
assert_eq!(
facts(&ws, &proposal_ref(AGENT)).as_deref(),
Some("raw observations are kept for 45 days"),
"the fact is on the proposal branch"
);
assert_eq!(facts(&ws, &config_ref("default")), None, "and nowhere else");
assert_eq!(
before,
git.run_capture(&repo_git(&ws), &["rev-parse", &config_ref("default")])
.unwrap(),
"the lineage did not move"
);
let subject = git
.run_capture(
&repo_git(&ws),
&["log", "-1", "--format=%s", &proposal_ref(AGENT)],
)
.unwrap();
assert_eq!(subject, "facts: raw observations are kept for 45 days");
}
#[test]
fn a_second_fact_amends_the_standing_proposal_and_it_stays_fresh() {
let (_h, home, ws) = fixture();
let git = RealGit::new();
remember(&home, &ws, "raw observations are kept for 45 days").unwrap();
let out = remember(&home, &ws, "the ingest window closes at 02:00 UTC").unwrap();
assert!(out.contains("\"status\":\"proposed\""), "{out}");
assert_eq!(
facts(&ws, &proposal_ref(AGENT)).as_deref(),
Some("raw observations are kept for 45 days\n\nthe ingest window closes at 02:00 UTC"),
);
let count = git
.run_capture(
&repo_git(&ws),
&[
"rev-list",
"--count",
&format!("{}..{}", config_ref("default"), proposal_ref(AGENT)),
],
)
.unwrap();
assert_eq!(count, "1", "one commit ahead of the lineage, not two");
let rows = crate::workspace::proposal::list(&ws, &git).unwrap();
assert_eq!(rows.len(), 1);
assert!(rows[0].fresh, "the amended proposal is still fresh");
}
#[test]
fn a_fact_already_recorded_proposes_nothing() {
let (_h, home, ws) = fixture();
let out = remember(&home, &ws, "the same fact").unwrap();
assert!(out.contains("\"status\":\"proposed\""), "{out}");
let again = remember(&home, &ws, " the same fact\n").unwrap();
assert!(again.contains("\"status\":\"already_recorded\""), "{again}");
assert_eq!(
facts(&ws, &proposal_ref(AGENT)).as_deref(),
Some("the same fact")
);
}
#[test]
fn a_duplicate_of_a_recorded_fact_leaves_no_proposal_branch() {
let (_h, home, ws) = fixture();
crate::template::authoring::author(
&ws,
&home,
"default",
crate::template::authoring::Origin::Advance,
|dir| std::fs::write(dir.join(crate::facts::FILE), "already known\n"),
&RealGit::new(),
)
.unwrap();
let out = remember(&home, &ws, "already known").unwrap();
assert!(out.contains("\"status\":\"already_recorded\""), "{out}");
assert!(
facts(&ws, &proposal_ref(AGENT)).is_none(),
"no proposal branch was left behind"
);
}
#[test]
fn an_over_cap_fact_is_refused_and_stages_nothing() {
let (_h, home, ws) = fixture();
let huge = "x".repeat(crate::facts::MAX_BYTES as usize + 1);
let err = remember(&home, &ws, &huge).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("facts.md"), "{msg}");
assert!(
facts(&ws, &proposal_ref(AGENT)).is_none(),
"a refused pass stages nothing"
);
assert!(
!ws.join(".config-author").exists(),
"and heals its checkout"
);
}
mod declines;