use agentstategraph::{CommitOptions, Repository};
use agentstategraph_core::{IntentCategory, Object};
use agentstategraph_storage::SqliteStorage;
use agentstategraph_taint::{
QuarantineParams, TaintEffect, TaintError, TaintKind, TaintMetadata, TaintParams,
TaintSeverity, UntaintParams, UnwatchParams, WatchDirection, WatchParams,
};
fn repo() -> Repository {
let r = Repository::new(Box::new(
SqliteStorage::in_memory().expect("in-memory sqlite"),
));
r.init().unwrap();
r
}
fn opts(agent: &str, confidence: Option<f64>) -> CommitOptions {
let mut o = CommitOptions::new(agent, IntentCategory::Refine, "write");
if let Some(c) = confidence {
o = o.with_confidence(c);
}
o
}
fn t_params(name: &str, effect: TaintEffect, agent: &str) -> TaintParams {
TaintParams {
name: name.into(),
effect,
reason: "test".into(),
severity: TaintSeverity::Medium,
expires_at: None,
propagate: true,
metadata: TaintMetadata::new(),
agent_id: agent.into(),
}
}
#[test]
fn taint_then_untaint_round_trip() {
let r = repo();
let id = r
.taint("main", "/x", t_params("t1", TaintEffect::Warn, "ops"))
.unwrap();
let listed = r.list_taints(None, Some(TaintKind::Taint), false).unwrap();
assert!(listed.iter().any(|t| t.id == id && !t.commit_id.is_empty()));
r.untaint(
"main",
"/x",
"t1",
UntaintParams {
reason: "resolved".into(),
proof: Some("commit-xyz".into()),
agent_id: "ops".into(),
},
)
.unwrap();
let active = r.list_taints(None, Some(TaintKind::Taint), false).unwrap();
assert!(active.iter().all(|t| t.id != id));
}
#[test]
fn block_effect_rejects_set() {
let r = repo();
r.taint(
"main",
"/cluster",
t_params("down", TaintEffect::Block, "ops"),
)
.unwrap();
let err = r
.set(
"main",
"/cluster/nodes/a",
&Object::string("hi"),
opts("agent-1", None),
)
.unwrap_err();
assert!(matches!(
err,
agentstategraph::RepoError::Taint {
source: TaintError::Blocked { .. },
..
}
));
}
#[test]
fn review_effect_rejects_low_confidence_accepts_high() {
let r = repo();
r.taint(
"main",
"/cluster",
t_params("rev", TaintEffect::Review, "ops"),
)
.unwrap();
let low = r.set(
"main",
"/cluster/x",
&Object::string("v"),
opts("agent-1", Some(0.5)),
);
assert!(matches!(
low,
Err(agentstategraph::RepoError::Taint {
source: TaintError::InsufficientConfidence { .. },
..
})
));
r.set(
"main",
"/cluster/y",
&Object::string("v"),
opts("agent-1", Some(0.95)),
)
.unwrap();
}
#[test]
fn quarantine_blocks_unauthorized_passes_authorized() {
let r = repo();
r.quarantine(
"main",
"/secrets",
QuarantineParams {
name: "sec".into(),
reason: "audit".into(),
severity: TaintSeverity::High,
authorized_agents: vec!["agent/security".into()],
expires_at: None,
propagate: true,
agent_id: "agent/security".into(),
},
)
.unwrap();
let denied = r.set(
"main",
"/secrets/x",
&Object::string("v"),
opts("agent-1", None),
);
assert!(matches!(
denied,
Err(agentstategraph::RepoError::Taint {
source: TaintError::NotAuthorized { .. },
..
})
));
r.set(
"main",
"/secrets/y",
&Object::string("v"),
opts("agent/security", None),
)
.unwrap();
}
#[test]
fn warn_effect_allows_write() {
let r = repo();
r.taint(
"main",
"/cluster",
t_params("deg", TaintEffect::Warn, "ops"),
)
.unwrap();
r.set(
"main",
"/cluster/x",
&Object::string("v"),
opts("agent-1", None),
)
.unwrap();
}
#[test]
fn non_propagating_taint_does_not_block_descendants() {
let r = repo();
let mut params = t_params("leaf", TaintEffect::Block, "ops");
params.propagate = false;
r.taint("main", "/cluster", params).unwrap();
r.set(
"main",
"/cluster/child",
&Object::string("v"),
opts("agent-1", None),
)
.unwrap();
}
#[test]
fn expired_taint_is_ignored_by_hook() {
let r = repo();
let mut params = t_params("old", TaintEffect::Block, "ops");
params.expires_at = Some(chrono::Utc::now() - chrono::Duration::seconds(10));
r.taint("main", "/cluster", params).unwrap();
r.set(
"main",
"/cluster/x",
&Object::string("v"),
opts("agent-1", None),
)
.unwrap();
}
#[test]
fn check_taint_surfaces_full_status() {
let r = repo();
r.taint("main", "/x", t_params("a", TaintEffect::Warn, "ops"))
.unwrap();
r.quarantine(
"main",
"/x",
QuarantineParams {
name: "q".into(),
reason: "audit".into(),
severity: TaintSeverity::High,
authorized_agents: vec!["agent/security".into()],
expires_at: None,
propagate: true,
agent_id: "agent/security".into(),
},
)
.unwrap();
let c = r.check_taint("/x/inner", "agent-1", 1.0).unwrap();
assert!(c.tainted && c.quarantined);
assert!(!c.can_write);
assert_eq!(c.authorized_agents, vec!["agent/security".to_string()]);
}
#[test]
fn watch_creates_and_lists() {
let r = repo();
let id = r
.watch_path(
"main",
"/cluster/disk",
WatchParams {
name: "disk-80".into(),
reason: "perf".into(),
metric: Some("disk_used_pct".into()),
threshold: Some(80.0),
direction: WatchDirection::Above,
check_interval_secs: Some(60),
expires_at: None,
severity: TaintSeverity::Low,
propagate: true,
agent_id: "ops".into(),
},
)
.unwrap();
let watches = r.list_taints(None, Some(TaintKind::Watch), false).unwrap();
assert!(watches.iter().any(|w| w.id == id));
r.set(
"main",
"/cluster/disk",
&Object::string("50"),
opts("agent-1", None),
)
.unwrap();
r.unwatch(
"main",
"/cluster/disk",
"disk-80",
UnwatchParams {
reason: Some("no longer needed".into()),
agent_id: "ops".into(),
},
)
.unwrap();
let active = r.list_taints(None, Some(TaintKind::Watch), false).unwrap();
assert!(active.iter().all(|w| w.id != id));
}
#[test]
fn taint_lifecycle_intents_bypass_hook() {
let r = repo();
r.taint(
"main",
"/cluster",
t_params("block", TaintEffect::Block, "ops"),
)
.unwrap();
assert!(
r.set(
"main",
"/cluster/anything",
&Object::string("x"),
opts("a", None)
)
.is_err()
);
r.untaint(
"main",
"/cluster",
"block",
UntaintParams {
reason: "fixed".into(),
proof: None,
agent_id: "ops".into(),
},
)
.unwrap();
}
#[test]
fn taint_commit_category_is_native() {
let r = repo();
r.taint("main", "/x", t_params("t", TaintEffect::Warn, "ops"))
.unwrap();
let listed = r.list_taints(None, None, false).unwrap();
let taint = listed.first().expect("taint present");
assert!(!taint.commit_id.is_empty());
}
#[test]
fn list_taints_filters_by_kind_and_prefix() {
let r = repo();
r.taint("main", "/a", t_params("a", TaintEffect::Warn, "ops"))
.unwrap();
r.watch_path(
"main",
"/b",
WatchParams {
name: "w".into(),
reason: "perf".into(),
metric: None,
threshold: None,
direction: WatchDirection::Above,
check_interval_secs: None,
expires_at: None,
severity: TaintSeverity::Low,
propagate: true,
agent_id: "ops".into(),
},
)
.unwrap();
let taints = r.list_taints(None, Some(TaintKind::Taint), false).unwrap();
assert!(taints.iter().all(|t| t.kind == TaintKind::Taint));
let a_only = r.list_taints(Some("/a"), None, false).unwrap();
assert!(a_only.iter().all(|t| t.path.starts_with("/a")));
}