use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use cortex_core::MemoryId;
use cortex_llm::{blake3_hex, LlmAdapter, LlmError, LlmRequest, LlmResponse};
use cortex_reflect::{
extract_candidates, AcceptedMemory, PrincipleExtractionWindow,
DEFAULT_PRINCIPLE_EXTRACTION_MODEL,
};
#[derive(Debug)]
struct FixedPrincipleAdapter {
output: String,
calls: AtomicUsize,
}
impl FixedPrincipleAdapter {
fn new(output: String) -> Self {
Self {
output,
calls: AtomicUsize::new(0),
}
}
fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl LlmAdapter for FixedPrincipleAdapter {
fn adapter_id(&self) -> &'static str {
"fixed-principles"
}
async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
self.calls.fetch_add(1, Ordering::SeqCst);
assert_eq!(req.model, DEFAULT_PRINCIPLE_EXTRACTION_MODEL);
Ok(LlmResponse {
text: self.output.clone(),
parsed_json: None,
model: req.model,
usage: None,
raw_hash: blake3_hex(self.output.as_bytes()),
})
}
}
fn mem(id: &str, claim: &str, domains: &[&str]) -> AcceptedMemory {
AcceptedMemory {
id: id.parse::<MemoryId>().expect("valid memory id"),
claim: claim.to_string(),
domains: domains.iter().map(|domain| (*domain).to_string()).collect(),
applies_when: vec!["cross-domain behavior recurs".to_string()],
does_not_apply_when: vec!["support is isolated".to_string()],
}
}
#[tokio::test]
async fn principles_two_memories_in_one_domain_returns_empty_without_adapter_call() {
let window = PrincipleExtractionWindow::new(vec![
mem(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"Parser failures need typed evidence.",
&["agents"],
),
mem(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"Reflection output should be validated before use.",
&["agents"],
),
]);
let adapter = FixedPrincipleAdapter::new(r#"{"candidate_principles":[]}"#.to_string());
let candidates = extract_candidates(window, &adapter)
.await
.expect("threshold miss is not an error");
assert!(candidates.is_empty());
assert_eq!(adapter.calls(), 0);
}
#[tokio::test]
async fn principles_three_memories_across_two_domains_parse_candidate_output() {
let ids = [
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAX",
];
let window = PrincipleExtractionWindow::new(vec![
mem(
ids[0],
"Evidence must be inspected before claiming state.",
&["agents"],
),
mem(
ids[1],
"Audit reports need direct observed anchors.",
&["audit"],
),
mem(
ids[2],
"Candidate output must stay separate from promotion.",
&["agents"],
),
]);
let output = format!(
r#"{{
"candidate_principles": [
{{
"statement": "Only promote durable guidance after evidence-bound validation.",
"supporting_memory_ids": ["{id0}", "{id1}", "{id2}"],
"contradicting_memory_ids": [],
"domains_observed": ["agents", "audit"],
"applies_when": ["turning repeated memories into reusable guidance"],
"does_not_apply_when": ["fewer than three accepted memories support it"],
"alternative_interpretations": ["This may be a local workflow convention."],
"confidence": 0.72,
"overgeneralisation_risk": 0.24
}}
]
}}"#,
id0 = ids[0],
id1 = ids[1],
id2 = ids[2],
);
let adapter = FixedPrincipleAdapter::new(output);
let candidates = extract_candidates(window, &adapter)
.await
.expect("valid principle candidate batch parses");
assert_eq!(adapter.calls(), 1);
assert_eq!(candidates.len(), 1);
assert_eq!(
candidates[0].statement,
"Only promote durable guidance after evidence-bound validation."
);
assert_eq!(candidates[0].supporting_memory_ids.len(), 3);
}
#[tokio::test]
async fn principles_adapter_output_below_support_threshold_is_filtered() {
let ids = [
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAX",
];
let window = PrincipleExtractionWindow::new(vec![
mem(ids[0], "First accepted memory.", &["agents"]),
mem(ids[1], "Second accepted memory.", &["audit"]),
mem(ids[2], "Third accepted memory.", &["security"]),
]);
let output = format!(
r#"{{
"candidate_principles": [
{{
"statement": "Unsupported proposal should be rejected.",
"supporting_memory_ids": ["{id0}", "{id1}"],
"contradicting_memory_ids": [],
"domains_observed": ["agents", "audit"],
"applies_when": ["model under-cites support"],
"does_not_apply_when": ["thresholds are satisfied"],
"alternative_interpretations": [],
"confidence": 0.8,
"overgeneralisation_risk": 0.1
}}
]
}}"#,
id0 = ids[0],
id1 = ids[1],
);
let adapter = FixedPrincipleAdapter::new(output);
let candidates = extract_candidates(window, &adapter)
.await
.expect("valid batch parses before deterministic filtering");
assert!(candidates.is_empty());
}