Files
oikos/internal/domain/domain_test.go
dtoro c96c795126 test: add unit tests for 6 previously-untested packages (R7)
Added pure unit tests for all packages that had 0% coverage. Where pure
logic was entangled with DB calls, extracted testable helpers first.

internal/domain (0% -> 100%):
- TestIsNil, TestCanTransition (all 30 state transitions), TestSentinelErrors,
  TestSignalTransitionsComplete

internal/learning (0% -> 26.2%):
- Refactored processGroup to extract 4 pure helpers: countOutcomes,
  computeConfidence, shouldValidate, shouldQuarantine
- TestWilsonLowerBound (monotonicity, edge cases, sample-size cap)
- TestCountOutcomes, TestComputeConfidence, TestShouldValidate,
  TestShouldQuarantine (table-driven)
- Remaining gap: extractPatterns/processGroup DB calls need make test-db

internal/policy (39% -> 50%):
- Extracted determineRoute from ClassifySignal (pure route logic)
- TestDetermineRoute (7 cases covering global/entity kill-switches, approval)
- Remaining gap: ClassifySignal/computeBlastRadius need DB mock

internal/knowledge (0% -> 14.2%):
- TestContentHash, TestStr, TestStrSlice, TestMapVal, TestToPGArray
- Documented latent bug: toPGArray doesn't escape " or \\ in tags
- Remaining gap: ingest* functions need make test-db

internal/actuator (0% -> 14.7%):
- TestSSHErrorClassString, TestClassifySSHError (11 cases incl. net.Error mock)
- TestParseProcedure, TestSetDefaultSSHTimeout
- Circuit breaker full state-machine test (open/close/reset/per-target)
- Remaining gap: ExecuteProcedure/ProvisionLXC need SSH+DB fixtures

internal/scheduler (0% -> 7.3%):
- TestParsePingLatency (Linux/macOS formats), TestAllowlistedScript
- TestEvaluateSeverity (threshold logic, crit:0 skip, signalKind fallback)
- Remaining gap: checkHTTP/checkTCP need httptest; runCheckPass needs DB

internal/notifier (0% -> 6.4%):
- TestHashToken, TestGenerateApprovalToken (HMAC re-derivation)
- Remaining gap: checkReaction/sendMatrixAlert need httptest; DB funcs need
  make test-db

All tests pass with -race. domain hits its 60% gate at 100%. The remaining
packages need integration tests (make test-db) and/or httptest-based tests
to reach their coverage gates — tracked as follow-up.
2026-07-17 22:54:44 +02:00

140 lines
4.0 KiB
Go

package domain
import (
"errors"
"testing"
)
func TestIsNil(t *testing.T) {
cases := []struct {
name string
u UUID
want bool
}{
{"empty string", UUID(""), true},
{"single char", UUID("x"), false},
{"uuid string", UUID("550e8400-e29b-41d4-a716-446655440000"), false},
{"nil literal", UUID(""), true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := c.u.IsNil()
if got != c.want {
t.Errorf("UUID(%q).IsNil() = %v, want %v", c.u, got, c.want)
}
})
}
}
func TestCanTransition(t *testing.T) {
type tc struct {
name string
from string
to string
want bool
}
var cases []tc
for from, targets := range ValidSignalTransitions {
for _, to := range targets {
cases = append(cases, tc{from + "->" + to, from, to, true})
}
}
disallowed := []tc{
{"raised->raised", SignalRaised, SignalRaised, false},
{"resolved->raised", SignalResolved, SignalRaised, false},
{"failed->raised", SignalFailed, SignalRaised, false},
{"acknowledged->raised", SignalAcknowledged, SignalRaised, false},
{"muted->resolved", SignalMuted, SignalResolved, false},
{"acting->acknowledged", SignalActing, SignalAcknowledged, false},
}
cases = append(cases, disallowed...)
cases = append(cases,
tc{"unknown source", "nonexistent", SignalRaised, false},
tc{"unknown target", SignalRaised, "nonexistent", false},
)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
s := &Signal{State: c.from}
got := s.CanTransition(c.to)
if got != c.want {
t.Errorf("CanTransition(%q -> %q) = %v, want %v", c.from, c.to, got, c.want)
}
})
}
}
func TestSentinelErrors(t *testing.T) {
cases := []struct {
name string
err error
msg string
}{
{"ErrNotFound", ErrNotFound, "entity not found"},
{"ErrInvalidTransition", ErrInvalidTransition, "invalid lifecycle transition"},
{"ErrApprovalRequired", ErrApprovalRequired, "operator approval required"},
{"ErrAutonomyBlocked", ErrAutonomyBlocked, "autonomy policy blocks this action"},
{"ErrConflict", ErrConflict, "concurrent modification conflict"},
{"ErrCircuitOpen", ErrCircuitOpen, "circuit breaker open for target"},
{"ErrAbstractType", ErrAbstractType, "cannot instantiate abstract entity type"},
{"ErrInvalidEdge", ErrInvalidEdge, "relationship endpoint type mismatch"},
{"ErrCardinality", ErrCardinality, "relationship cardinality violation"},
{"ErrSeedHashMismatch", ErrSeedHashMismatch, "seed content hash mismatch"},
{"ErrAlreadyExists", ErrAlreadyExists, "entity already exists"},
{"ErrQuarantined", ErrQuarantined, "pattern is quarantined"},
{"ErrSkillDeprecated", ErrSkillDeprecated, "skill is deprecated"},
{"ErrInvalidInput", ErrInvalidInput, "invalid input"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.err == nil {
t.Fatal("sentinel error is nil")
}
if !errors.Is(c.err, c.err) {
t.Errorf("errors.Is failed for %s", c.name)
}
if c.err.Error() != c.msg {
t.Errorf("Error() = %q, want %q", c.err.Error(), c.msg)
}
})
}
}
func TestSignalTransitionsComplete(t *testing.T) {
// Non-terminal states must be keys in ValidSignalTransitions.
// SignalResolved is a terminal state (no outgoing transitions) and is
// intentionally absent from the map.
nonTerminal := []string{
SignalRaised,
SignalAcknowledged,
SignalActing,
SignalMuted,
SignalFailed,
}
for _, state := range nonTerminal {
targets, ok := ValidSignalTransitions[state]
if !ok {
t.Errorf("non-terminal state %q missing from ValidSignalTransitions", state)
continue
}
if len(targets) == 0 {
t.Errorf("state %q maps to empty transition list", state)
}
}
// Resolved is terminal: it should not appear as a source key.
if _, ok := ValidSignalTransitions[SignalResolved]; ok {
t.Errorf("terminal state %q should not have outgoing transitions", SignalResolved)
}
// No state anywhere in the map may map to nil/empty.
for state, targets := range ValidSignalTransitions {
if len(targets) == 0 {
t.Errorf("state %q maps to empty/nil transition list", state)
}
}
}