agent/pkg/monitor/harvest_test.go
Virgil 1db52ab1de fix(monitor): unify runtime options type
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 20:00:57 +00:00

366 lines
11 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package monitor
import (
"context"
"fmt"
"testing"
"dappco.re/go/agent/pkg/agentic"
"dappco.re/go/agent/pkg/messages"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// initTestRepo creates a bare git repo and a workspace clone with a branch.
func initTestRepo(t *testing.T) (sourceDir, wsDir string) {
t.Helper()
// Create bare "source" repo
sourceDir = core.JoinPath(t.TempDir(), "source")
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")
// Create workspace dir with repo/ clone
wsDir = core.JoinPath(t.TempDir(), "workspace")
repoDir := core.JoinPath(wsDir, "repo")
fs.EnsureDir(wsDir)
run(t, wsDir, "git", "clone", sourceDir, "repo")
// Create agent branch with a commit
run(t, repoDir, "git", "checkout", "-b", "agent/test-task")
fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "agent work")
return sourceDir, wsDir
}
func run(t *testing.T, dir string, name string, args ...string) {
t.Helper()
gitEnv := []string{"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@test", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@test"}
r := testMon.Core().Process().RunWithEnv(context.Background(), dir, gitEnv, name, args...)
require.True(t, r.OK, "command %s %v failed: %s", name, args, r.Value)
}
func writeStatus(t *testing.T, wsDir, status, repo, branch string) {
t.Helper()
st := map[string]any{
"status": status,
"repo": repo,
"branch": branch,
}
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(st))
}
// --- Tests ---
func TestHarvest_DetectBranch_Good(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
branch := testMon.detectBranch(repoDir)
assert.Equal(t, "agent/test-task", branch)
}
func TestHarvest_DetectBranch_Bad_NoRepo(t *testing.T) {
branch := testMon.detectBranch(t.TempDir())
assert.Equal(t, "", branch)
}
func TestHarvest_CountUnpushed_Good(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
count := testMon.countUnpushed(repoDir, "agent/test-task")
assert.Equal(t, 1, count)
}
func TestHarvest_CountChangedFiles_Good(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
count := testMon.countChangedFiles(repoDir)
assert.Equal(t, 1, count)
}
func TestHarvest_CheckSafety_Good_CleanWorkspace(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
reason := testMon.checkSafety(repoDir)
assert.Equal(t, "", reason)
}
func TestHarvest_CheckSafety_Bad_BinaryFile(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
// Add a binary file
fs.Write(core.JoinPath(repoDir, "app.exe"), "binary")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "add binary")
reason := testMon.checkSafety(repoDir)
assert.Contains(t, reason, "binary file added")
assert.Contains(t, reason, "app.exe")
}
func TestHarvest_CheckSafety_Bad_LargeFile(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
// Add a file > 1MB
bigData := make([]byte, 1024*1024+1)
fs.Write(core.JoinPath(repoDir, "huge.txt"), string(bigData))
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "add large file")
reason := testMon.checkSafety(repoDir)
assert.Contains(t, reason, "large file")
assert.Contains(t, reason, "huge.txt")
}
func TestHarvest_UpdateStatus_Bad_WriteFailure(t *testing.T) {
assert.NotPanics(t, func() {
updateStatus("/dev/null/impossible", "ready-for-review", "")
})
}
func TestHarvest_HarvestWorkspace_Good(t *testing.T) {
_, wsDir := initTestRepo(t)
writeStatus(t, wsDir, "completed", "test-repo", "agent/test-task")
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
result := mon.harvestWorkspace(wsDir)
require.NotNil(t, result)
assert.Equal(t, "test-repo", result.repo)
assert.Equal(t, "agent/test-task", result.branch)
assert.Equal(t, 1, result.files)
assert.Equal(t, "", result.rejected)
// Verify status updated
var st map[string]any
require.True(t, core.JSONUnmarshalString(fs.Read(core.JoinPath(wsDir, "status.json")).Value.(string), &st).OK)
assert.Equal(t, "ready-for-review", st["status"])
}
func TestHarvest_HarvestWorkspace_Bad_NotCompleted(t *testing.T) {
_, wsDir := initTestRepo(t)
writeStatus(t, wsDir, "running", "test-repo", "agent/test-task")
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
result := mon.harvestWorkspace(wsDir)
assert.Nil(t, result)
}
func TestHarvest_HarvestWorkspace_Bad_MainBranch(t *testing.T) {
_, wsDir := initTestRepo(t)
// Switch back to main
repoDir := core.JoinPath(wsDir, "repo")
run(t, repoDir, "git", "checkout", "main")
writeStatus(t, wsDir, "completed", "test-repo", "main")
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
result := mon.harvestWorkspace(wsDir)
assert.Nil(t, result)
}
func TestHarvest_HarvestWorkspace_Bad_BinaryRejected(t *testing.T) {
_, wsDir := initTestRepo(t)
repoDir := core.JoinPath(wsDir, "repo")
// Add binary
fs.Write(core.JoinPath(repoDir, "build.so"), "elf")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "add binary")
writeStatus(t, wsDir, "completed", "test-repo", "agent/test-task")
mon := New()
mon.ServiceRuntime = testMon.ServiceRuntime
result := mon.harvestWorkspace(wsDir)
require.NotNil(t, result)
assert.Contains(t, result.rejected, "binary file added")
// Verify status set to rejected
var st map[string]any
core.JSONUnmarshalString(fs.Read(core.JoinPath(wsDir, "status.json")).Value.(string), &st)
assert.Equal(t, "rejected", st["status"])
}
func TestHarvest_HarvestCompleted_Good_ChannelEvents(t *testing.T) {
_, wsDir := initTestRepo(t)
writeStatus(t, wsDir, "completed", "test-repo", "agent/test-task")
// Create a Core with process + IPC handler to capture HarvestComplete messages
var captured []messages.HarvestComplete
c := core.New(core.WithService(agentic.ProcessRegister))
c.ServiceStartup(context.Background(), nil)
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(messages.HarvestComplete); ok {
captured = append(captured, ev)
}
return core.Result{OK: true}
})
mon := New()
mon.ServiceRuntime = core.NewServiceRuntime(c, Options{})
// Call harvestWorkspace directly since harvestCompleted uses agentic.WorkspaceRoot()
result := mon.harvestWorkspace(wsDir)
require.NotNil(t, result)
assert.Equal(t, "", result.rejected)
// Simulate what harvestCompleted does with the result — emit IPC
mon.Core().ACTION(messages.HarvestComplete{Repo: result.repo, Branch: result.branch, Files: result.files})
require.Len(t, captured, 1)
assert.Equal(t, "test-repo", captured[0].Repo)
assert.Equal(t, "agent/test-task", captured[0].Branch)
assert.Equal(t, 1, captured[0].Files)
}
func TestHarvest_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, "repo")
repoDir := core.JoinPath(wsDir, "repo")
run(t, repoDir, "git", "checkout", "-b", "agent/test-task")
fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "agent work")
writeStatus(t, wsDir, "completed", fmt.Sprintf("repo-%d", i), "agent/test-task")
}
var harvests []messages.HarvestComplete
c := core.New(core.WithService(agentic.ProcessRegister))
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, Options{})
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 TestHarvest_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 TestHarvest_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, "repo")
repoDir := core.JoinPath(wsDir, "repo")
run(t, repoDir, "git", "checkout", "-b", "agent/test-task")
fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "agent work")
fs.Write(core.JoinPath(repoDir, "app.exe"), "binary")
run(t, repoDir, "git", "add", ".")
run(t, repoDir, "git", "commit", "-m", "add binary")
writeStatus(t, wsDir, "completed", "rej-repo", "agent/test-task")
var rejections []messages.HarvestRejected
c := core.New(core.WithService(agentic.ProcessRegister))
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, Options{})
msg := mon.harvestCompleted()
assert.Contains(t, msg, "REJECTED")
require.Len(t, rejections, 1)
assert.Contains(t, rejections[0].Reason, "binary file added")
}
func TestHarvest_UpdateStatus_Good(t *testing.T) {
dir := t.TempDir()
initial := map[string]any{"status": "completed", "repo": "test"}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(initial))
updateStatus(dir, "ready-for-review", "")
var st map[string]any
core.JSONUnmarshalString(fs.Read(core.JoinPath(dir, "status.json")).Value.(string), &st)
assert.Equal(t, "ready-for-review", st["status"])
}
func TestHarvest_UpdateStatus_Good_WithQuestion(t *testing.T) {
dir := t.TempDir()
initial := map[string]any{"status": "completed", "repo": "test"}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(initial))
updateStatus(dir, "rejected", "binary file: app.exe")
var st map[string]any
core.JSONUnmarshalString(fs.Read(core.JoinPath(dir, "status.json")).Value.(string), &st)
assert.Equal(t, "rejected", st["status"])
assert.Equal(t, "binary file: app.exe", st["question"])
}