agent/pkg/monitor/monitor_test.go
Snider 537226bd4d feat: AX v0.8.0 upgrade — Core features + quality gates
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>
2026-03-26 06:38:02 +00:00

900 lines
25 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package monitor
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"dappco.re/go/agent/pkg/messages"
core "dappco.re/go/core"
"dappco.re/go/core/process"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setupBrainKey creates a ~/.claude/brain.key file for API auth tests.
func setupBrainKey(t *testing.T, key string) {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
claudeDir := core.JoinPath(home, ".claude")
fs.EnsureDir(claudeDir)
fs.Write(core.JoinPath(claudeDir, "brain.key"), key)
}
// setupAPIEnv sets up brain key, CORE_API_URL, and AGENT_NAME for API tests.
func setupAPIEnv(t *testing.T, apiURL string) {
t.Helper()
setupBrainKey(t, "key")
t.Setenv("CORE_API_URL", apiURL)
t.Setenv("AGENT_NAME", "test-agent")
}
// writeWorkspaceStatus creates a workspace directory with a status.json file
// under the given root. Returns the workspace directory path.
func writeWorkspaceStatus(t *testing.T, wsRoot, name string, fields map[string]any) string {
t.Helper()
dir := core.JoinPath(wsRoot, "workspace", name)
fs.EnsureDir(dir)
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(fields))
return dir
}
// --- New ---
func TestMonitor_New_Good_Defaults(t *testing.T) {
t.Setenv("MONITOR_INTERVAL", "")
mon := New()
assert.Equal(t, 2*time.Minute, mon.interval)
assert.NotNil(t, mon.poke)
}
func TestMonitor_New_Good_CustomInterval(t *testing.T) {
mon := New(Options{Interval: 30 * time.Second})
assert.Equal(t, 30*time.Second, mon.interval)
}
func TestMonitor_New_Bad_ZeroInterval(t *testing.T) {
t.Setenv("MONITOR_INTERVAL", "")
mon := New(Options{Interval: 0})
assert.Equal(t, 2*time.Minute, mon.interval)
}
func TestMonitor_Name_Good(t *testing.T) {
mon := New()
assert.Equal(t, "monitor", mon.Name())
}
// --- Poke ---
func TestMonitor_Poke_Good(t *testing.T) {
mon := New()
mon.Poke()
select {
case <-mon.poke:
default:
t.Fatal("expected poke to send a value")
}
}
func TestMonitor_Poke_Good_NonBlocking(t *testing.T) {
mon := New()
mon.Poke()
mon.Poke() // second poke should be a no-op, not block
select {
case <-mon.poke:
default:
t.Fatal("expected at least one poke")
}
select {
case <-mon.poke:
t.Fatal("expected channel to be empty after drain")
default:
}
}
// --- Start / Shutdown ---
func TestMonitor_StartShutdown_Good(t *testing.T) {
mon := New(Options{Interval: 1 * time.Hour})
ctx := context.Background()
mon.Start(ctx)
err := mon.Shutdown(ctx)
assert.NoError(t, err)
}
func TestMonitor_Shutdown_Good_NilCancel(t *testing.T) {
mon := New()
err := mon.Shutdown(context.Background())
assert.NoError(t, err)
}
// --- checkCompletions ---
func TestMonitor_CheckCompletions_Good_NewCompletions(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
// Create Core with IPC handler to capture QueueDrained messages
var drainEvents []messages.QueueDrained
c := core.New()
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.QueueDrained); ok {
drainEvents = append(drainEvents, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
assert.Equal(t, "", mon.checkCompletions())
for i := 0; i < 2; i++ {
writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{
"status": "completed",
"repo": fmt.Sprintf("repo-%d", i),
"agent": "claude:sonnet",
})
}
msg := mon.checkCompletions()
assert.Contains(t, msg, "2 agent(s) completed")
// checkCompletions emits QueueDrained via c.ACTION() when running=0 and queued=0
require.Len(t, drainEvents, 1)
assert.Equal(t, 2, drainEvents[0].Completed)
}
func TestMonitor_CheckCompletions_Good_MixedStatuses(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
mon := New()
assert.Equal(t, "", mon.checkCompletions())
for i, status := range []string{"completed", "running", "queued"} {
writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{
"status": status,
"repo": fmt.Sprintf("repo-%d", i),
"agent": "claude:sonnet",
})
}
msg := mon.checkCompletions()
assert.Contains(t, msg, "1 agent(s) completed")
assert.Contains(t, msg, "1 still running")
assert.Contains(t, msg, "1 queued")
}
func TestMonitor_CheckCompletions_Good_NoNewCompletions(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
writeWorkspaceStatus(t, wsRoot, "ws-0", map[string]any{
"status": "completed", "repo": "r", "agent": "a",
})
mon := New()
mon.checkCompletions() // sets baseline
msg := mon.checkCompletions()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckCompletions_Good_EmptyWorkspace(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
mon := New()
msg := mon.checkCompletions()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckCompletions_Bad_InvalidJSON(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
dir := core.JoinPath(wsRoot, "workspace", "ws-bad")
fs.EnsureDir(dir)
fs.Write(core.JoinPath(dir, "status.json"), "not json")
mon := New()
msg := mon.checkCompletions()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckCompletions_Good_NilRuntime(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
mon := New()
assert.Equal(t, "", mon.checkCompletions())
writeWorkspaceStatus(t, wsRoot, "ws-0", map[string]any{
"status": "completed", "repo": "r", "agent": "a",
})
msg := mon.checkCompletions()
assert.Contains(t, msg, "1 agent(s) completed")
}
// --- checkInbox ---
func TestMonitor_CheckInbox_Good_UnreadMessages(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/messages/inbox", r.URL.Path)
assert.NotEmpty(t, r.URL.Query().Get("agent"))
resp := map[string]any{
"data": []map[string]any{
{"id": 3, "read": false, "from": "clotho", "subject": "task done"},
{"id": 2, "read": false, "from": "gemini", "subject": "review ready"},
{"id": 1, "read": true, "from": "clotho", "subject": "old msg"},
},
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupBrainKey(t, "test-key")
t.Setenv("CORE_API_URL", srv.URL)
t.Setenv("AGENT_NAME", "test-agent")
// Create Core with IPC handler to capture InboxMessage
var captured []messages.InboxMessage
c := core.New()
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.InboxMessage); ok {
captured = append(captured, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
mon.inboxSeeded = true
msg := mon.checkInbox()
assert.Contains(t, msg, "2 unread message(s) in inbox")
require.Len(t, captured, 1)
assert.Equal(t, 3, captured[0].New)
assert.Equal(t, 2, captured[0].Total)
}
func TestMonitor_CheckInbox_Good_NoUnread(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"data": []map[string]any{
{"id": 1, "read": true, "from": "clotho", "subject": "old"},
},
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
msg := mon.checkInbox()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckInbox_Good_SameCountNoRepeat(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"data": []map[string]any{
{"id": 1, "read": false, "from": "clotho", "subject": "msg"},
},
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
mon.checkInbox() // sets baseline
msg := mon.checkInbox()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckInbox_Bad_NoBrainKey(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
mon := New()
msg := mon.checkInbox()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckInbox_Bad_APIError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
msg := mon.checkInbox()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckInbox_Bad_InvalidJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("not json"))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
msg := mon.checkInbox()
assert.Equal(t, "", msg)
}
func TestMonitor_CheckInbox_Good_MultipleSameSender(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"data": []map[string]any{
{"id": 3, "read": false, "from": "clotho", "subject": "msg1"},
{"id": 2, "read": false, "from": "clotho", "subject": "msg2"},
{"id": 1, "read": false, "from": "gemini", "subject": "msg3"},
},
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
// Create Core with IPC handler to capture InboxMessage
var captured []messages.InboxMessage
c := core.New()
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.InboxMessage); ok {
captured = append(captured, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
mon.inboxSeeded = true
msg := mon.checkInbox()
assert.Contains(t, msg, "3 unread message(s)")
require.Len(t, captured, 1)
assert.Equal(t, 3, captured[0].New)
assert.Equal(t, 3, captured[0].Total)
}
// --- check (integration of sub-checks) ---
func TestMonitor_Check_Good_CombinesMessages(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
writeWorkspaceStatus(t, wsRoot, "ws-0", map[string]any{
"status": "completed", "repo": "r", "agent": "a",
})
home := t.TempDir()
t.Setenv("HOME", home)
mon := New()
mon.check(context.Background())
mon.mu.Lock()
assert.True(t, mon.completionsSeeded)
assert.True(t, mon.seenCompleted["ws-0"])
mon.mu.Unlock()
}
func TestMonitor_Check_Good_NoMessages(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()
mon.check(context.Background())
}
// --- notify ---
func TestMonitor_Notify_Good_NilServer(t *testing.T) {
mon := New()
mon.notify(context.Background(), "test message")
}
// --- loop ---
func TestMonitor_Loop_Good_ImmediateCancel(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})
ctx, cancel := context.WithCancel(context.Background())
cancel()
done := make(chan struct{})
go func() {
mon.loop(ctx)
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("loop did not exit after context cancellation")
}
}
func TestMonitor_Loop_Good_PokeTriggersCheck(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})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mon.wg.Add(1)
go func() {
defer mon.wg.Done()
mon.loop(ctx)
}()
// Wait for initial delay (5s) + first check + scheduler overhead
time.Sleep(7 * time.Second)
writeWorkspaceStatus(t, wsRoot, "ws-poke", map[string]any{
"status": "completed", "repo": "poke-repo", "agent": "a",
})
mon.Poke()
// Poll until the poke-triggered check updates the count
require.Eventually(t, func() bool {
mon.mu.Lock()
defer mon.mu.Unlock()
return mon.seenCompleted["ws-poke"]
}, 5*time.Second, 50*time.Millisecond, "expected ws-poke completion to be recorded")
cancel()
mon.wg.Wait()
}
// --- initSyncTimestamp ---
func TestMonitor_InitSyncTimestamp_Good(t *testing.T) {
mon := New()
assert.Equal(t, int64(0), mon.lastSyncTimestamp)
before := time.Now().Unix()
mon.initSyncTimestamp()
after := time.Now().Unix()
mon.mu.Lock()
ts := mon.lastSyncTimestamp
mon.mu.Unlock()
assert.GreaterOrEqual(t, ts, before)
assert.LessOrEqual(t, ts, after)
}
func TestMonitor_InitSyncTimestamp_Good_NoOverwrite(t *testing.T) {
mon := New()
mon.lastSyncTimestamp = 12345
mon.initSyncTimestamp()
mon.mu.Lock()
assert.Equal(t, int64(12345), mon.lastSyncTimestamp)
mon.mu.Unlock()
}
// --- syncRepos ---
func TestMonitor_SyncRepos_Good_NoChanges(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/agent/checkin", r.URL.Path)
resp := CheckinResponse{Timestamp: time.Now().Unix()}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
msg := mon.syncRepos()
assert.Equal(t, "", msg)
}
func TestMonitor_SyncRepos_Bad_APIError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
msg := mon.syncRepos()
assert.Equal(t, "", msg)
}
func TestMonitor_SyncRepos_Good_UpdatesTimestamp(t *testing.T) {
newTS := time.Now().Unix() + 1000
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := CheckinResponse{Timestamp: newTS}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
mon := New()
mon.syncRepos()
mon.mu.Lock()
assert.Equal(t, newTS, mon.lastSyncTimestamp)
mon.mu.Unlock()
}
// --- agentStatusResource ---
func TestMonitor_AgentStatusResource_Good(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
for i, status := range []string{"completed", "running"} {
writeWorkspaceStatus(t, wsRoot, fmt.Sprintf("ws-%d", i), map[string]any{
"status": status,
"repo": fmt.Sprintf("repo-%d", i),
"agent": "claude:sonnet",
})
}
mon := New()
result, err := mon.agentStatusResource(context.Background(), &mcp.ReadResourceRequest{})
require.NoError(t, err)
require.Len(t, result.Contents, 1)
assert.Equal(t, "status://agents", result.Contents[0].URI)
var workspaces []map[string]any
require.True(t, core.JSONUnmarshalString(result.Contents[0].Text, &workspaces).OK)
assert.Len(t, workspaces, 2)
}
func TestMonitor_AgentStatusResource_Good_Empty(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
mon := New()
result, err := mon.agentStatusResource(context.Background(), &mcp.ReadResourceRequest{})
require.NoError(t, err)
require.Len(t, result.Contents, 1)
assert.Equal(t, "null", result.Contents[0].Text)
}
func TestMonitor_AgentStatusResource_Bad_InvalidJSON(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
dir := core.JoinPath(wsRoot, "workspace", "ws-bad")
fs.EnsureDir(dir)
fs.Write(core.JoinPath(dir, "status.json"), "bad")
mon := New()
result, err := mon.agentStatusResource(context.Background(), &mcp.ReadResourceRequest{})
require.NoError(t, err)
assert.Equal(t, "null", result.Contents[0].Text)
}
// --- syncRepos (git pull path) ---
func TestMonitor_SyncRepos_Good_PullsChangedRepo(t *testing.T) {
remoteDir := core.JoinPath(t.TempDir(), "remote")
fs.EnsureDir(remoteDir)
run(t, remoteDir, "git", "init", "--bare")
codeDir := t.TempDir()
repoDir := core.JoinPath(codeDir, "test-repo")
run(t, codeDir, "git", "clone", remoteDir, "test-repo")
run(t, repoDir, "git", "checkout", "-b", "main")
fs.Write(core.JoinPath(repoDir, "README.md"), "# test")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "init")
run(t, repoDir, "git", "push", "-u", "origin", "main")
// Simulate another agent pushing work via a second clone
clone2Parent := t.TempDir()
tmpClone := core.JoinPath(clone2Parent, "clone2")
run(t, clone2Parent, "git", "clone", remoteDir, "clone2")
run(t, tmpClone, "git", "checkout", "main")
fs.Write(core.JoinPath(tmpClone, "new.go"), "package main\n")
run(t, tmpClone, "git", "add", ".")
run(t, tmpClone, "git", "commit", "-m", "agent work")
run(t, tmpClone, "git", "push", "origin", "main")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := CheckinResponse{
Changed: []ChangedRepo{{Repo: "test-repo", Branch: "main", SHA: "abc"}},
Timestamp: time.Now().Unix() + 100,
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
t.Setenv("CODE_PATH", codeDir)
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
msg := mon.syncRepos()
assert.Contains(t, msg, "Synced 1 repo(s)")
assert.Contains(t, msg, "test-repo")
}
func TestMonitor_SyncRepos_Good_SkipsDirtyRepo(t *testing.T) {
remoteDir := core.JoinPath(t.TempDir(), "remote")
fs.EnsureDir(remoteDir)
run(t, remoteDir, "git", "init", "--bare")
codeDir := t.TempDir()
repoDir := core.JoinPath(codeDir, "dirty-repo")
run(t, codeDir, "git", "clone", remoteDir, "dirty-repo")
run(t, repoDir, "git", "checkout", "-b", "main")
fs.Write(core.JoinPath(repoDir, "README.md"), "# test")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "init")
run(t, repoDir, "git", "push", "-u", "origin", "main")
// Make the repo dirty
fs.Write(core.JoinPath(repoDir, "dirty.txt"), "uncommitted")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := CheckinResponse{
Changed: []ChangedRepo{{Repo: "dirty-repo", Branch: "main", SHA: "abc"}},
Timestamp: time.Now().Unix() + 100,
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
t.Setenv("CODE_PATH", codeDir)
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
msg := mon.syncRepos()
assert.Equal(t, "", msg)
}
func TestMonitor_SyncRepos_Good_SkipsNonMainBranch(t *testing.T) {
remoteDir := core.JoinPath(t.TempDir(), "remote")
fs.EnsureDir(remoteDir)
run(t, remoteDir, "git", "init", "--bare")
codeDir := t.TempDir()
repoDir := core.JoinPath(codeDir, "feature-repo")
run(t, codeDir, "git", "clone", remoteDir, "feature-repo")
run(t, repoDir, "git", "checkout", "-b", "main")
fs.Write(core.JoinPath(repoDir, "README.md"), "# test")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "init")
run(t, repoDir, "git", "push", "-u", "origin", "main")
run(t, repoDir, "git", "checkout", "-b", "feature/wip")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := CheckinResponse{
Changed: []ChangedRepo{{Repo: "feature-repo", Branch: "main", SHA: "abc"}},
Timestamp: time.Now().Unix() + 100,
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
t.Setenv("CODE_PATH", codeDir)
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
msg := mon.syncRepos()
assert.Equal(t, "", msg)
}
func TestMonitor_SyncRepos_Good_SkipsNonexistentRepo(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := CheckinResponse{
Changed: []ChangedRepo{{Repo: "nonexistent", Branch: "main", SHA: "abc"}},
Timestamp: time.Now().Unix() + 100,
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
setupAPIEnv(t, srv.URL)
t.Setenv("CODE_PATH", t.TempDir())
mon := New()
msg := mon.syncRepos()
assert.Equal(t, "", msg)
}
func TestMonitor_SyncRepos_Good_UsesEnvBrainKey(t *testing.T) {
var authHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader = r.Header.Get("Authorization")
resp := CheckinResponse{Timestamp: time.Now().Unix()}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(core.JSONMarshalString(resp)))
}))
defer srv.Close()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("CORE_BRAIN_KEY", "env-key-value")
t.Setenv("CORE_API_URL", srv.URL)
t.Setenv("AGENT_NAME", "test-agent")
mon := New()
mon.syncRepos()
assert.Equal(t, "Bearer env-key-value", authHeader)
}
// --- harvestCompleted (full path) ---
func TestMonitor_HarvestCompleted_Good_MultipleWorkspaces(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
for i := 0; i < 2; i++ {
name := fmt.Sprintf("ws-%d", i)
wsDir := core.JoinPath(wsRoot, "workspace", name)
sourceDir := core.JoinPath(wsRoot, fmt.Sprintf("source-%d", i))
fs.EnsureDir(sourceDir)
run(t, sourceDir, "git", "init")
run(t, sourceDir, "git", "checkout", "-b", "main")
fs.Write(core.JoinPath(sourceDir, "README.md"), "# test")
run(t, sourceDir, "git", "add", ".")
run(t, sourceDir, "git", "commit", "-m", "init")
fs.EnsureDir(wsDir)
run(t, wsDir, "git", "clone", sourceDir, "src")
srcDir := core.JoinPath(wsDir, "src")
run(t, srcDir, "git", "checkout", "-b", "agent/test-task")
fs.Write(core.JoinPath(srcDir, "new.go"), "package main\n")
run(t, srcDir, "git", "add", ".")
run(t, srcDir, "git", "commit", "-m", "agent work")
writeStatus(t, wsDir, "completed", fmt.Sprintf("repo-%d", i), "agent/test-task")
}
// Create Core with IPC handler to capture HarvestComplete messages
var harvests []messages.HarvestComplete
c := core.New(core.WithService(process.Register))
c.ServiceStartup(context.Background(), nil)
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.HarvestComplete); ok {
harvests = append(harvests, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
msg := mon.harvestCompleted()
assert.Contains(t, msg, "Harvested:")
assert.Contains(t, msg, "repo-0")
assert.Contains(t, msg, "repo-1")
assert.GreaterOrEqual(t, len(harvests), 2)
}
func TestMonitor_HarvestCompleted_Good_Empty(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
fs.EnsureDir(core.JoinPath(wsRoot, "workspace"))
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
msg := mon.harvestCompleted()
assert.Equal(t, "", msg)
}
func TestMonitor_HarvestCompleted_Good_RejectedWorkspace(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
sourceDir := core.JoinPath(wsRoot, "source-rej")
fs.EnsureDir(sourceDir)
run(t, sourceDir, "git", "init")
run(t, sourceDir, "git", "checkout", "-b", "main")
fs.Write(core.JoinPath(sourceDir, "README.md"), "# test")
run(t, sourceDir, "git", "add", ".")
run(t, sourceDir, "git", "commit", "-m", "init")
wsDir := core.JoinPath(wsRoot, "workspace", "ws-rej")
fs.EnsureDir(wsDir)
run(t, wsDir, "git", "clone", sourceDir, "src")
srcDir := core.JoinPath(wsDir, "src")
run(t, srcDir, "git", "checkout", "-b", "agent/test-task")
fs.Write(core.JoinPath(srcDir, "new.go"), "package main\n")
run(t, srcDir, "git", "add", ".")
run(t, srcDir, "git", "commit", "-m", "agent work")
// Add binary to trigger rejection
fs.Write(core.JoinPath(srcDir, "app.exe"), "binary")
run(t, srcDir, "git", "add", ".")
run(t, srcDir, "git", "commit", "-m", "add binary")
writeStatus(t, wsDir, "completed", "rej-repo", "agent/test-task")
// Create Core with IPC handler to capture HarvestRejected messages
var rejections []messages.HarvestRejected
c := core.New(core.WithService(process.Register))
c.ServiceStartup(context.Background(), nil)
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.HarvestRejected); ok {
rejections = append(rejections, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{})
msg := mon.harvestCompleted()
assert.Contains(t, msg, "REJECTED")
require.Len(t, rejections, 1)
assert.Contains(t, rejections[0].Reason, "binary file added")
}