// 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"]) }