Files
oikos/internal/policy/risk_test.go
dtoro 75c0848a6f
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
0.29.0 — code-quality refactor (plan E1–E5): file splits, sqlc migration, SSH unification, test coverage
E1: split monolithic files — cmd/nomos (main.go → server.go + mcp.go + workers.go),
    internal/mcp/tools.go → entity_tools/ops_tools/knowledge_tools/analysis_tools,
    internal/httpapi/impl.go → domain files (entities, events, signals, ontology,
    fleet_health, client_context, client_lifecycle, entity_mutations, query_audit).
E2: migrate raw pool.Exec queries to sqlc (entities/relationships queries + generated).
E3: unify SSH — consolidate crypto/ssh dial into actuator/client.go (+client_test).
E4/E5: add tests — db/lifecycle, checkdefaults/build, ontology/preconditions, policy/risk.
2026-08-08 22:47:06 +02:00

96 lines
3.5 KiB
Go

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)
}
}
}