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.
This commit is contained in:
95
internal/policy/risk_test.go
Normal file
95
internal/policy/risk_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user