use super::*;
#[test]
fn condition_grammar_parses_and_rejects() {
assert!(matches!(parse_condition("stop").unwrap(), Cond::Stop));
assert!(matches!(parse_condition("hitl").unwrap(), Cond::Hitl));
assert!(matches!(parse_condition("auq").unwrap(), Cond::Auq));
assert!(matches!(
parse_condition("notification").unwrap(),
Cond::Notification(None)
));
assert!(matches!(
parse_condition("notification:done").unwrap(),
Cond::Notification(Some(_))
));
match parse_condition("tool:Read:handover.md").unwrap() {
Cond::Tool { name, input_re } => {
assert_eq!(name, "Read");
assert!(input_re.is_some());
}
other => panic!("{other:?}"),
}
match parse_condition("write:plans/.*\\.md:DONE").unwrap() {
Cond::Write { line_re, .. } => assert!(line_re.is_some()),
other => panic!("{other:?}"),
}
assert!(matches!(
parse_condition("verdict:stale-dead").unwrap(),
Cond::VerdictIs(Verdict::StaleDead)
));
for bad in ["", "stopp", "tool:", "write:", "verdict:done", "tool"] {
assert!(parse_condition(bad).is_err(), "must reject `{bad}`");
}
}
#[test]
fn verdict_slugs_are_the_closed_wire_set() {
let all = [
(Verdict::Running, "running"),
(Verdict::WaitingChildren, "waiting-children"),
(Verdict::WaitingHitl, "waiting-hitl"),
(Verdict::IdleEot, "idle-eot"),
(Verdict::StaleDead, "stale-dead"),
(Verdict::Unknown, "unknown"),
];
for (v, s) in all {
assert_eq!(v.slug(), s);
assert!(matches!(
parse_condition(&format!("verdict:{s}")).unwrap(),
Cond::VerdictIs(x) if x == v
));
}
}
#[test]
fn condition_parse_error_forms_name_the_problem() {
for (tok, family) in [
("notification:(", "notification"),
("tool:Read:(", "tool"),
("write:(", "write"),
("write:ok:(", "write"),
] {
let e = parse_condition(tok).unwrap_err().to_string();
assert!(e.contains(family) && e.contains("bad regex"), "{tok}: {e}");
}
let e = parse_condition("notifications").unwrap_err().to_string();
assert!(e.contains("did you mean"), "{e}");
let e = parse_condition("growth").unwrap_err().to_string();
assert!(e.contains("stop | hitl | auq"), "{e}");
}
#[test]
fn notification_condition_scopes_to_main_and_matches_payload() {
let pulse: crate::model::Record = serde_json::from_str(
r#"{"type":"user","message":{"role":"user","content":"<task-notification>\n<task-id>b497m4ncp</task-id>\n<status>completed</status>\n<summary>Background command \"build venvs\" completed (exit code 0)</summary>\n</task-notification>"}}"#,
)
.unwrap();
let any = parse_condition("notification").unwrap();
assert!(record_matches(&any, &pulse, true));
assert!(
!record_matches(&any, &pulse, false),
"notifications persist in MAIN only; a child line never fires the condition"
);
let hit = parse_condition("notification:build venvs").unwrap();
assert!(record_matches(&hit, &pulse, true));
let miss = parse_condition("notification:some other job").unwrap();
assert!(!record_matches(&miss, &pulse, true));
let plain: crate::model::Record =
serde_json::from_str(r#"{"type":"user","message":{"role":"user","content":"hello"}}"#)
.unwrap();
assert!(!record_matches(&any, &plain, true));
}
#[test]
fn auq_condition_matches_sidecar_pending_and_native_ask() {
let auq = parse_condition("auq").unwrap();
let pending: crate::model::Record = serde_json::from_str(
r#"{"csift":"elicitation-marker-v1","csiftPhase":"pending","csiftKind":"AskUserQuestion","csiftKey":"toolu_1","type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"AskUserQuestion","input":{}}]}}"#,
)
.unwrap();
assert!(record_matches(&auq, &pending, true));
let resolved: crate::model::Record = serde_json::from_str(
r#"{"csift":"elicitation-marker-v1","csiftPhase":"resolved","csiftKind":"AskUserQuestion","csiftKey":"toolu_1","type":"csift-elicitation-resolved"}"#,
)
.unwrap();
assert!(!record_matches(&auq, &resolved, true));
let native: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"AskUserQuestion","input":{"questions":[]}}]}}"#,
)
.unwrap();
assert!(record_matches(&auq, &native, true));
let text_only: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"no ask"}]}}"#,
)
.unwrap();
assert!(!record_matches(&auq, &text_only, true));
}
#[test]
fn tool_and_write_condition_edge_arms() {
let bare: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read"}]}}"#,
)
.unwrap();
assert!(record_matches(
&parse_condition("tool:Read").unwrap(),
&bare,
true
));
assert!(!record_matches(
&parse_condition("tool:Read:handover").unwrap(),
&bare,
true
));
let nb: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"NotebookEdit","input":{"notebook_path":"/p/lab.ipynb","new_source":"x = 1"}}]}}"#,
)
.unwrap();
assert!(record_matches(
&parse_condition("write:lab\\.ipynb").unwrap(),
&nb,
true
));
assert!(!record_matches(
&parse_condition("write:other\\.md:x = 1").unwrap(),
&nb,
true
));
let read: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t3","name":"Read","input":{"file_path":"/p/lab.ipynb"}}]}}"#,
)
.unwrap();
assert!(!record_matches(
&parse_condition("write:lab\\.ipynb").unwrap(),
&read,
true
));
}
#[test]
fn verdict_matches_covers_hitl_and_rejects_record_class() {
let hitl = parse_condition("hitl").unwrap();
assert!(verdict_matches(&hitl, Verdict::WaitingHitl));
assert!(!verdict_matches(&hitl, Verdict::IdleEot));
assert!(verdict_matches(
&parse_condition("verdict:running").unwrap(),
Verdict::Running
));
assert!(!verdict_matches(
&parse_condition("auq").unwrap(),
Verdict::Running
));
}
#[test]
fn record_conditions_match_the_right_events() {
let read: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{"file_path":"/p/handover.md"}}]}}"#,
)
.unwrap();
let tool = parse_condition("tool:Read:handover").unwrap();
assert!(record_matches(&tool, &read, true));
let other = parse_condition("tool:Write").unwrap();
assert!(!record_matches(&other, &read, true));
let write: crate::model::Record = serde_json::from_str(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"Write","input":{"file_path":"/p/notes/final.md","content":"alpha\nDONE beacon\n"}}]}}"#,
)
.unwrap();
let w = parse_condition("write:final\\.md:DONE").unwrap();
assert!(record_matches(&w, &write, true));
let w_miss = parse_condition("write:final\\.md:ABSENT").unwrap();
assert!(!record_matches(&w_miss, &write, true));
assert!(!record_matches(
&parse_condition("stop").unwrap(),
&read,
true
));
assert!(verdict_matches(
&parse_condition("stop").unwrap(),
Verdict::StaleDead
));
assert!(!verdict_matches(
&parse_condition("stop").unwrap(),
Verdict::Running
));
}
#[test]
fn notification_regex_matches_the_synthesized_label_alone() {
let pulse: crate::model::Record = serde_json::from_str(
r#"{"type":"user","message":{"role":"user","content":"<task-notification>\n<task-id>b497m4ncp</task-id>\n<status>completed</status>\n<summary>Background command \"build venvs\" completed (exit code 0)</summary>\n</task-notification>"}}"#,
)
.unwrap();
let label_only = parse_condition("notification:\\[background-command b497m4ncp").unwrap();
assert!(record_matches(&label_only, &pulse, true));
}