use decapod::core::store::{Store, StoreKind};
use decapod::plugins::federation::{
FederationCli, FederationCommand, OutputFormat, add_edge, add_node, add_source_to_node,
edit_node, find_node_by_source, initialize_federation_db, rebuild_from_events,
run_federation_cli, supersede_node, transition_node_status, validate_federation,
};
use std::fs;
use tempfile::tempdir;
fn test_store() -> (tempfile::TempDir, Store) {
let tmp = tempdir().unwrap();
let root = tmp.path().to_path_buf();
initialize_federation_db(&root).unwrap();
let store = Store {
kind: StoreKind::Repo,
root,
};
(tmp, store)
}
fn build_derived(store: &Store) {
run_federation_cli(
store,
FederationCli {
format: OutputFormat::Json,
command: FederationCommand::IndexBuild,
},
)
.unwrap();
run_federation_cli(
store,
FederationCli {
format: OutputFormat::Json,
command: FederationCommand::GraphExport,
},
)
.unwrap();
}
#[test]
fn test_add_and_list_node() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"Test lesson",
"lesson",
"notable",
"agent_inferred",
"Learned something important.",
"",
"ops",
"repo",
None,
"test-agent",
)
.unwrap();
assert!(node.id.starts_with("F_"));
assert_eq!(node.node_type, "lesson");
assert_eq!(node.status, "active");
assert_eq!(node.priority, "notable");
assert_eq!(node.title, "Test lesson");
assert_eq!(node.actor, "test-agent");
}
#[test]
fn test_provenance_required_for_critical() {
let (_tmp, store) = test_store();
let result = add_node(
&store,
"Bad decision",
"decision",
"critical",
"agent_inferred",
"",
"", "",
"repo",
None,
"test",
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Provenance required"));
let result = add_node(
&store,
"Bad commitment",
"commitment",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
);
assert!(result.is_err());
let node = add_node(
&store,
"Good decision",
"decision",
"critical",
"agent_inferred",
"Chose X over Y",
"commit:abcdef01",
"",
"repo",
None,
"test",
)
.unwrap();
assert_eq!(node.node_type, "decision");
assert_eq!(node.sources.as_ref().unwrap().len(), 1);
}
#[test]
fn test_invalid_provenance_rejected() {
let (_tmp, store) = test_store();
let result = add_node(
&store,
"Bad source",
"lesson",
"critical",
"agent_inferred",
"",
"not-a-valid-source",
"",
"repo",
None,
"test",
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid provenance")
);
}
#[test]
fn test_edit_non_critical_node() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"Original title",
"lesson",
"notable",
"agent_inferred",
"Original body",
"",
"",
"repo",
None,
"test",
)
.unwrap();
edit_node(&store, &node.id, Some("New title"), None, None, None).unwrap();
}
#[test]
fn test_edit_critical_node_rejected() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"A decision",
"decision",
"critical",
"agent_inferred",
"",
"file:README.md",
"",
"repo",
None,
"test",
)
.unwrap();
let result = edit_node(&store, &node.id, Some("Changed"), None, None, None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Cannot edit critical")
);
}
#[test]
fn test_supersede_lifecycle() {
let (_tmp, store) = test_store();
let old = add_node(
&store,
"Old decision",
"decision",
"critical",
"agent_inferred",
"",
"file:old.rs",
"",
"repo",
None,
"test",
)
.unwrap();
let new = add_node(
&store,
"New decision",
"decision",
"critical",
"agent_inferred",
"",
"file:new.rs",
"",
"repo",
None,
"test",
)
.unwrap();
supersede_node(&store, &old.id, &new.id, "Requirements changed").unwrap();
build_derived(&store);
let results = validate_federation(&store.root).unwrap();
for (gate, passed, _msg) in &results {
assert!(passed, "Gate {gate} failed");
}
}
#[test]
fn test_status_transition_only_from_active() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"A lesson",
"lesson",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
transition_node_status(&store, &node.id, "deprecated", "node.deprecate", "outdated").unwrap();
let result = transition_node_status(&store, &node.id, "disputed", "node.dispute", "also bad");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Only active nodes")
);
}
#[test]
fn test_edge_operations() {
let (_tmp, store) = test_store();
let a = add_node(
&store,
"Node A",
"project",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let b = add_node(
&store,
"Node B",
"lesson",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let edge_id = add_edge(&store, &a.id, &b.id, "relates_to").unwrap();
assert!(edge_id.starts_with("FE_"));
}
#[test]
fn test_invalid_edge_type_rejected() {
let (_tmp, store) = test_store();
let a = add_node(
&store,
"Node A",
"project",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let b = add_node(
&store,
"Node B",
"lesson",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let result = add_edge(&store, &a.id, &b.id, "bogus_type");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid edge_type")
);
}
#[test]
fn test_rebuild_determinism() {
let (_tmp, store) = test_store();
let n1 = add_node(
&store,
"Decision 1",
"decision",
"critical",
"human_confirmed",
"Body 1",
"file:a.rs",
"tag1",
"repo",
None,
"test",
)
.unwrap();
let n2 = add_node(
&store,
"Lesson 1",
"lesson",
"notable",
"agent_inferred",
"Body 2",
"",
"",
"repo",
None,
"test",
)
.unwrap();
add_edge(&store, &n1.id, &n2.id, "relates_to").unwrap();
transition_node_status(&store, &n2.id, "deprecated", "node.deprecate", "old").unwrap();
let count = rebuild_from_events(&store.root).unwrap();
assert!(count > 0);
build_derived(&store);
let results = validate_federation(&store.root).unwrap();
for (gate, passed, msg) in &results {
assert!(passed, "Gate {gate} failed: {msg}");
}
}
#[test]
fn test_add_source_to_node() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"A lesson",
"lesson",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let src_id = add_source_to_node(&store, &node.id, "file:README.md").unwrap();
assert!(src_id.starts_with("FS_"));
let src_id2 = add_source_to_node(&store, &node.id, "commit:abc123").unwrap();
assert!(src_id2.starts_with("FS_"));
let result = add_source_to_node(&store, &node.id, "not-valid");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid provenance")
);
let result = add_source_to_node(&store, "F_nonexistent", "file:foo.rs");
assert!(result.is_err());
}
#[test]
fn test_add_source_survives_rebuild() {
let (_tmp, store) = test_store();
let node = add_node(
&store,
"Decision X",
"decision",
"critical",
"agent_inferred",
"body",
"file:initial.rs",
"",
"repo",
None,
"test",
)
.unwrap();
add_source_to_node(&store, &node.id, "commit:deadbeef").unwrap();
let count = rebuild_from_events(&store.root).unwrap();
assert!(count > 0);
build_derived(&store);
let results = validate_federation(&store.root).unwrap();
for (gate, passed, msg) in &results {
assert!(passed, "Gate {gate} failed: {msg}");
}
}
#[test]
fn test_init_idempotent() {
let tmp = tempdir().unwrap();
let root = tmp.path().to_path_buf();
initialize_federation_db(&root).unwrap();
initialize_federation_db(&root).unwrap();
let store = Store {
kind: StoreKind::Repo,
root,
};
let node = add_node(
&store,
"After re-init",
"lesson",
"notable",
"agent_inferred",
"",
"",
"",
"repo",
None,
"test",
)
.unwrap();
assert!(node.id.starts_with("F_"));
}
#[test]
fn test_rebuild_determinism_gate_passes() {
let (_tmp, store) = test_store();
let n1 = add_node(
&store,
"Decision A",
"decision",
"critical",
"human_confirmed",
"Body",
"file:a.rs",
"arch",
"repo",
None,
"test",
)
.unwrap();
let n2 = add_node(
&store,
"Lesson B",
"lesson",
"notable",
"agent_inferred",
"Body 2",
"",
"",
"repo",
None,
"test",
)
.unwrap();
add_edge(&store, &n1.id, &n2.id, "relates_to").unwrap();
add_source_to_node(&store, &n2.id, "file:b.rs").unwrap();
transition_node_status(&store, &n2.id, "deprecated", "node.deprecate", "stale").unwrap();
build_derived(&store);
let results = validate_federation(&store.root).unwrap();
let determinism_gate = results
.iter()
.find(|(name, _, _)| name == "federation.rebuild_determinism");
assert!(
determinism_gate.is_some(),
"rebuild_determinism gate should exist"
);
let (_, passed, msg) = determinism_gate.unwrap();
assert!(passed, "rebuild_determinism gate failed: {msg}");
}
#[test]
fn test_derived_artifacts_build_and_validate() {
let (_tmp, store) = test_store();
let a = add_node(
&store,
"Project A",
"project",
"notable",
"agent_inferred",
"body",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let b = add_node(
&store,
"Lesson B",
"lesson",
"notable",
"agent_inferred",
"body",
"",
"",
"repo",
None,
"test",
)
.unwrap();
add_edge(&store, &a.id, &b.id, "relates_to").unwrap();
build_derived(&store);
let index_path = store.root.join("federation/_index.md");
let graph_path = store.root.join("federation/_graph.json");
assert!(index_path.exists());
assert!(graph_path.exists());
assert!(
fs::read_to_string(index_path)
.unwrap()
.contains("Federation Vault Index")
);
let results = validate_federation(&store.root).unwrap();
let index_gate = results
.iter()
.find(|(name, _, _)| name == "federation.derived_index_fresh")
.unwrap();
let graph_gate = results
.iter()
.find(|(name, _, _)| name == "federation.derived_graph_fresh")
.unwrap();
assert!(index_gate.1, "{}", index_gate.2);
assert!(graph_gate.1, "{}", graph_gate.2);
}
#[test]
fn test_derived_freshness_detects_drift_after_write() {
let (_tmp, store) = test_store();
add_node(
&store,
"Before drift",
"lesson",
"notable",
"agent_inferred",
"body",
"",
"",
"repo",
None,
"test",
)
.unwrap();
build_derived(&store);
add_node(
&store,
"After drift",
"lesson",
"notable",
"agent_inferred",
"body2",
"",
"",
"repo",
None,
"test",
)
.unwrap();
let results = validate_federation(&store.root).unwrap();
let index_gate = results
.iter()
.find(|(name, _, _)| name == "federation.derived_index_fresh")
.unwrap();
let graph_gate = results
.iter()
.find(|(name, _, _)| name == "federation.derived_graph_fresh")
.unwrap();
assert!(!index_gate.1, "index freshness should fail after mutation");
assert!(!graph_gate.1, "graph freshness should fail after mutation");
}
#[test]
fn test_validate_clean_store() {
let (_tmp, store) = test_store();
let results = validate_federation(&store.root).unwrap();
for (gate, passed, _msg) in &results {
assert!(passed, "Gate {gate} failed on empty store");
}
}
#[test]
fn test_find_node_by_source() {
let (_tmp, store) = test_store();
let _node = add_node(
&store,
"Task Intent",
"commitment",
"notable",
"agent_inferred",
"Test task created",
"event:R01KHG4QFQ6ZQAN2F3SR6XC5NAZ",
"todo",
"repo",
None,
"decapod",
)
.unwrap();
let found = find_node_by_source(&store, "event:R01KHG4QFQ6ZQAN2F3SR6XC5NAZ").unwrap();
assert!(found.is_some());
assert!(found.unwrap().starts_with("F_"));
let found = find_node_by_source(&store, "event:NONEXISTENT").unwrap();
assert!(found.is_none());
}
#[test]
fn test_intent_proof_chain() {
let (_tmp, store) = test_store();
let intent = add_node(
&store,
"Task: Fix bug",
"commitment",
"notable",
"agent_inferred",
"Task created with priority high",
"event:R01KHG4QFQ6ZQAN2F3SR6XC5NA",
"bugfix",
"repo",
None,
"decapod",
)
.unwrap();
let proof = add_node(
&store,
"Proof: Task completed",
"lesson",
"notable",
"agent_inferred",
"Task marked as done",
"",
"proof,completion",
"repo",
None,
"decapod",
)
.unwrap();
add_edge(&store, &intent.id, &proof.id, "depends_on").unwrap();
let found_intent = find_node_by_source(&store, "event:R01KHG4QFQ6ZQAN2F3SR6XC5NA").unwrap();
assert!(found_intent.is_some());
}