Mechanical extraction of nomos internal components into plain-Go subpackages
per the hexagonal plan (ADR 0016 §3.1 rule 3):
turngate/ — per-session turn serialization (plan 2026-08-03 F1)
retrycap/ — per-turn run retry cap (maxRunRetries=3)
messagequeue/ — operator-message queue for busy-turn re-entry (F2)
assent/ — chat-assent detection (isAssent, isTypedConfirmation,
ExtractPendingApprovals), decoupled from agent via
[]string input instead of persistedCall
session/ — store (chat sessions, plan execution, DB persistence),
migration runner + local emitEvent to break adapter
dependency
internal/migrate/ — shared migration runner extracted from postgres pool,
used by both the oikos postgres adapter and session tests.
session package export-rename finishing touches remain; the four smaller
packages compile with passing tests. Depguard rules and ADR-0016 leaf-note
update deferred to a followup. VERSION 0.35.1.
130 lines
4.1 KiB
Go
130 lines
4.1 KiB
Go
package retrycap
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRunFailureKey_StableAcrossWhitespace(t *testing.T) {
|
|
cases := []struct{ a, b string }{
|
|
{"chown :10000 /mnt/media_local && chmod 2775 /mnt/media_local",
|
|
"chown :10000 /mnt/media_local && chmod 2775 /mnt/media_local"},
|
|
{"chown :10000 /mnt/media_local\n&& chmod 2775 /mnt/media_local",
|
|
"chown :10000 /mnt/media_local && chmod 2775 /mnt/media_local"},
|
|
{"chown :10000 /mnt/media_local && chmod 2775 /mnt/media_local ",
|
|
" chown :10000 /mnt/media_local && chmod 2775 /mnt/media_local"},
|
|
}
|
|
for i, c := range cases {
|
|
ka := RunFailureKey("host:strong", c.a)
|
|
kb := RunFailureKey("host:strong", c.b)
|
|
if ka != kb {
|
|
t.Errorf("case %d: keys differ for whitespace-equivalent commands:\n a=%q\n b=%q", i, c.a, c.b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunFailureKey_DiffersByTarget(t *testing.T) {
|
|
a := RunFailureKey("host:strong", "echo hi")
|
|
b := RunFailureKey("host:hubris", "echo hi")
|
|
if a == b {
|
|
t.Error("keys should differ when target differs")
|
|
}
|
|
}
|
|
|
|
func TestRunFailureKey_DiffersByCommand(t *testing.T) {
|
|
a := RunFailureKey("host:strong", "echo hi")
|
|
b := RunFailureKey("host:strong", "echo bye")
|
|
if a == b {
|
|
t.Error("keys should differ when command differs")
|
|
}
|
|
}
|
|
|
|
func TestRunRetryTracker_CountsAndCaps(t *testing.T) {
|
|
r := New()
|
|
key := RunFailureKey("host:strong", "chown :10000 /mnt/media_local")
|
|
for i := 1; i <= MaxRunRetries; i++ {
|
|
if got := r.RecordFailure(key); got != i {
|
|
t.Errorf("RecordFailure #%d = %d, want %d", i, got, i)
|
|
}
|
|
}
|
|
// At the cap, Failures() should report MaxRunRetries, and the next
|
|
// identical call should be refused by the agent loop (Failures() >=
|
|
// MaxRunRetries).
|
|
if got := r.Failures(key); got != MaxRunRetries {
|
|
t.Errorf("Failures = %d, want %d", got, MaxRunRetries)
|
|
}
|
|
if r.Failures(key) < MaxRunRetries {
|
|
t.Errorf("cap should be enforced at MaxRunRetries=%d", MaxRunRetries)
|
|
}
|
|
}
|
|
|
|
func TestRunRetryTracker_PerTurnIsolation(t *testing.T) {
|
|
// Different keys don't interfere.
|
|
r := New()
|
|
k1 := RunFailureKey("host:strong", "echo a")
|
|
k2 := RunFailureKey("host:strong", "echo b")
|
|
r.RecordFailure(k1)
|
|
r.RecordFailure(k1)
|
|
if got := r.Failures(k2); got != 0 {
|
|
t.Errorf("k2 Failures = %d, want 0 (keys are isolated)", got)
|
|
}
|
|
}
|
|
|
|
func TestIsRunFailure(t *testing.T) {
|
|
cases := []struct {
|
|
desc string
|
|
tool string
|
|
result string
|
|
callErr error
|
|
want bool
|
|
}{
|
|
{"run with ERROR prefix", "run", "run on host:strong: ERROR ssh: signal: killed", nil, true},
|
|
{"run with exit error", "run", "run on lxc:caddy: ERROR exit status 1", nil, true},
|
|
{"run success (read-only auto)", "run", "run on host:strong (read_only, auto): hello", nil, false},
|
|
{"run success (assent window)", "run", "run on host:strong (config_mutation, auto via assent window): done", nil, false},
|
|
{"run queued for approval", "run", "run on host:strong requires approval (risk: config_mutation) — execution 019f4930 queued. Present the command and purpose to the operator and wait; do not re-request.", nil, false},
|
|
{"non-run tool", "get_entity", "lxc list result", nil, false},
|
|
{"callErr set (dispatch failure)", "run", "", errFake{}, true},
|
|
{"callErr set on non-run tool", "get_entity", "some result", errFake{}, true}, // callErr trumps name
|
|
}
|
|
for i, c := range cases {
|
|
got := IsRunFailure(c.tool, c.result, c.callErr)
|
|
if got != c.want {
|
|
t.Errorf("case %d (%s): IsRunFailure = %v, want %v", i, c.desc, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
type errFake struct{}
|
|
|
|
func (errFake) Error() string { return "fake dispatch error" }
|
|
|
|
func TestRunRetryDirective_Content(t *testing.T) {
|
|
d := RunRetryDirective("host:strong", "chown :10000 /mnt/media_local", 3)
|
|
for _, want := range []string{
|
|
"Refused:",
|
|
"host:strong",
|
|
"3 times",
|
|
"retry cap hit",
|
|
"Do NOT retry",
|
|
"strace",
|
|
"ps aux",
|
|
"lsof",
|
|
"surface the blocker",
|
|
} {
|
|
if !strings.Contains(d, want) {
|
|
t.Errorf("directive missing %q; got:\n%s", want, d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestItoa(t *testing.T) {
|
|
cases := map[int]string{0: "0", 1: "1", 9: "9", 10: "10", 42: "42",
|
|
100: "100", -1: "-1", -42: "-42"}
|
|
for in, want := range cases {
|
|
if got := itoa(in); got != want {
|
|
t.Errorf("itoa(%d) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|