package policy import "testing" // The escalation ladder is the load-bearing invariant of the policy layer: // computed risk may only escalate, never de-escalate, against the caller's // declaration. These pin the rank order and the unknown-input defaults that // ClassifyCommand relies on (riskRank/normalizeRisk were only 66% covered). func TestRiskRankOrder(t *testing.T) { cases := []struct { a, b string want bool // want riskRank(a) < riskRank(b) }{ {RiskReadOnly, RiskReversibleLow, true}, {RiskReversibleLow, RiskConfigMutation, true}, {RiskConfigMutation, RiskDestructive, true}, {RiskReadOnly, RiskDestructive, true}, {RiskDestructive, RiskReadOnly, false}, {RiskConfigMutation, RiskConfigMutation, false}, } for _, c := range cases { if got := riskRank(c.a) < riskRank(c.b); got != c.want { t.Errorf("riskRank(%q) < riskRank(%q) = %v, want %v", c.a, c.b, got, c.want) } } } func TestRiskRankUnknownDefaultsToConfigMutation(t *testing.T) { // An unrecognized declared risk is treated as config_mutation — the // safer-to-gate default — not as the lowest tier. if r := riskRank("totally_made_up"); r != riskRank(RiskConfigMutation) { t.Errorf("riskRank(unknown) = %d, want %d (config_mutation)", r, riskRank(RiskConfigMutation)) } // It therefore outranks read_only and reversible_low... if riskRank("made_up") <= riskRank(RiskReadOnly) { t.Error("unknown risk should outrank read_only") } if riskRank("made_up") <= riskRank(RiskReversibleLow) { t.Error("unknown risk should outrank reversible_low") } // ...but never outranks destructive. if riskRank("made_up") >= riskRank(RiskDestructive) { t.Error("unknown risk must not outrank destructive") } } func TestNormalizeRisk(t *testing.T) { cases := []struct { in string want string }{ {RiskReadOnly, RiskReadOnly}, {RiskReversibleLow, RiskReversibleLow}, {RiskConfigMutation, RiskConfigMutation}, {RiskDestructive, RiskDestructive}, // Unknown / empty / malformed declared risks collapse to the gated // default rather than the most-permissive tier. {"", RiskConfigMutation}, {"bogus", RiskConfigMutation}, {"READ_ONLY", RiskConfigMutation}, // case-sensitive: not normalized {"read-only", RiskConfigMutation}, // hyphen, not underscore } for _, c := range cases { if got := normalizeRisk(c.in); got != c.want { t.Errorf("normalizeRisk(%q) = %q, want %q", c.in, got, c.want) } } } // Escalation property: ClassifyCommand returns max(rank(computed), rank(declared)). // Over a read-only command (computed rank 0) the declared risk passes through // (undeclared → read_only; bogus → config_mutation); over a destructive command // (computed rank 3) the result is always destructive. func TestClassifyCommandEscalationIsMaxOfRanks(t *testing.T) { readOnlyExpected := []struct { declared, want string }{ {"", RiskReadOnly}, {RiskReadOnly, RiskReadOnly}, {RiskReversibleLow, RiskReversibleLow}, {RiskConfigMutation, RiskConfigMutation}, {RiskDestructive, RiskDestructive}, {"bogus", RiskConfigMutation}, // unknown declared → config_mutation rank } for _, c := range readOnlyExpected { if got := ClassifyCommand("uptime", c.declared); got != c.want { t.Errorf("read-only cmd + declared %q = %q, want %q", c.declared, got, c.want) } } for _, d := range []string{"", RiskReadOnly, RiskReversibleLow, RiskConfigMutation, RiskDestructive, "bogus"} { if got := ClassifyCommand("rm -rf /var/lib/x", d); got != RiskDestructive { t.Errorf("destructive cmd + declared %q = %q, want destructive", d, got) } } }