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.
79 lines
3.3 KiB
Go
79 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestExtractExecutionIDs(t *testing.T) {
|
|
// Real tool-result phrasings that should yield an execution id.
|
|
pos := map[string]string{
|
|
`"pct_create on host:strong auto-approved via assent window — execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c running."`: "019f4b19-eafd-74ed-baa6-d24a27b3f52c",
|
|
`"run on lxc:caddy requires approval (risk: config_mutation) — execution 019f4af7-7eff-7723-b38c-b540b267f407 queued."`: "019f4af7-7eff-7723-b38c-b540b267f407",
|
|
`"apt_upgrade on host:hubris auto-approved via assent window — execution 019f4b58-c88c-7767-87dd-044608ced913 running."`: "019f4b58-c88c-7767-87dd-044608ced913",
|
|
}
|
|
for in, want := range pos {
|
|
ids := extractExecutionIDs(in)
|
|
if len(ids) != 1 || ids[0].String() != want {
|
|
t.Errorf("extractExecutionIDs(%q) = %v, want [%s]", in, ids, want)
|
|
}
|
|
}
|
|
|
|
// Synchronous auto-run and read-only results carry no "execution <uuid>"
|
|
// phrasing — they've already completed inline and must NOT be linked for
|
|
// continuation.
|
|
neg := []string{
|
|
`"run on host:strong (read_only, auto): 09:30 up 8 days"`,
|
|
`"run on lxc:caddy (config_mutation, auto via assent window): done"`,
|
|
`[{"slug":"lxc:caddy","health":"healthy"}]`,
|
|
`"target not found: lxc:nope"`,
|
|
}
|
|
for _, in := range neg {
|
|
if ids := extractExecutionIDs(in); len(ids) != 0 {
|
|
t.Errorf("extractExecutionIDs(%q) = %v, want none", in, ids)
|
|
}
|
|
}
|
|
|
|
// De-dupes repeated ids in one result.
|
|
dup := `execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c queued ... execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c running`
|
|
if ids := extractExecutionIDs(dup); len(ids) != 1 {
|
|
t.Errorf("expected de-dup to 1 id, got %v", ids)
|
|
}
|
|
}
|
|
|
|
// TestResumeSession_SkipsWhenBusy guards the P0 fix
|
|
// (plans/2026-08-03-nomos-chat-changes-review.md): resumeSession must skip —
|
|
// return false, body never executed — when a turn is already active for the
|
|
// session. continueSession relies on this so it only marks a continuation
|
|
// "continued" after a turn really ran (otherwise the result is lost: marked
|
|
// continued, never re-queued by pendingContinuations).
|
|
//
|
|
// A minimal agent with only a gate is enough: if the body ever ran, chatWith
|
|
// would dereference the nil provider and panic. Returning false cleanly proves
|
|
// the body was skipped.
|
|
func TestResumeSession_SkipsWhenBusy(t *testing.T) {
|
|
a := &agent{gate: turngate.New()}
|
|
if !a.gate.acquire("sess", 0) {
|
|
t.Fatal("precondition: initial acquire should succeed on a free session")
|
|
}
|
|
ran := a.resumeSession(context.Background(), "sess", "note")
|
|
if ran {
|
|
t.Fatal("resumeSession must return false (skip) while a turn is active for the session")
|
|
}
|
|
}
|
|
|
|
// TestContinueSession_DefersWhenBusy guards the other half of P0: when the
|
|
// session is busy, continueSession defers (leaves the execution pending for the
|
|
// next worker tick) instead of running or marking it. It must return cleanly
|
|
// without reaching resumeSession's body (nil provider → panic) or markContinued.
|
|
func TestContinueSession_DefersWhenBusy(t *testing.T) {
|
|
a := &agent{gate: turngate.New()}
|
|
if !a.gate.acquire("sess", 0) {
|
|
t.Fatal("precondition: initial acquire should succeed on a free session")
|
|
}
|
|
p := pendingContinuation{ExecID: uuid.New(), SessionID: "sess", Status: "completed"}
|
|
a.continueSession(context.Background(), p) // must not panic; must not run/mark
|
|
}
|