use std::path::{Path, PathBuf};
use std::process::Command;
use chrono::{TimeZone, Utc};
use cortex_core::{
Attestor, Event, EventSource, EventType, InMemoryAttestor, KeyLifecycleState, PrincipleId,
TrustTier, SCHEMA_VERSION,
};
use cortex_store::migrate::apply_pending;
use cortex_store::repo::memories::MemoryAcceptanceAudit;
use cortex_store::repo::{
AuthorityRepo, EventRepo, KeyTimelineRecord, MemoryCandidate, MemoryRepo,
PrincipalTimelineRecord, PrincipleCandidateRow, PrincipleRepo,
};
use rusqlite::Connection;
use serde_json::json;
fn cortex_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}
fn run_in(cwd: &Path, args: &[&str]) -> std::process::Output {
Command::new(cortex_bin())
.current_dir(cwd)
.env("XDG_DATA_HOME", cwd.join("xdg"))
.env("HOME", cwd)
.args(args)
.output()
.expect("spawn cortex")
}
fn assert_exit(out: &std::process::Output, expected: i32) {
let code = out.status.code().expect("process exited via signal");
assert_eq!(
code,
expected,
"expected exit {expected}, got {code}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
fn init(tmp: &Path) -> PathBuf {
let out = run_in(tmp, &["init"]);
assert_exit(&out, 0);
let stdout = String::from_utf8_lossy(&out.stdout);
let db_line = stdout
.lines()
.find(|line| line.starts_with("cortex init: db"))
.expect("init stdout includes db path");
let path = db_line
.split_once('=')
.expect("db line has equals")
.1
.trim()
.split_once(" (")
.expect("db line has status suffix")
.0;
PathBuf::from(path)
}
fn at(second: u32) -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, second).unwrap()
}
fn attestation_key_fixture() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("attested-user-event-key.bin")
}
fn signed_run_attestor(path: &Path) -> InMemoryAttestor {
let raw = std::fs::read(path).expect("read attestation key");
let seed: [u8; 32] = raw.as_slice().try_into().expect("key must be 32 bytes");
InMemoryAttestor::from_seed(&seed)
}
fn ensure_source_event(pool: &Connection, second: u32) {
let event_id = "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap();
let repo = EventRepo::new(pool);
if repo
.get_by_id(&event_id)
.expect("query source event")
.is_some()
{
return;
}
repo.append(&Event {
id: event_id,
schema_version: SCHEMA_VERSION,
observed_at: at(second),
recorded_at: at(second),
source: EventSource::Tool {
name: "doctrine-cli-test".into(),
},
event_type: EventType::ToolResult,
trace_id: None,
session_id: Some("doctrine-cli-test".into()),
domain_tags: vec!["doctrine".into(), "test".into()],
payload: json!({"source": "doctrine-cli-test", "second": second}),
payload_hash: format!("payload-source-{second}"),
prev_event_hash: None,
event_hash: format!("event-source-{second}"),
})
.expect("append source event");
}
fn insert_active_memory(
db_path: &Path,
memory_id: &str,
claim: &str,
domains: &[&str],
second: u32,
) {
let pool = Connection::open(db_path).expect("open sqlite db");
apply_pending(&pool).expect("apply migrations");
ensure_source_event(&pool, second);
let repo = MemoryRepo::new(&pool);
let candidate = MemoryCandidate {
id: memory_id.parse().unwrap(),
memory_type: "semantic".into(),
claim: claim.into(),
source_episodes_json: json!([]),
source_events_json: json!(["evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"]),
domains_json: json!(domains),
salience_json: json!({"score": 0.8, "validation": 0.6}),
confidence: 0.9,
authority: "user".into(),
applies_when_json: json!(["querying stored memories"]),
does_not_apply_when_json: json!([]),
created_at: at(second),
updated_at: at(second),
};
repo.insert_candidate(&candidate).expect("insert candidate");
let audit = MemoryAcceptanceAudit {
id: format!("aud_01ARZ3NDEKTSV4RRFFQ69G5F{second:02}")
.parse()
.unwrap(),
actor_json: json!({"kind": "test"}),
reason: "doctrine test active memory".into(),
source_refs_json: json!([memory_id]),
created_at: at(second + 1),
};
repo.accept_candidate(
&memory_id.parse().unwrap(),
at(second + 2),
&audit,
&cortex_store::repo::memories::accept_candidate_policy_decision_test_allow(),
)
.expect("accept candidate");
}
fn insert_fixture_memories(db_path: &Path) -> Vec<String> {
let specs: &[(&str, &str, &[&str], u32)] = &[
(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"Evidence must be inspected before claiming state.",
&["agents"],
4,
),
(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAX",
"Audit reports need direct observed anchors.",
&["audit"],
6,
),
(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAY",
"Candidate output must stay separate from promotion.",
&["agents"],
8,
),
];
specs
.iter()
.map(|&(id, claim, domains, second)| {
insert_active_memory(db_path, id, claim, domains, second);
id.to_string()
})
.collect()
}
fn insert_principle_candidate(db_path: &Path, memory_ids: &[String]) -> PrincipleId {
let pool = Connection::open(db_path).expect("open sqlite db");
apply_pending(&pool).expect("apply migrations");
let repo = PrincipleRepo::new(&pool);
let principle_id: PrincipleId = "prn_01ARZ3NDEKTSV4RRFFQ69G5FAV"
.parse()
.expect("valid principle id");
repo.insert_candidate(
&PrincipleCandidateRow {
id: principle_id,
statement: "Preserve evidence-bound guidance before doctrine promotion.".into(),
status: "candidate".into(),
supporting_memories_json: json!(memory_ids),
contradicting_memories_json: json!([]),
domains_observed_json: json!(["agents", "audit"]),
applies_when_json: json!(["the pattern recurs across domains"]),
does_not_apply_when_json: json!(["support is below threshold"]),
confidence: 0.72,
validation: 0.6,
brightness: 0.7,
created_by_json: json!({
"kind": "test",
"falsification": {
"status": "attempted",
"failed_attempts": [
"counterexample search found no high-risk blocker in the fixture window"
],
"unresolved_high_risk_counterexamples": []
}
}),
created_at: at(10),
updated_at: at(10),
},
&cortex_store::repo::principles::insert_candidate_policy_decision_test_allow(),
)
.expect("insert principle candidate");
principle_id
}
fn seed_operator_authority(db_path: &Path, attestation_key: &Path) {
let attestor = signed_run_attestor(attestation_key);
let pool = Connection::open(db_path).expect("open sqlite db");
apply_pending(&pool).expect("apply migrations");
let repo = AuthorityRepo::new(&pool);
repo.append_principal_state(
&PrincipalTimelineRecord {
principal_id: "operator-principal".into(),
trust_tier: TrustTier::Operator,
effective_at: at(1),
trust_review_due_at: None,
removed_at: None,
audit_ref: None,
},
&cortex_store::repo::authority::principal_state_policy_decision_test_allow(),
)
.expect("append operator trust state");
repo.append_key_state(
&KeyTimelineRecord {
key_id: attestor.key_id().to_string(),
principal_id: "operator-principal".into(),
state: KeyLifecycleState::Active,
effective_at: at(1),
reason: None,
audit_ref: None,
},
&cortex_store::repo::authority::key_state_policy_decision_test_allow(),
)
.expect("append active operator key state");
}
fn promote_principle(tmp: &Path, db_path: &Path) -> String {
let memory_ids = insert_fixture_memories(db_path);
let principle_id = insert_principle_candidate(db_path, &memory_ids);
let attestation_key = attestation_key_fixture();
seed_operator_authority(db_path, &attestation_key);
let out = run_in(
tmp,
&[
"principle",
"promote",
&principle_id.to_string(),
"--force",
"advisory",
"--reason",
"operator reviewed support",
"--attestation",
attestation_key.to_str().expect("attestation key path utf8"),
],
);
assert_exit(&out, 0);
principle_id.to_string()
}
#[test]
fn context_build_with_doctrine_includes_promoted_principles() {
let tmp = tempfile::tempdir().unwrap();
let db_path = init(tmp.path());
promote_principle(tmp.path(), &db_path);
let out = run_in(
tmp.path(),
&[
"context",
"build",
"--task",
"answer with doctrine",
"--include-doctrine",
],
);
assert_exit(&out, 0);
let pack: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("context stdout is JSON");
let doctrine = pack["doctrine"]
.as_array()
.expect("output includes a doctrine array");
assert_eq!(doctrine.len(), 1, "one promoted principle should appear");
let entry = &doctrine[0];
assert_eq!(
entry["claim"], "Preserve evidence-bound guidance before doctrine promotion.",
"doctrine entry should carry the principle claim"
);
assert!(
entry["doctrine_id"].as_str().is_some(),
"doctrine entry should carry a doctrine_id"
);
assert!(
entry["source_principle"].as_str().is_some(),
"doctrine entry should carry source_principle"
);
assert_eq!(entry["force"], "Advisory");
}
#[test]
fn context_build_without_doctrine_flag_excludes_doctrine() {
let tmp = tempfile::tempdir().unwrap();
let db_path = init(tmp.path());
promote_principle(tmp.path(), &db_path);
let out = run_in(
tmp.path(),
&["context", "build", "--task", "answer without doctrine"],
);
assert_exit(&out, 0);
let pack: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("context stdout is JSON");
assert!(
pack.get("doctrine").is_none(),
"doctrine key must be absent when --include-doctrine is not set"
);
}