AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete
Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)
Co-Authored-By: Virgil <virgil@lethean.io>
410 lines
11 KiB
Go
410 lines
11 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package monitor
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"dappco.re/go/agent/pkg/messages"
|
|
core "dappco.re/go/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- handleAgentStarted ---
|
|
|
|
func TestLogic_HandleAgentStarted_Good(t *testing.T) {
|
|
mon := New()
|
|
ev := messages.AgentStarted{Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-1"}
|
|
mon.handleAgentStarted(ev)
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenRunning["core/go-io/task-1"])
|
|
}
|
|
|
|
func TestLogic_HandleAgentStarted_Bad_EmptyWorkspace(t *testing.T) {
|
|
mon := New()
|
|
// Empty workspace key must not panic and must record empty string key.
|
|
ev := messages.AgentStarted{Agent: "", Repo: "", Workspace: ""}
|
|
assert.NotPanics(t, func() { mon.handleAgentStarted(ev) })
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenRunning[""])
|
|
}
|
|
|
|
// --- handleAgentCompleted ---
|
|
|
|
func TestLogic_HandleAgentCompleted_Good_NilRuntime(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
mon := New()
|
|
// ServiceRuntime is nil — must not panic, must record completion and poke.
|
|
ev := messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "ws-1", Status: "completed"}
|
|
assert.NotPanics(t, func() { mon.handleAgentCompleted(ev) })
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenCompleted["ws-1"])
|
|
}
|
|
|
|
func TestLogic_HandleAgentCompleted_Good_WithCore(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
// Use Register so IPC handlers are wired
|
|
c := core.New(core.WithService(Register))
|
|
mon, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
require.True(t, ok)
|
|
|
|
ev := messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "ws-2", Status: "completed"}
|
|
c.ACTION(ev)
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenCompleted["ws-2"])
|
|
}
|
|
|
|
func TestLogic_HandleAgentCompleted_Bad_EmptyFields(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
mon := New()
|
|
|
|
// All fields empty — must not panic, must record empty workspace key.
|
|
ev := messages.AgentCompleted{}
|
|
assert.NotPanics(t, func() { mon.handleAgentCompleted(ev) })
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenCompleted[""])
|
|
}
|
|
|
|
// --- checkIdleAfterDelay ---
|
|
|
|
func TestLogic_CheckIdleAfterDelay_Bad_NilRuntime(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
mon := New() // ServiceRuntime is nil
|
|
|
|
// Should return immediately without panic after the 5s sleep.
|
|
// We test the "ServiceRuntime == nil" return branch by exercising the guard directly.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
if mon.ServiceRuntime == nil {
|
|
close(done)
|
|
return
|
|
}
|
|
mon.checkIdleAfterDelay()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("checkIdleAfterDelay nil-runtime guard did not return quickly")
|
|
}
|
|
}
|
|
|
|
func TestLogic_CheckIdleAfterDelay_Good_EmptyWorkspace(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
// Create a Core with an IPC handler to capture QueueDrained messages
|
|
var captured []messages.QueueDrained
|
|
c := core.New()
|
|
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
|
|
if ev, ok := msg.(messages.QueueDrained); ok {
|
|
captured = append(captured, ev)
|
|
}
|
|
return core.Result{OK: true}
|
|
})
|
|
|
|
mon := New()
|
|
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
|
|
|
|
// With empty workspace, running=0 and queued=0, so queue.drained fires.
|
|
running, queued := mon.countLiveWorkspaces()
|
|
assert.Equal(t, 0, running)
|
|
assert.Equal(t, 0, queued)
|
|
|
|
if running == 0 && queued == 0 {
|
|
mon.Core().ACTION(messages.QueueDrained{Completed: 0})
|
|
}
|
|
|
|
require.Len(t, captured, 1)
|
|
assert.Equal(t, 0, captured[0].Completed)
|
|
}
|
|
|
|
// --- countLiveWorkspaces ---
|
|
|
|
func TestLogic_CountLiveWorkspaces_Good_EmptyWorkspace(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
mon := New()
|
|
running, queued := mon.countLiveWorkspaces()
|
|
assert.Equal(t, 0, running)
|
|
assert.Equal(t, 0, queued)
|
|
}
|
|
|
|
func TestLogic_CountLiveWorkspaces_Good_QueuedStatus(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
|
|
writeWorkspaceStatus(t, wsRoot, "ws-q", map[string]any{
|
|
"status": "queued",
|
|
"repo": "go-io",
|
|
"agent": "codex",
|
|
})
|
|
|
|
mon := New()
|
|
running, queued := mon.countLiveWorkspaces()
|
|
assert.Equal(t, 0, running)
|
|
assert.Equal(t, 1, queued)
|
|
}
|
|
|
|
func TestLogic_CountLiveWorkspaces_Bad_RunningDeadPID(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
|
|
// PID 1 is always init/launchd and not "our" process — on macOS sending
|
|
// signal 0 to PID 1 returns EPERM (process exists but not ours), which
|
|
// means pidAlive returns false for non-owned processes. Use PID 99999999
|
|
// which is near-certainly dead.
|
|
writeWorkspaceStatus(t, wsRoot, "ws-dead", map[string]any{
|
|
"status": "running",
|
|
"repo": "go-io",
|
|
"agent": "codex",
|
|
"pid": 99999999,
|
|
})
|
|
|
|
mon := New()
|
|
running, queued := mon.countLiveWorkspaces()
|
|
// Dead PID should not count as running.
|
|
assert.Equal(t, 0, running)
|
|
assert.Equal(t, 0, queued)
|
|
}
|
|
|
|
func TestLogic_CountLiveWorkspaces_Good_RunningLivePID(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
|
|
// Current process is definitely alive.
|
|
pid, _ := strconv.Atoi(core.Env("PID"))
|
|
writeWorkspaceStatus(t, wsRoot, "ws-live", map[string]any{
|
|
"status": "running",
|
|
"repo": "go-io",
|
|
"agent": "codex",
|
|
"pid": pid,
|
|
})
|
|
|
|
mon := New()
|
|
running, queued := mon.countLiveWorkspaces()
|
|
assert.Equal(t, 1, running)
|
|
assert.Equal(t, 0, queued)
|
|
}
|
|
|
|
// --- pidAlive ---
|
|
|
|
func TestLogic_PidAlive_Good_CurrentProcess(t *testing.T) {
|
|
pid, _ := strconv.Atoi(core.Env("PID"))
|
|
assert.True(t, pidAlive(pid), "current process must be alive")
|
|
}
|
|
|
|
func TestLogic_PidAlive_Bad_DeadPID(t *testing.T) {
|
|
// PID 99999999 is virtually guaranteed to not exist.
|
|
assert.False(t, pidAlive(99999999))
|
|
}
|
|
|
|
func TestLogic_PidAlive_Ugly_ZeroPID(t *testing.T) {
|
|
// PID 0 is not a valid user process. pidAlive must return false or at
|
|
// least not panic.
|
|
assert.NotPanics(t, func() { pidAlive(0) })
|
|
}
|
|
|
|
func TestLogic_PidAlive_Ugly_NegativePID(t *testing.T) {
|
|
// Negative PID is invalid. Must not panic.
|
|
assert.NotPanics(t, func() { pidAlive(-1) })
|
|
}
|
|
|
|
// --- SetCore ---
|
|
|
|
func TestLogic_SetCore_Good_RegistersIPCHandler(t *testing.T) {
|
|
c := core.New()
|
|
mon := New()
|
|
|
|
// SetCore must not panic and must wire ServiceRuntime.
|
|
assert.NotPanics(t, func() { mon.SetCore(c) })
|
|
assert.NotNil(t, mon.ServiceRuntime)
|
|
assert.Equal(t, c, mon.Core())
|
|
}
|
|
|
|
func TestLogic_SetCore_Good_IPCHandlerFires(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
// IPC handlers are registered via Register, not SetCore
|
|
c := core.New(core.WithService(Register))
|
|
|
|
mon, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
require.True(t, ok)
|
|
|
|
// Dispatch an AgentStarted via Core IPC — handler must update seenRunning.
|
|
c.ACTION(messages.AgentStarted{Agent: "codex", Repo: "go-io", Workspace: "ws-ipc"})
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenRunning["ws-ipc"])
|
|
}
|
|
|
|
func TestLogic_SetCore_Good_CompletedIPCHandler(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
// IPC handlers are registered via Register, not SetCore
|
|
c := core.New(core.WithService(Register))
|
|
|
|
mon, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
require.True(t, ok)
|
|
|
|
// Dispatch AgentCompleted — handler must update seenCompleted.
|
|
c.ACTION(messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "ws-done", Status: "completed"})
|
|
|
|
mon.mu.Lock()
|
|
defer mon.mu.Unlock()
|
|
assert.True(t, mon.seenCompleted["ws-done"])
|
|
}
|
|
|
|
// --- OnStartup / OnShutdown ---
|
|
|
|
func TestLogic_OnStartup_Good_StartsLoop(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
mon := New(Options{Interval: 1 * time.Hour})
|
|
r := mon.OnStartup(context.Background())
|
|
assert.True(t, r.OK)
|
|
|
|
// cancel must be non-nil after startup (loop running)
|
|
assert.NotNil(t, mon.cancel)
|
|
|
|
// Cleanup.
|
|
r2 := mon.OnShutdown(context.Background())
|
|
assert.True(t, r2.OK)
|
|
}
|
|
|
|
func TestLogic_OnStartup_Good_NoError(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
mon := New(Options{Interval: 1 * time.Hour})
|
|
assert.True(t, mon.OnStartup(context.Background()).OK)
|
|
_ = mon.OnShutdown(context.Background())
|
|
}
|
|
|
|
func TestLogic_OnShutdown_Good_NoError(t *testing.T) {
|
|
mon := New(Options{Interval: 1 * time.Hour})
|
|
assert.True(t, mon.OnShutdown(context.Background()).OK)
|
|
}
|
|
|
|
func TestLogic_OnShutdown_Good_StopsLoop(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
mon := New(Options{Interval: 1 * time.Hour})
|
|
require.True(t, mon.OnStartup(context.Background()).OK)
|
|
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
done <- mon.OnShutdown(context.Background()).OK
|
|
}()
|
|
|
|
select {
|
|
case ok := <-done:
|
|
assert.True(t, ok)
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("OnShutdown did not return in time")
|
|
}
|
|
}
|
|
|
|
func TestLogic_OnShutdown_Ugly_NilCancel(t *testing.T) {
|
|
// OnShutdown without prior OnStartup must not panic.
|
|
mon := New()
|
|
assert.NotPanics(t, func() {
|
|
_ = mon.OnShutdown(context.Background())
|
|
})
|
|
}
|
|
|
|
// --- Register ---
|
|
|
|
func TestLogic_Register_Good_ReturnsSubsystem(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
|
|
c := core.New(core.WithService(Register))
|
|
require.NotNil(t, c)
|
|
|
|
// Register returns the Subsystem as Value; WithService auto-registers it
|
|
// under the package name "monitor".
|
|
svc, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
assert.True(t, ok, "Subsystem must be registered as \"monitor\"")
|
|
assert.NotNil(t, svc)
|
|
}
|
|
|
|
func TestLogic_Register_Good_CoreWired(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
|
|
c := core.New(core.WithService(Register))
|
|
require.NotNil(t, c)
|
|
|
|
svc, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
require.True(t, ok)
|
|
|
|
// Register must set ServiceRuntime.
|
|
assert.NotNil(t, svc.ServiceRuntime)
|
|
assert.Equal(t, c, svc.Core())
|
|
}
|
|
|
|
func TestLogic_Register_Good_IPCHandlerActive(t *testing.T) {
|
|
wsRoot := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", wsRoot)
|
|
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
|
|
|
|
c := core.New(core.WithService(Register))
|
|
require.NotNil(t, c)
|
|
|
|
svc, ok := core.ServiceFor[*Subsystem](c, "monitor")
|
|
require.True(t, ok)
|
|
|
|
// Fire an AgentStarted message — the registered IPC handler must update seenRunning.
|
|
c.ACTION(messages.AgentStarted{Agent: "codex", Repo: "go-io", Workspace: "ws-reg"})
|
|
|
|
svc.mu.Lock()
|
|
defer svc.mu.Unlock()
|
|
assert.True(t, svc.seenRunning["ws-reg"])
|
|
}
|