mod common;
use common::temp_store;
use hikmah_kernel::policy::KernelPolicy;
use hikmah_kernel::recall::{RecallQuery, RecallResult};
use hikmah_kernel::trace::{PrivacyClass, Trace, TraceKind};
use hikmah_kernel::MemoryStore;
fn store(name: &str) -> MemoryStore {
MemoryStore::open(temp_store(name), KernelPolicy::default()).unwrap()
}
fn region(store: &mut MemoryStore, key: &str, value: &str, source: &str) -> String {
claim(
store,
key,
value,
source,
&format!("The deploy region is {value}"),
)
}
fn claim(store: &mut MemoryStore, key: &str, value: &str, source: &str, content: &str) -> String {
let mut trace = Trace::new(TraceKind::Belief, content, source);
trace.claim_key = Some(key.into());
trace.claim_value = Some(value.into());
store.remember(trace).unwrap().0.id
}
fn result<'a>(results: &'a [RecallResult], id: &str) -> &'a RecallResult {
results
.iter()
.find(|r| r.trace.id == id)
.unwrap_or_else(|| panic!("{id} not recalled: {results:#?}"))
}
#[test]
fn recall_shows_conflicting_claims_beside_each_other() {
let mut s = store("recall-conflicts");
let east = region(&mut s, "deploy.region", "us-east-1", "runbook");
let west = region(&mut s, "Deploy.Region", "eu-west-1", "oncall");
let east_again = claim(
&mut s,
"deploy.region",
"us-east-1 ",
"ci",
"CI pipeline config pins the deploy region",
);
let mut note = Trace::new(TraceKind::Observation, "deploy region dashboards", "test");
note.tags = vec!["ops".into()];
let note = s.remember(note).unwrap().0.id;
let results = s.recall(&RecallQuery::new("deploy region"));
assert_eq!(result(&results, &east).conflicts, vec![west.clone()]);
assert_eq!(result(&results, &east_again).conflicts, vec![west.clone()]);
let mut expected = vec![east.clone(), east_again.clone()];
expected.sort();
assert_eq!(result(&results, &west).conflicts, expected);
assert!(result(&results, ¬e).conflicts.is_empty());
let conflicts = s.active_conflicts();
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].claim_key, "deploy.region");
assert_eq!(conflicts[0].values.len(), 2);
let east_group = conflicts[0]
.values
.iter()
.find(|g| g.value == "us-east-1")
.unwrap();
assert_eq!(east_group.trace_ids.len(), 2);
}
#[test]
fn a_correction_shows_what_it_replaced_and_resolves_the_conflict() {
let mut s = store("recall-supersession");
let old = region(&mut s, "deploy.region", "us-east-1", "runbook");
let rival = region(&mut s, "deploy.region", "eu-west-1", "oncall");
assert_eq!(s.active_conflicts().len(), 1);
let mut fix = Trace::new(
TraceKind::Correction,
"Correction after the March outage: deploy region changed to eu-west-1",
"runbook",
);
fix.claim_key = Some("deploy.region".into());
fix.claim_value = Some("eu-west-1".into());
fix.supersedes = Some(old.clone());
let fix = s.remember(fix).unwrap().0.id;
assert!(s.active_conflicts().is_empty());
let results = s.recall(&RecallQuery::new("deploy region"));
let correction = result(&results, &fix);
assert_eq!(correction.supersedes.as_deref(), Some(old.as_str()));
assert!(correction.superseded_by.is_none());
assert!(correction.conflicts.is_empty());
assert!(result(&results, &rival).conflicts.is_empty());
let mut history = RecallQuery::new("deploy region");
history.include_superseded = true;
let results = s.recall(&history);
let replaced = result(&results, &old);
assert_eq!(replaced.superseded_by.as_deref(), Some(fix.as_str()));
assert!(
replaced.conflicts.is_empty(),
"a superseded claim is not an open conflict"
);
}
#[test]
fn sensitive_traces_are_not_listed_as_conflicts_under_the_default_policy() {
let path = temp_store("sensitive-conflicts");
let permissive = KernelPolicy {
allow_sensitive_persistence: true,
..KernelPolicy::default()
};
let mut s = MemoryStore::open(&path, permissive).unwrap();
let public = region(&mut s, "deploy.region", "us-east-1", "runbook");
let mut private = Trace::new(TraceKind::Belief, "deploy region is secret-site", "ops");
private.claim_key = Some("deploy.region".into());
private.claim_value = Some("secret-site".into());
private.privacy = PrivacyClass::Sensitive;
s.remember(private).unwrap();
drop(s);
let strict = MemoryStore::open(&path, KernelPolicy::default()).unwrap();
assert!(strict.active_conflicts().is_empty());
let results = strict.recall(&RecallQuery::new("deploy region"));
assert!(result(&results, &public).conflicts.is_empty());
}
#[test]
fn near_identical_wording_does_not_fold_a_competing_claim_away() {
let mut s = store("fold-conflicts");
let text = |v: &str| format!("The production deploy region for the billing service is {v}");
let east = claim(
&mut s,
"billing.region",
"us-east-1",
"runbook",
&text("us-east-1"),
);
let west = claim(
&mut s,
"billing.region",
"us-east-2",
"oncall",
&text("us-east-2"),
);
claim(
&mut s,
"billing.region",
"us-east-1",
"ci",
&text("us-east-1"),
);
let results = s.recall(&RecallQuery::new("billing deploy region"));
assert_eq!(results.len(), 2, "{results:#?}");
let east_result = results
.iter()
.find(|r| r.trace.claim_value.as_deref() == Some("us-east-1"))
.unwrap();
assert_eq!(east_result.duplicates, 1, "the agreeing twin folds");
assert!(east_result.conflicts.contains(&west));
let west_result = result(&results, &west);
assert_eq!(west_result.conflicts.len(), 2);
assert!(west_result.conflicts.contains(&east));
}
#[test]
fn a_purged_correction_is_not_named_as_the_successor() {
let mut s = MemoryStore::open(temp_store("purged-successor"), KernelPolicy::default()).unwrap();
let mut old = Trace::new(TraceKind::Belief, "The car color is blue", "owner");
old.claim_key = Some("car.color".into());
old.claim_value = Some("blue".into());
let old = s.remember(old).unwrap().0.id;
let mut fix = Trace::new(TraceKind::Correction, "The car color is red", "owner");
fix.claim_key = Some("car.color".into());
fix.claim_value = Some("red".into());
fix.supersedes = Some(old.clone());
let fix = s.remember(fix).unwrap().0.id;
s.purge(&fix, "entered on the wrong record").unwrap();
let mut history = RecallQuery::new("car color");
history.include_superseded = true;
let results = s.recall(&history);
let replaced = results.iter().find(|r| r.trace.id == old).unwrap();
assert_eq!(replaced.superseded_by, None, "points at a purged trace");
}