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.
96 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|