use tower_rules::*;
fn assert_round_trip<T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
value: &T,
) {
let json = serde_json::to_string(value).expect("serialize");
let restored: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(value, &restored, "round-trip mismatch for {json}");
}
#[test]
fn predicate_event_match_minimal() {
let p = Predicate::EventMatch {
scope: ScopeFilter::Any,
level: None,
target: None,
fields: vec![],
rate: None,
};
assert_round_trip(&p);
}
#[test]
fn predicate_event_match_full() {
let p = Predicate::EventMatch {
scope: ScopeFilter::Service {
ident: Some(MeshIdent("yubaba.local".into())),
},
level: Some(LevelFilter::Error),
target: Some(StringFilter::Glob { pattern: "raft.*".into() }),
fields: vec![FieldFilter {
path: "error.code".into(),
op: FieldOp::Eq { value: serde_json::json!("quorum_lost") },
}],
rate: Some(RatePredicate {
min_count: 3,
window_ms: 5_000,
}),
};
assert_round_trip(&p);
}
#[test]
fn predicate_scryer_health() {
let p = Predicate::ScryerHealth {
signal: HealthSignal::YubabaRaftQuorumLost,
};
assert_round_trip(&p);
}
#[test]
fn predicate_scryer_health_all_signals() {
for signal in [
HealthSignal::IngestionLag,
HealthSignal::RingOverflow,
HealthSignal::FederationTimeout,
HealthSignal::YubabaRaftQuorumLost,
] {
assert_round_trip(&Predicate::ScryerHealth { signal });
}
}
#[test]
fn predicate_compound_and() {
let p = Predicate::Compound {
op: CompoundOp::And,
children: vec![
Predicate::ScryerHealth { signal: HealthSignal::YubabaRaftQuorumLost },
Predicate::EventMatch {
scope: ScopeFilter::Any,
level: Some(LevelFilter::Warn),
target: None,
fields: vec![],
rate: None,
},
],
};
assert_round_trip(&p);
}
#[test]
fn predicate_compound_not() {
let p = Predicate::Compound {
op: CompoundOp::Not,
children: vec![Predicate::EventMatch {
scope: ScopeFilter::Forge { id: None },
level: None,
target: Some(StringFilter::Exact { value: "forge.start".into() }),
fields: vec![],
rate: None,
}],
};
assert_round_trip(&p);
}
#[test]
fn trigger_agent_dispatch() {
let t = Trigger::AgentDispatch {
agent_class: AgentClassRef("gnome/fixer".into()),
prompt_template: PromptTemplate(
"Yubaba lost quorum. Context: {{event}}. Investigate and remediate.".into(),
),
placement: TaskPlacement::new(TaskLocation::Local, TaskRuntime::Native),
};
assert_round_trip(&t);
}
#[test]
fn trigger_notification_desktop() {
let t = Trigger::Notification {
channels: vec![NotificationChannel::DesktopBadge],
severity: Severity::Critical,
};
assert_round_trip(&t);
}
#[test]
fn trigger_notification_webhook() {
let t = Trigger::Notification {
channels: vec![
NotificationChannel::DesktopBadge,
NotificationChannel::Webhook { url: "https://hooks.example.com/alert".into() },
],
severity: Severity::Warning,
};
assert_round_trip(&t);
}
#[test]
fn trigger_yubaba_action_restart() {
let t = Trigger::YubabaAction {
kind: YubabaActionKind::RestartWorkload,
target: MeshIdent("noisetable-api.pdx".into()),
};
assert_round_trip(&t);
}
#[test]
fn trigger_yubaba_action_scale() {
let t = Trigger::YubabaAction {
kind: YubabaActionKind::Scale { replicas: 3 },
target: MeshIdent("noisetable-worker.pdx".into()),
};
assert_round_trip(&t);
}
#[test]
fn federation_local_only() {
assert_round_trip(&FederationPolicy::LocalOnly);
}
#[test]
fn federation_mesh_wide() {
assert_round_trip(&FederationPolicy::MeshWide);
}
#[test]
fn federation_mirror_set() {
let f = FederationPolicy::MirrorSet {
mirrors: vec!["prod-pdx".into(), "prod-fra".into()],
};
assert_round_trip(&f);
}
#[test]
fn tower_rule_yubaba_quorum_loss() {
let rule = TowerRule {
schema_version: SchemaVersion::V1,
id: RuleId("yubaba-quorum-loss".into()),
name: "Yubaba raft quorum loss".into(),
predicate: Predicate::ScryerHealth {
signal: HealthSignal::YubabaRaftQuorumLost,
},
trigger: Trigger::Notification {
channels: vec![NotificationChannel::DesktopBadge],
severity: Severity::Critical,
},
debounce_ms: Some(30_000),
federation: FederationPolicy::LocalOnly,
enabled: true,
};
assert_round_trip(&rule);
}
#[test]
fn tower_rule_agent_dispatch_on_error_burst() {
let rule = TowerRule {
schema_version: SchemaVersion::V1,
id: RuleId("db-error-burst-repair".into()),
name: "Database error burst → gnome repair".into(),
predicate: Predicate::EventMatch {
scope: ScopeFilter::Service {
ident: Some(MeshIdent("noisetable-db.pdx".into())),
},
level: Some(LevelFilter::Error),
target: Some(StringFilter::Glob { pattern: "database.*".into() }),
fields: vec![],
rate: Some(RatePredicate { min_count: 10, window_ms: 60_000 }),
},
trigger: Trigger::AgentDispatch {
agent_class: AgentClassRef("gnome/db-repair".into()),
prompt_template: PromptTemplate(
"Database error burst detected. Events: {{events}}. Diagnose and repair.".into(),
),
placement: TaskPlacement::new(
TaskLocation::RemoteAny { tier: TierTag("infra".into()) },
TaskRuntime::Container,
),
},
debounce_ms: Some(300_000),
federation: FederationPolicy::MirrorSet {
mirrors: vec!["prod-pdx".into()],
},
enabled: false,
};
assert_round_trip(&rule);
}
#[test]
fn task_runtime_round_trip() {
assert_round_trip(&TaskRuntime::Native);
assert_round_trip(&TaskRuntime::Container);
}
#[test]
fn task_location_local_round_trip() {
assert_round_trip(&TaskLocation::Local);
}
#[test]
fn task_location_remote_any_round_trip() {
assert_round_trip(&TaskLocation::RemoteAny { tier: TierTag("infra".into()) });
}
#[test]
fn task_placement_four_quadrants_round_trip() {
for location in [TaskLocation::Local, TaskLocation::RemoteAny { tier: TierTag("infra".into()) }] {
for runtime in [TaskRuntime::Native, TaskRuntime::Container] {
assert_round_trip(&TaskPlacement::new(location.clone(), runtime));
}
}
}
#[test]
fn task_placement_local_container_wire_format() {
let placement = TaskPlacement::new(TaskLocation::Local, TaskRuntime::Container);
let json = serde_json::to_value(&placement).unwrap();
assert_eq!(
json,
serde_json::json!({
"location": { "kind": "local" },
"runtime": "container",
})
);
}
#[test]
fn task_placement_remote_any_native_wire_format() {
let placement = TaskPlacement::new(
TaskLocation::RemoteAny { tier: TierTag("infra".into()) },
TaskRuntime::Native,
);
let json = serde_json::to_value(&placement).unwrap();
assert_eq!(
json,
serde_json::json!({
"location": { "kind": "remote_any", "tier": "infra" },
"runtime": "native",
})
);
}
#[test]
fn string_filter_all_variants() {
assert_round_trip(&StringFilter::Glob { pattern: "err.*".into() });
assert_round_trip(&StringFilter::Regex { pattern: r"^err\..+".into() });
assert_round_trip(&StringFilter::Exact { value: "err.timeout".into() });
}
#[test]
fn field_op_all_variants() {
assert_round_trip(&FieldFilter {
path: "code".into(),
op: FieldOp::Eq { value: serde_json::json!(42) },
});
assert_round_trip(&FieldFilter {
path: "msg".into(),
op: FieldOp::Contains { substring: "timeout".into() },
});
assert_round_trip(&FieldFilter {
path: "target".into(),
op: FieldOp::Matches {
filter: StringFilter::Glob { pattern: "db.*".into() },
},
});
assert_round_trip(&FieldFilter {
path: "error".into(),
op: FieldOp::Exists,
});
}