use std::collections::BTreeSet;
use serde_json::json;
use sysknife_brain::planning_tools::propose_plan::KNOWN_ACTIONS;
use sysknife_core::action_family::{DEBIAN_ONLY_ACTIONS, FEDORA_ONLY_ACTIONS, UBUNTU_ONLY_ACTIONS};
use sysknife_daemon::actions::{all_specs, ActionSpec, DISPATCHER_INTERNAL_ACTIONS};
use sysknife_daemon::executor::build_action_spec;
use sysknife_daemon::policy::{min_role_for_action, role_for_risk_level};
use sysknife_daemon::preview::preview_action;
use sysknife_types::{CallerRole, RequestEnvelope, RequestHash, RiskLevel};
fn all_spec_action_names() -> BTreeSet<&'static str> {
let mut names = BTreeSet::new();
for &name in DISPATCHER_INTERNAL_ACTIONS {
names.insert(name);
}
for spec in all_specs() {
names.insert(spec.action_name);
}
names
}
#[test]
fn every_spec_action_has_a_policy_entry() {
let mut missing = Vec::new();
for name in all_spec_action_names() {
if min_role_for_action(name).is_none() {
missing.push(name);
}
}
assert!(
missing.is_empty(),
"actions present in specs but missing from policy::min_role_for_action: {missing:?}"
);
}
#[test]
fn every_spec_action_is_recognised_by_executor() {
let dispatcher_internal: BTreeSet<&str> = DISPATCHER_INTERNAL_ACTIONS.iter().copied().collect();
let mut missing = Vec::new();
for name in all_spec_action_names() {
if dispatcher_internal.contains(name) {
continue;
}
if let Err(sysknife_daemon::executor::ExecutorError::UnknownAction(_)) =
build_action_spec(name, &json!({}))
{
missing.push(name);
}
}
assert!(
missing.is_empty(),
"actions present in specs but unknown to executor::build_action_spec: {missing:?}"
);
}
#[test]
fn every_spec_action_exists_in_brain_known_actions() {
let known: BTreeSet<&str> = KNOWN_ACTIONS.iter().map(|(n, _)| *n).collect();
let mut missing = Vec::new();
for name in all_spec_action_names() {
if !known.contains(name) {
missing.push(name);
}
}
assert!(
missing.is_empty(),
"actions present in specs but missing from brain KNOWN_ACTIONS: {missing:?}"
);
}
#[test]
fn brain_known_actions_has_no_stale_entries() {
let spec_names = all_spec_action_names();
let mut stale = Vec::new();
for &(name, _) in KNOWN_ACTIONS {
if !spec_names.contains(name) {
stale.push(name);
}
}
assert!(
stale.is_empty(),
"KNOWN_ACTIONS contains entries not present in any action module specs(): {stale:?}"
);
}
fn preview_envelope(action_name: &str) -> sysknife_types::PreviewEnvelope {
let request = RequestEnvelope {
action_name: action_name.to_string(),
request_id: "action-consistency".to_string(),
params: serde_json::Value::Null,
caller_role: CallerRole::Dev,
request_hash: RequestHash::new("hash".to_string()),
};
preview_action(&request, serde_json::Value::Null, serde_json::Value::Null)
}
fn preview_risk(action_name: &str) -> RiskLevel {
preview_envelope(action_name).risk_level
}
fn role_rank(role: CallerRole) -> u8 {
match role {
CallerRole::Observer => 0,
CallerRole::Dev => 1,
CallerRole::Admin => 2,
CallerRole::Boot => 3,
}
}
#[test]
fn preview_risk_matches_spec_risk_for_every_action() {
let mut mismatches = Vec::new();
for spec in all_specs() {
let got = preview_risk(spec.action_name);
if got != spec.risk_level {
mismatches.push(format!(
"{}: spec={:?} but preview gate={:?}",
spec.action_name, spec.risk_level, got
));
}
}
assert!(
mismatches.is_empty(),
"preview/approval-gate risk diverged from ActionSpec (single source of truth):\n{}",
mismatches.join("\n")
);
}
#[test]
fn role_mirrors_risk_except_documented_monotonic_exceptions() {
const RAISED_EXCEPTIONS: &[&str] = &[];
let mut violations = Vec::new();
for spec in all_specs() {
let baseline = role_for_risk_level(spec.risk_level);
let actual =
min_role_for_action(spec.action_name).expect("every spec action has a policy role");
if actual == baseline {
continue;
}
let raised = role_rank(actual) > role_rank(baseline);
let documented = RAISED_EXCEPTIONS.contains(&spec.action_name);
if !(raised && documented) {
let why = if !raised {
"role is LOWER than the risk floor — would weaken the gate"
} else {
"undocumented exception — add to RAISED_EXCEPTIONS + policy::role_exception"
};
violations.push(format!(
"{}: risk={:?} implies {:?}, but role={:?} ({why})",
spec.action_name, spec.risk_level, baseline, actual
));
}
}
assert!(
violations.is_empty(),
"RBAC role \u{2194} risk invariant violated:\n{}",
violations.join("\n")
);
}
#[test]
fn preview_reboot_and_rollback_match_spec_for_every_action() {
let mut mismatches = Vec::new();
for spec in all_specs() {
let env = preview_envelope(spec.action_name);
if env.reboot_required != spec.reboot_required
|| env.rollback_available != spec.rollback_available
{
mismatches.push(format!(
"{}: spec reboot={}/rollback={} but preview reboot={}/rollback={}",
spec.action_name,
spec.reboot_required,
spec.rollback_available,
env.reboot_required,
env.rollback_available,
));
}
}
assert!(
mismatches.is_empty(),
"preview reboot/rollback diverged from ActionSpec (single source of truth):\n{}",
mismatches.join("\n")
);
}
#[test]
fn every_catalogued_action_has_a_preview_profile() {
let mut unclassified = Vec::new();
for spec in all_specs() {
let env = preview_envelope(spec.action_name);
let unrecognised = env
.expected_side_effects
.iter()
.any(|e| e.contains("unclassified action"))
|| env
.warnings
.iter()
.any(|w| w.contains("action profile not recognized"));
if unrecognised {
unclassified.push(spec.action_name);
}
}
assert!(
unclassified.is_empty(),
"catalogued actions with no preview_profile arm (they render as \
'unclassified action'): {unclassified:?}"
);
}
const FEDORA_TOOLS: &[&str] = &["rpm-ostree", "ostree", "dnf", "rpm"];
const FEDORA_PATHS: &[&str] = &["/etc/yum.repos.d"];
const DEBIAN_TOOLS: &[&str] = &[
"apt",
"apt-get",
"apt-mark",
"apt-cache",
"dpkg",
"apt-pin-edit",
"unattended-upgrades-edit",
"grub-kargs-edit",
"update-grub",
"unattended-upgrade",
];
const DEBIAN_PATHS: &[&str] = &["/etc/apt/", "/var/log/apt/", "/etc/default/grub"];
const UBUNTU_TOOLS: &[&str] = &[
"pro",
"add-apt-repository",
"do-release-upgrade",
"canonical-livepatch",
];
const UBUNTU_PATHS: &[&str] = &["/var/run/reboot-required"];
fn mechanism_text(spec: &ActionSpec) -> String {
match &spec.mechanism {
sysknife_daemon::actions::ActionMechanism::Command { program, args } => {
format!("{program} {}", args.join(" "))
}
sysknife_daemon::actions::ActionMechanism::FileScan { path }
| sysknife_daemon::actions::ActionMechanism::FileWrite { path, .. }
| sysknife_daemon::actions::ActionMechanism::FilePatch { path, .. }
| sysknife_daemon::actions::ActionMechanism::FileDelete { path } => path.clone(),
}
}
fn mentions_tool(text: &str, tool: &str) -> bool {
text.split(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.')
.any(|word| word == tool)
}
const UNFENCED_BY_DECISION: &[&str] = &[];
#[test]
fn family_fence_agrees_with_each_action_s_mechanism() {
let mut wrong = Vec::new();
let specs = all_specs();
for name in FEDORA_ONLY_ACTIONS
.iter()
.chain(DEBIAN_ONLY_ACTIONS)
.chain(UBUNTU_ONLY_ACTIONS)
{
if !specs.iter().any(|spec| spec.action_name == *name) {
wrong.push(format!(
"{name}: hard fence names an action absent from the catalogue"
));
}
}
for spec in specs {
let name = spec.action_name;
let text = mechanism_text(&spec);
let fedora_shaped = FEDORA_TOOLS.iter().any(|t| mentions_tool(&text, t))
|| FEDORA_PATHS.iter().any(|p| text.contains(p));
let debian_shaped = DEBIAN_TOOLS.iter().any(|t| mentions_tool(&text, t))
|| DEBIAN_PATHS.iter().any(|p| text.contains(p));
let ubuntu_shaped = UBUNTU_TOOLS.iter().any(|t| mentions_tool(&text, t))
|| UBUNTU_PATHS.iter().any(|p| text.contains(p));
if [fedora_shaped, debian_shaped, ubuntu_shaped]
.iter()
.filter(|x| **x)
.count()
> 1
{
wrong.push(format!(
"{name}: mechanism mentions both families' tooling: {text}"
));
continue;
}
if fedora_shaped
&& !FEDORA_ONLY_ACTIONS.contains(&name)
&& !UNFENCED_BY_DECISION.contains(&name)
{
wrong.push(format!(
"{name}: drives Fedora-only tooling but is not in FEDORA_ONLY_ACTIONS ({text})"
));
}
if debian_shaped && !DEBIAN_ONLY_ACTIONS.contains(&name) {
wrong.push(format!(
"{name}: drives Debian-only tooling but is not in DEBIAN_ONLY_ACTIONS ({text})"
));
}
if ubuntu_shaped != UBUNTU_ONLY_ACTIONS.contains(&name) {
wrong.push(format!(
"{name}: Ubuntu fence disagrees with mechanism ({text})"
));
}
if FEDORA_ONLY_ACTIONS.contains(&name) && !fedora_shaped {
wrong.push(format!(
"{name}: Fedora fence exceeds its mechanism ({text})"
));
}
if DEBIAN_ONLY_ACTIONS.contains(&name) && !debian_shaped {
wrong.push(format!(
"{name}: Debian fence exceeds its mechanism ({text})"
));
}
}
assert!(
wrong.is_empty(),
"the family fence disagrees with what these actions actually run:\n {}",
wrong.join("\n ")
);
}
#[test]
fn the_unfenced_by_decision_list_is_still_load_bearing() {
for name in UNFENCED_BY_DECISION {
assert!(
!FEDORA_ONLY_ACTIONS.contains(name)
&& !DEBIAN_ONLY_ACTIONS.contains(name)
&& !UBUNTU_ONLY_ACTIONS.contains(name),
"{name} is now fenced; remove it from UNFENCED_BY_DECISION"
);
let spec = all_specs()
.into_iter()
.find(|s| s.action_name == *name)
.unwrap_or_else(|| panic!("{name} is not in the catalogue at all"));
let text = mechanism_text(&spec);
assert!(
FEDORA_TOOLS.iter().any(|t| mentions_tool(&text, t))
|| FEDORA_PATHS.iter().any(|p| text.contains(p)),
"{name} no longer drives family-specific tooling ({text}); the exemption is stale"
);
}
}