test: 282 new tests — agentic 44.1%, monitor 84.2% #18

Merged
Virgil merged 12 commits from feat/core-di-migration into dev 2026-03-25 00:00:02 +00:00
15 changed files with 4709 additions and 1 deletions

2
go.mod
View file

@ -3,7 +3,7 @@ module dappco.re/go/agent
go 1.26.0
require (
dappco.re/go/core v0.6.0
dappco.re/go/core v0.7.0
dappco.re/go/core/api v0.2.0
dappco.re/go/core/process v0.3.0
dappco.re/go/core/ws v0.3.0

View file

@ -0,0 +1,301 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os/exec"
"path/filepath"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- dispatch (validation) ---
func TestDispatch_Bad_NoRepo(t *testing.T) {
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.dispatch(context.Background(), nil, DispatchInput{
Task: "Fix the bug",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo is required")
}
func TestDispatch_Bad_NoTask(t *testing.T) {
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.dispatch(context.Background(), nil, DispatchInput{
Repo: "go-io",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "task is required")
}
func TestDispatch_Good_DefaultsApplied(t *testing.T) {
// We can't test full dispatch without Docker, but we can verify defaults
// by using DryRun and checking the workspace prep
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Mock forge server for issue fetching
forgeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"title": "Test issue",
"body": "Fix the thing",
})
}))
t.Cleanup(forgeSrv.Close)
// Create source repo to clone from
srcRepo := filepath.Join(t.TempDir(), "core", "go-io")
require.NoError(t, exec.Command("git", "init", "-b", "main", srcRepo).Run())
gitCmd := exec.Command("git", "config", "user.name", "Test")
gitCmd.Dir = srcRepo
gitCmd.Run()
gitCmd = exec.Command("git", "config", "user.email", "test@test.com")
gitCmd.Dir = srcRepo
gitCmd.Run()
require.True(t, fs.Write(filepath.Join(srcRepo, "go.mod"), "module test\n\ngo 1.22").OK)
gitCmd = exec.Command("git", "add", ".")
gitCmd.Dir = srcRepo
gitCmd.Run()
gitCmd = exec.Command("git", "commit", "-m", "init")
gitCmd.Dir = srcRepo
gitCmd.Env = append(gitCmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
gitCmd.Run()
s := &PrepSubsystem{
forge: forge.NewForge(forgeSrv.URL, "test-token"),
codePath: filepath.Dir(filepath.Dir(srcRepo)), // parent of core/go-io
client: forgeSrv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.dispatch(context.Background(), nil, DispatchInput{
Repo: "go-io",
Task: "Fix stuff",
Issue: 42,
DryRun: true,
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Equal(t, "codex", out.Agent) // default agent
assert.Equal(t, "go-io", out.Repo)
assert.NotEmpty(t, out.WorkspaceDir)
assert.NotEmpty(t, out.Prompt)
}
// --- runQA ---
func TestRunQA_Good_GoProject(t *testing.T) {
// Create a minimal valid Go project
wsDir := t.TempDir()
repoDir := filepath.Join(wsDir, "repo")
require.True(t, fs.EnsureDir(repoDir).OK)
require.True(t, fs.Write(filepath.Join(repoDir, "go.mod"), "module testmod\n\ngo 1.22\n").OK)
require.True(t, fs.Write(filepath.Join(repoDir, "main.go"), "package main\n\nfunc main() {}\n").OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// go build, go vet, go test should all pass on this minimal project
result := s.runQA(wsDir)
assert.True(t, result)
}
func TestRunQA_Bad_GoBrokenCode(t *testing.T) {
wsDir := t.TempDir()
repoDir := filepath.Join(wsDir, "repo")
require.True(t, fs.EnsureDir(repoDir).OK)
require.True(t, fs.Write(filepath.Join(repoDir, "go.mod"), "module testmod\n\ngo 1.22\n").OK)
// Deliberately broken Go code — won't compile
require.True(t, fs.Write(filepath.Join(repoDir, "main.go"), "package main\n\nfunc main( {\n}\n").OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runQA(wsDir)
assert.False(t, result)
}
func TestRunQA_Good_UnknownLanguage(t *testing.T) {
// No go.mod, composer.json, or package.json → passes QA (no checks)
wsDir := t.TempDir()
repoDir := filepath.Join(wsDir, "repo")
require.True(t, fs.EnsureDir(repoDir).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runQA(wsDir)
assert.True(t, result)
}
func TestRunQA_Good_GoVetFailure(t *testing.T) {
wsDir := t.TempDir()
repoDir := filepath.Join(wsDir, "repo")
require.True(t, fs.EnsureDir(repoDir).OK)
require.True(t, fs.Write(filepath.Join(repoDir, "go.mod"), "module testmod\n\ngo 1.22\n").OK)
// Code that compiles but has a vet issue (unreachable code after return)
code := `package main
import "fmt"
func main() {
fmt.Printf("%d", "not a number")
}
`
require.True(t, fs.Write(filepath.Join(repoDir, "main.go"), code).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runQA(wsDir)
// go vet should catch the Printf format mismatch
assert.False(t, result)
}
// --- workspaceDir ---
func TestWorkspaceDir_Good_Issue(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
dir, err := workspaceDir("core", "go-io", PrepInput{Issue: 42})
require.NoError(t, err)
assert.Contains(t, dir, "task-42")
}
func TestWorkspaceDir_Good_PR(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
dir, err := workspaceDir("core", "go-io", PrepInput{PR: 7})
require.NoError(t, err)
assert.Contains(t, dir, "pr-7")
}
func TestWorkspaceDir_Good_Branch(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
dir, err := workspaceDir("core", "go-io", PrepInput{Branch: "feature/new-api"})
require.NoError(t, err)
assert.Contains(t, dir, "feature/new-api")
}
func TestWorkspaceDir_Good_Tag(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
dir, err := workspaceDir("core", "go-io", PrepInput{Tag: "v1.0.0"})
require.NoError(t, err)
assert.Contains(t, dir, "v1.0.0")
}
func TestWorkspaceDir_Bad_NoIdentifier(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
_, err := workspaceDir("core", "go-io", PrepInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "one of issue, pr, branch, or tag is required")
}
// --- DispatchInput defaults ---
func TestDispatchInput_Good_Defaults(t *testing.T) {
input := DispatchInput{
Repo: "go-io",
Task: "Fix it",
}
// Verify default values are empty until dispatch applies them
assert.Empty(t, input.Org)
assert.Empty(t, input.Agent)
assert.Empty(t, input.Template)
}
// --- buildPRBody ---
func TestBuildPRBody_Good_AllFields(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "Implement new feature",
Agent: "claude",
Issue: 15,
Branch: "agent/implement-new-feature",
Runs: 3,
}
body := s.buildPRBody(st)
assert.Contains(t, body, "Implement new feature")
assert.Contains(t, body, "Closes #15")
assert.Contains(t, body, "**Agent:** claude")
assert.Contains(t, body, "**Runs:** 3")
}
func TestBuildPRBody_Good_NoIssue(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "Refactor internals",
Agent: "codex",
Runs: 1,
}
body := s.buildPRBody(st)
assert.Contains(t, body, "Refactor internals")
assert.NotContains(t, body, "Closes #")
}
func TestBuildPRBody_Bad_EmptyStatus(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{}
body := s.buildPRBody(st)
// Should still produce valid markdown, just with empty fields
assert.Contains(t, body, "## Summary")
}
// --- canDispatchAgent ---
func TestCanDispatchAgent_Good_NoLimitsConfigured(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
codePath: t.TempDir(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// No config, no running agents — should allow dispatch
assert.True(t, s.canDispatchAgent("claude"))
}

393
pkg/agentic/epic_test.go Normal file
View file

@ -0,0 +1,393 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockForgeServer creates an httptest server that handles Forge API calls
// for issues and labels. Returns the server and a counter of issues created.
func mockForgeServer(t *testing.T) (*httptest.Server, *atomic.Int32) {
t.Helper()
issueCounter := &atomic.Int32{}
mux := http.NewServeMux()
// Create issue
mux.HandleFunc("/api/v1/repos/", func(w http.ResponseWriter, r *http.Request) {
// Route based on method + path suffix
if r.Method == "POST" && pathEndsWith(r.URL.Path, "/issues") {
num := int(issueCounter.Add(1))
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"number": num,
"html_url": "https://forge.test/core/test-repo/issues/" + itoa(num),
})
return
}
// Create/list labels
if pathEndsWith(r.URL.Path, "/labels") {
if r.Method == "GET" {
json.NewEncoder(w).Encode([]map[string]any{
{"id": 1, "name": "agentic"},
{"id": 2, "name": "bug"},
})
return
}
if r.Method == "POST" {
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"id": issueCounter.Load() + 100,
})
return
}
}
// List issues (for scan)
if r.Method == "GET" && pathEndsWith(r.URL.Path, "/issues") {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 1,
"title": "Test issue",
"labels": []map[string]any{{"name": "agentic"}},
"assignee": nil,
"html_url": "https://forge.test/core/test-repo/issues/1",
},
})
return
}
// Issue labels (for verify)
if r.Method == "POST" && containsStr(r.URL.Path, "/labels") {
w.WriteHeader(200)
return
}
// PR merge
if r.Method == "POST" && containsStr(r.URL.Path, "/merge") {
w.WriteHeader(200)
return
}
// Issue comments
if r.Method == "POST" && containsStr(r.URL.Path, "/comments") {
w.WriteHeader(201)
return
}
w.WriteHeader(404)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv, issueCounter
}
func pathEndsWith(path, suffix string) bool {
if len(path) < len(suffix) {
return false
}
return path[len(path)-len(suffix):] == suffix
}
func containsStr(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
func itoa(n int) string {
if n == 0 {
return "0"
}
digits := make([]byte, 0, 10)
for n > 0 {
digits = append([]byte{byte('0' + n%10)}, digits...)
n /= 10
}
return string(digits)
}
// newTestSubsystem creates a PrepSubsystem wired to a mock Forge server.
func newTestSubsystem(t *testing.T, srv *httptest.Server) *PrepSubsystem {
t.Helper()
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
brainURL: srv.URL,
brainKey: "test-brain-key",
codePath: t.TempDir(),
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
return s
}
// --- createIssue ---
func TestCreateIssue_Good_Success(t *testing.T) {
srv, counter := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "Fix the bug", "Description", []int64{1})
require.NoError(t, err)
assert.Equal(t, 1, child.Number)
assert.Equal(t, "Fix the bug", child.Title)
assert.Contains(t, child.URL, "issues/1")
assert.Equal(t, int32(1), counter.Load())
}
func TestCreateIssue_Good_NoLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "No labels task", "", nil)
require.NoError(t, err)
assert.Equal(t, "No labels task", child.Title)
}
func TestCreateIssue_Good_WithBody(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "Task with body", "Detailed description", []int64{1, 2})
require.NoError(t, err)
assert.NotZero(t, child.Number)
}
func TestCreateIssue_Bad_ServerDown(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close() // immediately close
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.createIssue(context.Background(), "core", "test-repo", "Title", "", nil)
assert.Error(t, err)
}
func TestCreateIssue_Bad_Non201Response(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.createIssue(context.Background(), "core", "test-repo", "Title", "", nil)
assert.Error(t, err)
}
// --- resolveLabelIDs ---
func TestResolveLabelIDs_Good_ExistingLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"agentic", "bug"})
assert.Len(t, ids, 2)
assert.Contains(t, ids, int64(1))
assert.Contains(t, ids, int64(2))
}
func TestResolveLabelIDs_Good_NewLabel(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// "new-label" doesn't exist in mock, so it will be created
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"new-label"})
assert.NotEmpty(t, ids)
}
func TestResolveLabelIDs_Good_EmptyNames(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", nil)
assert.Nil(t, ids)
}
func TestResolveLabelIDs_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"agentic"})
assert.Nil(t, ids)
}
// --- createLabel ---
func TestCreateLabel_Good_Known(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
id := s.createLabel(context.Background(), "core", "test-repo", "agentic")
assert.NotZero(t, id)
}
func TestCreateLabel_Good_Unknown(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// Unknown label uses default colour
id := s.createLabel(context.Background(), "core", "test-repo", "custom-label")
assert.NotZero(t, id)
}
func TestCreateLabel_Bad_ServerDown(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close()
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
id := s.createLabel(context.Background(), "core", "test-repo", "agentic")
assert.Zero(t, id)
}
// --- createEpic (validation only, not full dispatch) ---
func TestCreateEpic_Bad_NoTitle(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Tasks: []string{"Task 1"},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "title is required")
}
func TestCreateEpic_Bad_NoTasks(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Epic Title",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one task")
}
func TestCreateEpic_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Epic",
Tasks: []string{"Task"},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
func TestCreateEpic_Good_WithTasks(t *testing.T) {
srv, counter := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Test Epic",
Tasks: []string{"Task 1", "Task 2"},
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.NotZero(t, out.EpicNumber)
assert.Len(t, out.Children, 2)
assert.Equal(t, "Task 1", out.Children[0].Title)
assert.Equal(t, "Task 2", out.Children[1].Title)
// 2 children + 1 epic = 3 issues
assert.Equal(t, int32(3), counter.Load())
}
func TestCreateEpic_Good_WithLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Labelled Epic",
Tasks: []string{"Do it"},
Labels: []string{"bug"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestCreateEpic_Good_AgenticLabelAutoAdded(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// No labels specified — "agentic" should be auto-added
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Auto-labelled",
Tasks: []string{"Task"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestCreateEpic_Good_AgenticLabelNotDuplicated(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// agentic already present — should not be duplicated
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "With agentic",
Tasks: []string{"Task"},
Labels: []string{"agentic"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}

285
pkg/agentic/ingest_test.go Normal file
View file

@ -0,0 +1,285 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- ingestFindings ---
func TestIngestFindings_Good_WithFindings(t *testing.T) {
// Track the issue creation call
issueCalled := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && containsStr(r.URL.Path, "/issues") {
issueCalled = true
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
assert.Contains(t, body["title"], "Scan findings")
w.WriteHeader(201)
return
}
w.WriteHeader(200)
}))
t.Cleanup(srv.Close)
// Create a workspace with status and log file
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Agent: "codex",
}))
// Write a log file with file:line references
logContent := "Found issues:\n" +
"- `pkg/core/app.go:42` has an unused variable\n" +
"- `pkg/core/service.go:100` has a missing error check\n" +
"- `pkg/core/config.go:25` needs documentation\n" +
"This is padding to get past the 100 char minimum length requirement for the log file content parsing."
require.True(t, fs.Write(filepath.Join(wsDir, "agent-codex.log"), logContent).OK)
// Set up HOME for the agent-api.key read
home := t.TempDir()
t.Setenv("DIR_HOME", home)
require.True(t, fs.EnsureDir(filepath.Join(home, ".claude")).OK)
require.True(t, fs.Write(filepath.Join(home, ".claude", "agent-api.key"), "test-api-key").OK)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.ingestFindings(wsDir)
assert.True(t, issueCalled, "should have created an issue via API")
}
func TestIngestFindings_Bad_NotCompleted(t *testing.T) {
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "running",
Repo: "go-io",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return early — status is not "completed"
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
func TestIngestFindings_Bad_NoLogFile(t *testing.T) {
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return early — no log files
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
func TestIngestFindings_Bad_TooFewFindings(t *testing.T) {
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
}))
// Only 1 finding (need >= 2 to ingest)
logContent := "Found: `main.go:1` has an issue. This padding makes the content long enough to pass the 100 char minimum check."
require.True(t, fs.Write(filepath.Join(wsDir, "agent-codex.log"), logContent).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
func TestIngestFindings_Bad_QuotaExhausted(t *testing.T) {
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
}))
// Log contains quota error — should skip
logContent := "QUOTA_EXHAUSTED: Rate limit exceeded. `main.go:1` `other.go:2` padding to ensure we pass length check and get past the threshold."
require.True(t, fs.Write(filepath.Join(wsDir, "agent-codex.log"), logContent).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
func TestIngestFindings_Bad_NoStatusFile(t *testing.T) {
wsDir := t.TempDir()
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
func TestIngestFindings_Bad_ShortLogFile(t *testing.T) {
wsDir := t.TempDir()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
}))
// Log content is less than 100 bytes — should skip
require.True(t, fs.Write(filepath.Join(wsDir, "agent-codex.log"), "short").OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
assert.NotPanics(t, func() {
s.ingestFindings(wsDir)
})
}
// --- createIssueViaAPI ---
func TestCreateIssueViaAPI_Good_Success(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
assert.Equal(t, "POST", r.Method)
assert.Contains(t, r.URL.Path, "/v1/issues")
// Auth header should be present (Bearer + some key)
assert.Contains(t, r.Header.Get("Authorization"), "Bearer ")
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
assert.Equal(t, "Test Issue", body["title"])
assert.Equal(t, "bug", body["type"])
assert.Equal(t, "high", body["priority"])
w.WriteHeader(201)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.createIssueViaAPI("go-io", "Test Issue", "Description", "bug", "high", "scan")
assert.True(t, called)
}
func TestCreateIssueViaAPI_Bad_NoBrainKey(t *testing.T) {
s := &PrepSubsystem{
brainKey: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return early without panic
assert.NotPanics(t, func() {
s.createIssueViaAPI("go-io", "Title", "Body", "task", "normal", "scan")
})
}
func TestCreateIssueViaAPI_Bad_NoAPIKey(t *testing.T) {
home := t.TempDir()
t.Setenv("DIR_HOME", home)
// No agent-api.key file
s := &PrepSubsystem{
brainURL: "https://example.com",
brainKey: "test-brain-key",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return early — no API key file
assert.NotPanics(t, func() {
s.createIssueViaAPI("go-io", "Title", "Body", "task", "normal", "scan")
})
}
func TestCreateIssueViaAPI_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
home := t.TempDir()
t.Setenv("DIR_HOME", home)
require.True(t, fs.EnsureDir(filepath.Join(home, ".claude")).OK)
require.True(t, fs.Write(filepath.Join(home, ".claude", "agent-api.key"), "test-key").OK)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should not panic even on server error
assert.NotPanics(t, func() {
s.createIssueViaAPI("go-io", "Title", "Body", "task", "normal", "scan")
})
}
// --- countFileRefs (additional security-related) ---
func TestCountFileRefs_Good_SecurityFindings(t *testing.T) {
body := "Security scan found:\n" +
"- `pkg/auth/token.go:55` hardcoded secret\n" +
"- `pkg/auth/middleware.go:12` missing auth check\n"
assert.Equal(t, 2, countFileRefs(body))
}
func TestCountFileRefs_Good_PHPSecurityFindings(t *testing.T) {
body := "PHP audit:\n" +
"- `src/Controller/Api.php:42` SQL injection risk\n" +
"- `src/Service/Auth.php:100` session fixation\n" +
"- `src/Config/routes.php:5` open redirect\n"
assert.Equal(t, 3, countFileRefs(body))
}

664
pkg/agentic/logic_test.go Normal file
View file

@ -0,0 +1,664 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"bufio"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- agentCommand ---
func TestAgentCommand_Good_Gemini(t *testing.T) {
cmd, args, err := agentCommand("gemini", "do the thing")
require.NoError(t, err)
assert.Equal(t, "gemini", cmd)
assert.Contains(t, args, "-p")
assert.Contains(t, args, "do the thing")
assert.Contains(t, args, "--yolo")
assert.Contains(t, args, "--sandbox")
}
func TestAgentCommand_Good_GeminiWithModel(t *testing.T) {
cmd, args, err := agentCommand("gemini:flash", "my prompt")
require.NoError(t, err)
assert.Equal(t, "gemini", cmd)
assert.Contains(t, args, "-m")
assert.Contains(t, args, "gemini-2.5-flash")
}
func TestAgentCommand_Good_Codex(t *testing.T) {
cmd, args, err := agentCommand("codex", "fix the tests")
require.NoError(t, err)
assert.Equal(t, "codex", cmd)
assert.Contains(t, args, "exec")
assert.Contains(t, args, "--dangerously-bypass-approvals-and-sandbox")
assert.Contains(t, args, "fix the tests")
}
func TestAgentCommand_Good_CodexReview(t *testing.T) {
cmd, args, err := agentCommand("codex:review", "")
require.NoError(t, err)
assert.Equal(t, "codex", cmd)
assert.Contains(t, args, "exec")
// Review mode should NOT include -o flag
for _, a := range args {
assert.NotEqual(t, "-o", a)
}
}
func TestAgentCommand_Good_CodexWithModel(t *testing.T) {
cmd, args, err := agentCommand("codex:gpt-5.4", "refactor this")
require.NoError(t, err)
assert.Equal(t, "codex", cmd)
assert.Contains(t, args, "--model")
assert.Contains(t, args, "gpt-5.4")
}
func TestAgentCommand_Good_Claude(t *testing.T) {
cmd, args, err := agentCommand("claude", "add tests")
require.NoError(t, err)
assert.Equal(t, "claude", cmd)
assert.Contains(t, args, "-p")
assert.Contains(t, args, "add tests")
assert.Contains(t, args, "--dangerously-skip-permissions")
}
func TestAgentCommand_Good_ClaudeWithModel(t *testing.T) {
cmd, args, err := agentCommand("claude:haiku", "write docs")
require.NoError(t, err)
assert.Equal(t, "claude", cmd)
assert.Contains(t, args, "--model")
assert.Contains(t, args, "haiku")
}
func TestAgentCommand_Good_CodeRabbit(t *testing.T) {
cmd, args, err := agentCommand("coderabbit", "")
require.NoError(t, err)
assert.Equal(t, "coderabbit", cmd)
assert.Contains(t, args, "review")
assert.Contains(t, args, "--plain")
}
func TestAgentCommand_Good_Local(t *testing.T) {
cmd, args, err := agentCommand("local", "do stuff")
require.NoError(t, err)
assert.Equal(t, "sh", cmd)
assert.Equal(t, "-c", args[0])
// Script should contain socat proxy setup
assert.Contains(t, args[1], "socat")
assert.Contains(t, args[1], "devstral-24b")
}
func TestAgentCommand_Good_LocalWithModel(t *testing.T) {
cmd, args, err := agentCommand("local:mistral-nemo", "do stuff")
require.NoError(t, err)
assert.Equal(t, "sh", cmd)
assert.Contains(t, args[1], "mistral-nemo")
}
func TestAgentCommand_Bad_Unknown(t *testing.T) {
cmd, args, err := agentCommand("robot-from-the-future", "take over")
assert.Error(t, err)
assert.Empty(t, cmd)
assert.Nil(t, args)
}
func TestAgentCommand_Ugly_EmptyAgent(t *testing.T) {
cmd, args, err := agentCommand("", "prompt")
assert.Error(t, err)
assert.Empty(t, cmd)
assert.Nil(t, args)
}
// --- containerCommand ---
func TestContainerCommand_Good_Codex(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
cmd, args := containerCommand("codex", "codex", []string{"exec", "--dangerously-bypass-approvals-and-sandbox", "do it"}, "/ws/repo", "/ws/.meta")
assert.Equal(t, "docker", cmd)
assert.Contains(t, args, "run")
assert.Contains(t, args, "--rm")
assert.Contains(t, args, "/ws/repo:/workspace")
assert.Contains(t, args, "/ws/.meta:/workspace/.meta")
assert.Contains(t, args, "codex")
// Should use default image
assert.Contains(t, args, defaultDockerImage)
}
func TestContainerCommand_Good_CustomImage(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "my-custom-image:latest")
t.Setenv("DIR_HOME", "/home/dev")
cmd, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
assert.Equal(t, "docker", cmd)
assert.Contains(t, args, "my-custom-image:latest")
}
func TestContainerCommand_Good_ClaudeMountsConfig(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
_, args := containerCommand("claude", "claude", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta")
joined := strings.Join(args, " ")
assert.Contains(t, joined, ".claude:/home/dev/.claude:ro")
}
func TestContainerCommand_Good_GeminiMountsConfig(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
_, args := containerCommand("gemini", "gemini", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta")
joined := strings.Join(args, " ")
assert.Contains(t, joined, ".gemini:/home/dev/.gemini:ro")
}
func TestContainerCommand_Good_CodexNoClaudeMount(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
_, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
joined := strings.Join(args, " ")
// codex agent must NOT mount .claude config
assert.NotContains(t, joined, ".claude:/home/dev/.claude:ro")
}
func TestContainerCommand_Good_APIKeysPassedByRef(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
_, args := containerCommand("codex", "codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
joined := strings.Join(args, " ")
assert.Contains(t, joined, "OPENAI_API_KEY")
assert.Contains(t, joined, "ANTHROPIC_API_KEY")
assert.Contains(t, joined, "GEMINI_API_KEY")
}
func TestContainerCommand_Ugly_EmptyDirs(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "")
// Should not panic with empty paths
cmd, args := containerCommand("codex", "codex", []string{"exec"}, "", "")
assert.Equal(t, "docker", cmd)
assert.NotEmpty(t, args)
}
// --- buildAutoPRBody ---
func TestBuildAutoPRBody_Good_Basic(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "Fix the login bug",
Agent: "codex",
Branch: "agent/fix-login-bug",
}
body := s.buildAutoPRBody(st, 3)
assert.Contains(t, body, "Fix the login bug")
assert.Contains(t, body, "codex")
assert.Contains(t, body, "3")
assert.Contains(t, body, "agent/fix-login-bug")
assert.Contains(t, body, "Co-Authored-By: Virgil <virgil@lethean.io>")
}
func TestBuildAutoPRBody_Good_WithIssue(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "Add rate limiting",
Agent: "claude",
Branch: "agent/add-rate-limiting",
Issue: 42,
}
body := s.buildAutoPRBody(st, 1)
assert.Contains(t, body, "Closes #42")
}
func TestBuildAutoPRBody_Good_NoIssue(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "Refactor internals",
Agent: "gemini",
Branch: "agent/refactor-internals",
}
body := s.buildAutoPRBody(st, 5)
assert.NotContains(t, body, "Closes #")
}
func TestBuildAutoPRBody_Good_CommitCount(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{Agent: "codex", Branch: "agent/foo"}
body1 := s.buildAutoPRBody(st, 1)
body5 := s.buildAutoPRBody(st, 5)
assert.Contains(t, body1, "**Commits:** 1")
assert.Contains(t, body5, "**Commits:** 5")
}
func TestBuildAutoPRBody_Bad_EmptyTask(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{
Task: "",
Agent: "codex",
Branch: "agent/something",
}
// Should not panic; body should still have the structure
body := s.buildAutoPRBody(st, 0)
assert.Contains(t, body, "## Task")
assert.Contains(t, body, "**Agent:** codex")
}
func TestBuildAutoPRBody_Ugly_ZeroCommits(t *testing.T) {
s := &PrepSubsystem{}
st := &WorkspaceStatus{Agent: "codex", Branch: "agent/test"}
body := s.buildAutoPRBody(st, 0)
assert.Contains(t, body, "**Commits:** 0")
}
// --- emitEvent ---
func TestEmitEvent_Good_WritesJSONL(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
emitEvent("agent_completed", "codex", "core/go-io/task-5", "completed")
eventsFile := filepath.Join(root, "workspace", "events.jsonl")
r := fs.Read(eventsFile)
require.True(t, r.OK, "events.jsonl should exist after emitEvent")
content := r.Value.(string)
assert.Contains(t, content, "agent_completed")
assert.Contains(t, content, "codex")
assert.Contains(t, content, "core/go-io/task-5")
assert.Contains(t, content, "completed")
}
func TestEmitEvent_Good_ValidJSON(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
emitEvent("agent_started", "claude", "core/agent/task-1", "running")
eventsFile := filepath.Join(root, "workspace", "events.jsonl")
f, err := os.Open(eventsFile)
require.NoError(t, err)
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var ev CompletionEvent
require.NoError(t, json.Unmarshal([]byte(line), &ev), "each line must be valid JSON")
assert.Equal(t, "agent_started", ev.Type)
}
}
func TestEmitEvent_Good_Appends(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
emitEvent("agent_started", "codex", "core/go-io/task-1", "running")
emitEvent("agent_completed", "codex", "core/go-io/task-1", "completed")
eventsFile := filepath.Join(root, "workspace", "events.jsonl")
r := fs.Read(eventsFile)
require.True(t, r.OK)
lines := 0
for _, line := range strings.Split(strings.TrimSpace(r.Value.(string)), "\n") {
if line != "" {
lines++
}
}
assert.Equal(t, 2, lines, "both events should be in the log")
}
func TestEmitEvent_Good_StartHelper(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
emitStartEvent("gemini", "core/go-log/task-3")
eventsFile := filepath.Join(root, "workspace", "events.jsonl")
r := fs.Read(eventsFile)
require.True(t, r.OK)
assert.Contains(t, r.Value.(string), "agent_started")
assert.Contains(t, r.Value.(string), "running")
}
func TestEmitEvent_Good_CompletionHelper(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
emitCompletionEvent("claude", "core/agent/task-7", "failed")
eventsFile := filepath.Join(root, "workspace", "events.jsonl")
r := fs.Read(eventsFile)
require.True(t, r.OK)
assert.Contains(t, r.Value.(string), "agent_completed")
assert.Contains(t, r.Value.(string), "failed")
}
func TestEmitEvent_Bad_NoWorkspaceDir(t *testing.T) {
// CORE_WORKSPACE points to a directory that doesn't allow writing events.jsonl
// because workspace/ subdir doesn't exist. Should not panic.
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Do NOT create workspace/ subdir — emitEvent must handle this gracefully
assert.NotPanics(t, func() {
emitEvent("agent_completed", "codex", "test", "completed")
})
}
func TestEmitEvent_Ugly_EmptyFields(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
// Should not panic with all empty fields
assert.NotPanics(t, func() {
emitEvent("", "", "", "")
})
}
// --- countFileRefs ---
func TestCountFileRefs_Good_GoRefs(t *testing.T) {
body := "Found issue in `pkg/core/app.go:42` and `pkg/core/service.go:100`."
assert.Equal(t, 2, countFileRefs(body))
}
func TestCountFileRefs_Good_PHPRefs(t *testing.T) {
body := "See `src/Core/Boot.php:15` for details."
assert.Equal(t, 1, countFileRefs(body))
}
func TestCountFileRefs_Good_Mixed(t *testing.T) {
body := "Go file: `main.go:1`, PHP file: `index.php:99`, plain text ref."
assert.Equal(t, 2, countFileRefs(body))
}
func TestCountFileRefs_Good_NoRefs(t *testing.T) {
body := "This is just plain text with no file references."
assert.Equal(t, 0, countFileRefs(body))
}
func TestCountFileRefs_Good_UnrelatedBacktick(t *testing.T) {
// Backtick-quoted string that is not a file:line reference
body := "Run `go test ./...` to execute tests."
assert.Equal(t, 0, countFileRefs(body))
}
func TestCountFileRefs_Bad_EmptyBody(t *testing.T) {
assert.Equal(t, 0, countFileRefs(""))
}
func TestCountFileRefs_Bad_ShortBody(t *testing.T) {
// Body too short to contain a valid reference
assert.Equal(t, 0, countFileRefs("`a`"))
}
func TestCountFileRefs_Ugly_MalformedBackticks(t *testing.T) {
// Unclosed backtick — should not panic or hang
body := "Something `unclosed"
assert.NotPanics(t, func() {
countFileRefs(body)
})
}
func TestCountFileRefs_Ugly_LongRef(t *testing.T) {
// Reference longer than 100 chars should not be counted (loop limit)
longRef := "`" + strings.Repeat("a", 101) + ".go:1`"
assert.Equal(t, 0, countFileRefs(longRef))
}
// --- modelVariant ---
func TestModelVariant_Good_WithModel(t *testing.T) {
assert.Equal(t, "gpt-5.4", modelVariant("codex:gpt-5.4"))
assert.Equal(t, "flash", modelVariant("gemini:flash"))
assert.Equal(t, "opus", modelVariant("claude:opus"))
assert.Equal(t, "haiku", modelVariant("claude:haiku"))
}
func TestModelVariant_Good_NoVariant(t *testing.T) {
assert.Equal(t, "", modelVariant("codex"))
assert.Equal(t, "", modelVariant("claude"))
assert.Equal(t, "", modelVariant("gemini"))
}
func TestModelVariant_Good_MultipleColons(t *testing.T) {
// SplitN(2) only splits on first colon; rest is preserved as the model
assert.Equal(t, "gpt-5.3-codex-spark", modelVariant("codex:gpt-5.3-codex-spark"))
}
func TestModelVariant_Bad_EmptyString(t *testing.T) {
assert.Equal(t, "", modelVariant(""))
}
func TestModelVariant_Ugly_ColonOnly(t *testing.T) {
// Just a colon with no model name
assert.Equal(t, "", modelVariant(":"))
}
// --- baseAgent ---
func TestBaseAgent_Good_Variants(t *testing.T) {
assert.Equal(t, "gemini", baseAgent("gemini:flash"))
assert.Equal(t, "gemini", baseAgent("gemini:pro"))
assert.Equal(t, "claude", baseAgent("claude:haiku"))
assert.Equal(t, "codex", baseAgent("codex:gpt-5.4"))
}
func TestBaseAgent_Good_NoVariant(t *testing.T) {
assert.Equal(t, "codex", baseAgent("codex"))
assert.Equal(t, "claude", baseAgent("claude"))
assert.Equal(t, "gemini", baseAgent("gemini"))
}
func TestBaseAgent_Good_CodexSparkSpecialCase(t *testing.T) {
// codex-spark variants map to their own pool name
assert.Equal(t, "codex-spark", baseAgent("codex:gpt-5.3-codex-spark"))
assert.Equal(t, "codex-spark", baseAgent("codex-spark"))
}
func TestBaseAgent_Bad_EmptyString(t *testing.T) {
// Empty string — SplitN returns [""], so first element is ""
assert.Equal(t, "", baseAgent(""))
}
func TestBaseAgent_Ugly_JustColon(t *testing.T) {
// Just a colon — base is empty string before colon
assert.Equal(t, "", baseAgent(":model"))
}
// --- resolveWorkspace ---
func TestResolveWorkspace_Good_ExistingDir(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Create the workspace directory structure
wsName := "core/go-io/task-5"
wsDir := filepath.Join(root, "workspace", wsName)
require.True(t, fs.EnsureDir(wsDir).OK)
result := resolveWorkspace(wsName)
assert.Equal(t, wsDir, result)
}
func TestResolveWorkspace_Good_NestedPath(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsName := "core/agent/pr-42"
wsDir := filepath.Join(root, "workspace", wsName)
require.True(t, fs.EnsureDir(wsDir).OK)
result := resolveWorkspace(wsName)
assert.Equal(t, wsDir, result)
}
func TestResolveWorkspace_Bad_NonExistentDir(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
result := resolveWorkspace("core/go-io/task-999")
assert.Equal(t, "", result)
}
func TestResolveWorkspace_Bad_EmptyName(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Empty name resolves to the workspace root itself — which is a dir but not a workspace
// The function returns "" if the path is not a directory, and the workspace root *is*
// a directory if created. This test verifies the path arithmetic is sane.
result := resolveWorkspace("")
// Either the workspace root itself or "" — both are acceptable; must not panic.
_ = result
}
func TestResolveWorkspace_Ugly_PathTraversal(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Path traversal attempt should return "" (parent of workspace root won't be a workspace)
result := resolveWorkspace("../../etc")
assert.Equal(t, "", result)
}
// --- findWorkspaceByPR ---
func TestFindWorkspaceByPR_Good_MatchesFlatLayout(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "task-10")
require.True(t, fs.EnsureDir(wsDir).OK)
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/fix-timeout",
}))
result := findWorkspaceByPR("go-io", "agent/fix-timeout")
assert.Equal(t, wsDir, result)
}
func TestFindWorkspaceByPR_Good_MatchesDeepLayout(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "core", "go-io", "task-15")
require.True(t, fs.EnsureDir(wsDir).OK)
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "running",
Repo: "go-io",
Branch: "agent/add-metrics",
}))
result := findWorkspaceByPR("go-io", "agent/add-metrics")
assert.Equal(t, wsDir, result)
}
func TestFindWorkspaceByPR_Bad_NoMatch(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "task-99")
require.True(t, fs.EnsureDir(wsDir).OK)
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/some-other-branch",
}))
result := findWorkspaceByPR("go-io", "agent/nonexistent-branch")
assert.Equal(t, "", result)
}
func TestFindWorkspaceByPR_Bad_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// No workspaces at all
result := findWorkspaceByPR("go-io", "agent/any-branch")
assert.Equal(t, "", result)
}
func TestFindWorkspaceByPR_Bad_RepoDiffers(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "task-5")
require.True(t, fs.EnsureDir(wsDir).OK)
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-log",
Branch: "agent/fix-formatter",
}))
// Same branch, different repo
result := findWorkspaceByPR("go-io", "agent/fix-formatter")
assert.Equal(t, "", result)
}
func TestFindWorkspaceByPR_Ugly_CorruptStatusFile(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "corrupt-ws")
require.True(t, fs.EnsureDir(wsDir).OK)
require.True(t, fs.Write(filepath.Join(wsDir, "status.json"), "not-valid-json{").OK)
// Should skip corrupt entries, not panic
result := findWorkspaceByPR("go-io", "agent/any")
assert.Equal(t, "", result)
}
// --- extractPRNumber ---
func TestExtractPRNumber_Good_FullURL(t *testing.T) {
assert.Equal(t, 42, extractPRNumber("https://forge.lthn.ai/core/agent/pulls/42"))
assert.Equal(t, 1, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/1"))
assert.Equal(t, 999, extractPRNumber("https://forge.lthn.ai/core/go-log/pulls/999"))
}
func TestExtractPRNumber_Good_NumberOnly(t *testing.T) {
// If someone passes a bare number as a URL it should still work
assert.Equal(t, 7, extractPRNumber("7"))
}
func TestExtractPRNumber_Bad_EmptyURL(t *testing.T) {
assert.Equal(t, 0, extractPRNumber(""))
}
func TestExtractPRNumber_Bad_TrailingSlash(t *testing.T) {
// URL ending with slash has empty last segment
assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/"))
}
func TestExtractPRNumber_Bad_NonNumericEnd(t *testing.T) {
assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/abc"))
}
func TestExtractPRNumber_Ugly_JustSlashes(t *testing.T) {
// All slashes — last segment is empty
assert.Equal(t, 0, extractPRNumber("///"))
}

357
pkg/agentic/mirror_test.go Normal file
View file

@ -0,0 +1,357 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// initBareRepo creates a minimal git repo with one commit and returns its path.
func initBareRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "cmd %v failed: %s", args, string(out))
}
run("git", "init", "-b", "main")
run("git", "config", "user.name", "Test")
run("git", "config", "user.email", "test@test.com")
// Create a file and commit
require.True(t, fs.Write(filepath.Join(dir, "README.md"), "# Test").OK)
run("git", "add", "README.md")
run("git", "commit", "-m", "initial commit")
return dir
}
// --- hasRemote ---
func TestHasRemote_Good_OriginExists(t *testing.T) {
dir := initBareRepo(t)
// origin won't exist for a fresh repo, so add it
cmd := exec.Command("git", "remote", "add", "origin", "https://example.com/repo.git")
cmd.Dir = dir
require.NoError(t, cmd.Run())
assert.True(t, hasRemote(dir, "origin"))
}
func TestHasRemote_Good_CustomRemote(t *testing.T) {
dir := initBareRepo(t)
cmd := exec.Command("git", "remote", "add", "github", "https://github.com/test/repo.git")
cmd.Dir = dir
require.NoError(t, cmd.Run())
assert.True(t, hasRemote(dir, "github"))
}
func TestHasRemote_Bad_NoSuchRemote(t *testing.T) {
dir := initBareRepo(t)
assert.False(t, hasRemote(dir, "nonexistent"))
}
func TestHasRemote_Bad_NotAGitRepo(t *testing.T) {
dir := t.TempDir() // plain directory, no .git
assert.False(t, hasRemote(dir, "origin"))
}
func TestHasRemote_Ugly_EmptyDir(t *testing.T) {
// Empty dir defaults to cwd which may or may not be a repo.
// Just ensure no panic.
assert.NotPanics(t, func() {
hasRemote("", "origin")
})
}
// --- commitsAhead ---
func TestCommitsAhead_Good_OneAhead(t *testing.T) {
dir := initBareRepo(t)
// Create a branch at the current commit to act as "base"
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "cmd %v failed: %s", args, string(out))
}
run("git", "branch", "base")
// Add a commit on main
require.True(t, fs.Write(filepath.Join(dir, "new.txt"), "data").OK)
run("git", "add", "new.txt")
run("git", "commit", "-m", "second commit")
ahead := commitsAhead(dir, "base", "main")
assert.Equal(t, 1, ahead)
}
func TestCommitsAhead_Good_ThreeAhead(t *testing.T) {
dir := initBareRepo(t)
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "cmd %v failed: %s", args, string(out))
}
run("git", "branch", "base")
for i := 0; i < 3; i++ {
name := filepath.Join(dir, "file"+string(rune('a'+i))+".txt")
require.True(t, fs.Write(name, "content").OK)
run("git", "add", ".")
run("git", "commit", "-m", "commit "+string(rune('0'+i)))
}
ahead := commitsAhead(dir, "base", "main")
assert.Equal(t, 3, ahead)
}
func TestCommitsAhead_Good_ZeroAhead(t *testing.T) {
dir := initBareRepo(t)
// Same ref on both sides
ahead := commitsAhead(dir, "main", "main")
assert.Equal(t, 0, ahead)
}
func TestCommitsAhead_Bad_InvalidRef(t *testing.T) {
dir := initBareRepo(t)
ahead := commitsAhead(dir, "nonexistent-ref", "main")
assert.Equal(t, 0, ahead)
}
func TestCommitsAhead_Bad_NotARepo(t *testing.T) {
ahead := commitsAhead(t.TempDir(), "main", "dev")
assert.Equal(t, 0, ahead)
}
func TestCommitsAhead_Ugly_EmptyDir(t *testing.T) {
ahead := commitsAhead("", "a", "b")
assert.Equal(t, 0, ahead)
}
// --- filesChanged ---
func TestFilesChanged_Good_OneFile(t *testing.T) {
dir := initBareRepo(t)
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "cmd %v failed: %s", args, string(out))
}
run("git", "branch", "base")
require.True(t, fs.Write(filepath.Join(dir, "changed.txt"), "new").OK)
run("git", "add", "changed.txt")
run("git", "commit", "-m", "add file")
files := filesChanged(dir, "base", "main")
assert.Equal(t, 1, files)
}
func TestFilesChanged_Good_MultipleFiles(t *testing.T) {
dir := initBareRepo(t)
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(),
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@test.com",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@test.com",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "cmd %v failed: %s", args, string(out))
}
run("git", "branch", "base")
for _, name := range []string{"a.go", "b.go", "c.go"} {
require.True(t, fs.Write(filepath.Join(dir, name), "package main").OK)
}
run("git", "add", ".")
run("git", "commit", "-m", "add three files")
files := filesChanged(dir, "base", "main")
assert.Equal(t, 3, files)
}
func TestFilesChanged_Good_NoChanges(t *testing.T) {
dir := initBareRepo(t)
files := filesChanged(dir, "main", "main")
assert.Equal(t, 0, files)
}
func TestFilesChanged_Bad_InvalidRef(t *testing.T) {
dir := initBareRepo(t)
files := filesChanged(dir, "nonexistent", "main")
assert.Equal(t, 0, files)
}
func TestFilesChanged_Bad_NotARepo(t *testing.T) {
files := filesChanged(t.TempDir(), "main", "dev")
assert.Equal(t, 0, files)
}
func TestFilesChanged_Ugly_EmptyDir(t *testing.T) {
files := filesChanged("", "a", "b")
assert.Equal(t, 0, files)
}
// --- extractJSONField (extending existing 91% coverage) ---
func TestExtractJSONField_Good_ArrayFirstItem(t *testing.T) {
json := `[{"url":"https://github.com/test/pr/1","title":"Fix bug"}]`
assert.Equal(t, "https://github.com/test/pr/1", extractJSONField(json, "url"))
}
func TestExtractJSONField_Good_ObjectField(t *testing.T) {
json := `{"name":"test-repo","status":"active"}`
assert.Equal(t, "test-repo", extractJSONField(json, "name"))
}
func TestExtractJSONField_Good_ArrayMultipleItems(t *testing.T) {
json := `[{"id":"first"},{"id":"second"}]`
// Should return the first match
assert.Equal(t, "first", extractJSONField(json, "id"))
}
func TestExtractJSONField_Bad_EmptyJSON(t *testing.T) {
assert.Equal(t, "", extractJSONField("", "url"))
}
func TestExtractJSONField_Bad_EmptyField(t *testing.T) {
assert.Equal(t, "", extractJSONField(`{"url":"test"}`, ""))
}
func TestExtractJSONField_Bad_FieldNotFound(t *testing.T) {
json := `{"name":"test"}`
assert.Equal(t, "", extractJSONField(json, "missing"))
}
func TestExtractJSONField_Bad_InvalidJSON(t *testing.T) {
assert.Equal(t, "", extractJSONField("not json at all", "url"))
}
func TestExtractJSONField_Ugly_EmptyArray(t *testing.T) {
assert.Equal(t, "", extractJSONField("[]", "url"))
}
func TestExtractJSONField_Ugly_EmptyObject(t *testing.T) {
assert.Equal(t, "", extractJSONField("{}", "url"))
}
func TestExtractJSONField_Ugly_NumericValue(t *testing.T) {
// Field exists but is not a string — should return ""
json := `{"count":42}`
assert.Equal(t, "", extractJSONField(json, "count"))
}
func TestExtractJSONField_Ugly_NullValue(t *testing.T) {
json := `{"url":null}`
assert.Equal(t, "", extractJSONField(json, "url"))
}
// --- DefaultBranch ---
func TestDefaultBranch_Good_MainBranch(t *testing.T) {
dir := initBareRepo(t)
// initBareRepo creates with -b main
branch := DefaultBranch(dir)
assert.Equal(t, "main", branch)
}
func TestDefaultBranch_Bad_NotARepo(t *testing.T) {
dir := t.TempDir()
// Falls back to "main" when detection fails
branch := DefaultBranch(dir)
assert.Equal(t, "main", branch)
}
// --- listLocalRepos ---
func TestListLocalRepos_Good_FindsRepos(t *testing.T) {
base := t.TempDir()
// Create two git repos under base
for _, name := range []string{"repo-a", "repo-b"} {
repoDir := filepath.Join(base, name)
cmd := exec.Command("git", "init", repoDir)
require.NoError(t, cmd.Run())
}
// Create a non-repo directory
require.True(t, fs.EnsureDir(filepath.Join(base, "not-a-repo")).OK)
s := &PrepSubsystem{}
repos := s.listLocalRepos(base)
assert.Contains(t, repos, "repo-a")
assert.Contains(t, repos, "repo-b")
assert.NotContains(t, repos, "not-a-repo")
}
func TestListLocalRepos_Bad_EmptyDir(t *testing.T) {
base := t.TempDir()
s := &PrepSubsystem{}
repos := s.listLocalRepos(base)
assert.Empty(t, repos)
}
func TestListLocalRepos_Bad_NonExistentDir(t *testing.T) {
s := &PrepSubsystem{}
repos := s.listLocalRepos("/nonexistent/path/that/doesnt/exist")
assert.Nil(t, repos)
}
// --- GitHubOrg ---
func TestGitHubOrg_Good_Default(t *testing.T) {
t.Setenv("GITHUB_ORG", "")
assert.Equal(t, "dAppCore", GitHubOrg())
}
func TestGitHubOrg_Good_Custom(t *testing.T) {
t.Setenv("GITHUB_ORG", "my-org")
assert.Equal(t, "my-org", GitHubOrg())
}

View file

@ -0,0 +1,175 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- planPath ---
func TestPlanPath_Good_BasicFormat(t *testing.T) {
result := planPath("/tmp/plans", "my-plan-abc123")
assert.Equal(t, "/tmp/plans/my-plan-abc123.json", result)
}
func TestPlanPath_Good_NestedIDStripped(t *testing.T) {
// PathBase strips directory component — prevents path traversal
result := planPath("/plans", "../../../etc/passwd")
assert.Equal(t, "/plans/passwd.json", result)
}
func TestPlanPath_Good_SimpleID(t *testing.T) {
assert.Equal(t, "/data/test.json", planPath("/data", "test"))
}
func TestPlanPath_Good_SlugWithDashes(t *testing.T) {
assert.Equal(t, "/root/migrate-core-abc123.json", planPath("/root", "migrate-core-abc123"))
}
func TestPlanPath_Bad_DotID(t *testing.T) {
// "." is sanitised to "invalid" to prevent exploiting the root directory
result := planPath("/plans", ".")
assert.Equal(t, "/plans/invalid.json", result)
}
func TestPlanPath_Bad_DoubleDotID(t *testing.T) {
result := planPath("/plans", "..")
assert.Equal(t, "/plans/invalid.json", result)
}
func TestPlanPath_Bad_EmptyID(t *testing.T) {
result := planPath("/plans", "")
assert.Equal(t, "/plans/invalid.json", result)
}
// --- readPlan / writePlan ---
func TestReadWritePlan_Good_BasicRoundtrip(t *testing.T) {
dir := t.TempDir()
now := time.Now().Truncate(time.Second)
plan := &Plan{
ID: "basic-plan-abc",
Title: "Basic Plan",
Status: "draft",
Repo: "go-io",
Org: "core",
Objective: "Verify round-trip works",
Agent: "claude:opus",
CreatedAt: now,
UpdatedAt: now,
}
path, err := writePlan(dir, plan)
require.NoError(t, err)
assert.Equal(t, filepath.Join(dir, "basic-plan-abc.json"), path)
read, err := readPlan(dir, "basic-plan-abc")
require.NoError(t, err)
assert.Equal(t, plan.ID, read.ID)
assert.Equal(t, plan.Title, read.Title)
assert.Equal(t, plan.Status, read.Status)
assert.Equal(t, plan.Repo, read.Repo)
assert.Equal(t, plan.Org, read.Org)
assert.Equal(t, plan.Objective, read.Objective)
assert.Equal(t, plan.Agent, read.Agent)
}
func TestReadWritePlan_Good_WithPhases(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "phase-plan-abc",
Title: "Phased Work",
Status: "in_progress",
Objective: "Multi-phase plan",
Phases: []Phase{
{Number: 1, Name: "Setup", Status: "done", Criteria: []string{"repo cloned", "deps installed"}, Tests: 3},
{Number: 2, Name: "Implement", Status: "in_progress", Notes: "WIP"},
{Number: 3, Name: "Verify", Status: "pending"},
},
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "phase-plan-abc")
require.NoError(t, err)
require.Len(t, read.Phases, 3)
assert.Equal(t, "Setup", read.Phases[0].Name)
assert.Equal(t, "done", read.Phases[0].Status)
assert.Equal(t, []string{"repo cloned", "deps installed"}, read.Phases[0].Criteria)
assert.Equal(t, 3, read.Phases[0].Tests)
assert.Equal(t, "WIP", read.Phases[1].Notes)
assert.Equal(t, "pending", read.Phases[2].Status)
}
func TestReadPlan_Bad_MissingFile(t *testing.T) {
dir := t.TempDir()
_, err := readPlan(dir, "nonexistent-plan")
assert.Error(t, err)
}
func TestReadPlan_Bad_CorruptJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "bad.json"), `{broken`).OK)
_, err := readPlan(dir, "bad")
assert.Error(t, err)
}
func TestWritePlan_Good_CreatesNestedDir(t *testing.T) {
base := t.TempDir()
nested := filepath.Join(base, "deep", "nested", "plans")
plan := &Plan{
ID: "deep-plan-xyz",
Title: "Deep",
Status: "draft",
Objective: "Test nested dir creation",
}
path, err := writePlan(nested, plan)
require.NoError(t, err)
assert.Equal(t, filepath.Join(nested, "deep-plan-xyz.json"), path)
assert.True(t, fs.IsFile(path))
}
func TestWritePlan_Good_OverwriteExistingLogic(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "overwrite-plan-abc",
Title: "First Title",
Status: "draft",
Objective: "Initial",
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
plan.Title = "Second Title"
plan.Status = "approved"
_, err = writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "overwrite-plan-abc")
require.NoError(t, err)
assert.Equal(t, "Second Title", read.Title)
assert.Equal(t, "approved", read.Status)
}
func TestReadPlan_Ugly_EmptyFileLogic(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "empty.json"), "").OK)
_, err := readPlan(dir, "empty")
assert.Error(t, err)
}

266
pkg/agentic/pr_test.go Normal file
View file

@ -0,0 +1,266 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os/exec"
"path/filepath"
"testing"
"time"
"dappco.re/go/core/forge"
forge_types "dappco.re/go/core/forge/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockPRForgeServer creates a Forge API mock that handles PR creation and comments.
func mockPRForgeServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
// Create PR endpoint — returns Forgejo-compatible JSON
mux.HandleFunc("/api/v1/repos/core/test-repo/pulls", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
var body forge_types.CreatePullRequestOption
json.NewDecoder(r.Body).Decode(&body)
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"number": 12,
"html_url": "https://forge.test/core/test-repo/pulls/12",
"title": body.Title,
"head": map[string]any{"ref": body.Head},
"base": map[string]any{"ref": body.Base},
})
return
}
// GET — list PRs
json.NewEncoder(w).Encode([]map[string]any{})
})
// Issue comments
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && containsStr(r.URL.Path, "/comments") {
w.WriteHeader(201)
return
}
w.WriteHeader(200)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
// --- forgeCreatePR ---
func TestForgeCreatePR_Good_Success(t *testing.T) {
srv := mockPRForgeServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
prURL, prNum, err := s.forgeCreatePR(
context.Background(),
"core", "test-repo",
"agent/fix-bug", "dev",
"Fix the login bug", "PR body text",
)
require.NoError(t, err)
assert.Equal(t, 12, prNum)
assert.Contains(t, prURL, "pulls/12")
}
func TestForgeCreatePR_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]any{"message": "internal error"})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.forgeCreatePR(
context.Background(),
"core", "test-repo",
"agent/fix", "dev",
"Title", "Body",
)
assert.Error(t, err)
}
// --- createPR (MCP tool) ---
func TestCreatePR_Bad_NoWorkspace(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.createPR(context.Background(), nil, CreatePRInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "workspace is required")
}
func TestCreatePR_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.createPR(context.Background(), nil, CreatePRInput{
Workspace: "test-ws",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
func TestCreatePR_Bad_WorkspaceNotFound(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.createPR(context.Background(), nil, CreatePRInput{
Workspace: "nonexistent-workspace",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "workspace not found")
}
func TestCreatePR_Good_DryRun(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Create workspace with repo/.git
wsDir := filepath.Join(root, "workspace", "test-ws")
repoDir := filepath.Join(wsDir, "repo")
require.NoError(t, exec.Command("git", "init", "-b", "main", repoDir).Run())
gitCmd := exec.Command("git", "config", "user.name", "Test")
gitCmd.Dir = repoDir
gitCmd.Run()
gitCmd = exec.Command("git", "config", "user.email", "test@test.com")
gitCmd.Dir = repoDir
gitCmd.Run()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/fix-bug",
Task: "Fix the login bug",
}))
s := &PrepSubsystem{
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.createPR(context.Background(), nil, CreatePRInput{
Workspace: "test-ws",
DryRun: true,
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Equal(t, "agent/fix-bug", out.Branch)
assert.Equal(t, "go-io", out.Repo)
assert.Equal(t, "Fix the login bug", out.Title)
}
func TestCreatePR_Good_CustomTitle(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := filepath.Join(root, "workspace", "test-ws-2")
repoDir := filepath.Join(wsDir, "repo")
require.NoError(t, exec.Command("git", "init", "-b", "main", repoDir).Run())
gitCmd := exec.Command("git", "config", "user.name", "Test")
gitCmd.Dir = repoDir
gitCmd.Run()
gitCmd = exec.Command("git", "config", "user.email", "test@test.com")
gitCmd.Dir = repoDir
gitCmd.Run()
require.NoError(t, writeStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/fix",
Task: "Default task",
}))
s := &PrepSubsystem{
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.createPR(context.Background(), nil, CreatePRInput{
Workspace: "test-ws-2",
Title: "Custom PR title",
DryRun: true,
})
require.NoError(t, err)
assert.Equal(t, "Custom PR title", out.Title)
}
// --- listPRs ---
func TestListPRs_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.listPRs(context.Background(), nil, ListPRsInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
// --- commentOnIssue ---
func TestCommentOnIssue_Good_PostsComment(t *testing.T) {
commentPosted := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
commentPosted = true
w.WriteHeader(201)
}
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.commentOnIssue(context.Background(), "core", "go-io", 42, "Test comment")
assert.True(t, commentPosted)
}

View file

@ -0,0 +1,220 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- countRunningByModel ---
func TestCountRunningByModel_Good_Empty(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByModel("claude:opus"))
}
func TestCountRunningByModel_Good_SkipsNonRunning(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Completed workspace — must not be counted
ws := filepath.Join(root, "workspace", "test-ws")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "codex:gpt-5.4",
PID: 0,
}))
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByModel("codex:gpt-5.4"))
}
func TestCountRunningByModel_Good_SkipsMismatchedModel(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
ws := filepath.Join(root, "workspace", "model-ws")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "running",
Agent: "gemini:flash",
PID: 0,
}))
s := &PrepSubsystem{}
// Asking for gemini:pro — must not count gemini:flash
assert.Equal(t, 0, s.countRunningByModel("gemini:pro"))
}
func TestCountRunningByModel_Good_DeepLayout(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Deep layout: workspace/org/repo/task-N/status.json
ws := filepath.Join(root, "workspace", "core", "go-io", "task-1")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "codex:gpt-5.4",
}))
s := &PrepSubsystem{}
// Completed, so count is still 0
assert.Equal(t, 0, s.countRunningByModel("codex:gpt-5.4"))
}
// --- drainQueue ---
func TestDrainQueue_Good_FrozenReturnsImmediately(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{frozen: true, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
// Must not panic and must not block
assert.NotPanics(t, func() {
s.drainQueue()
})
}
func TestDrainQueue_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{frozen: false, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
// No workspaces — must return without error/panic
assert.NotPanics(t, func() {
s.drainQueue()
})
}
// --- Poke ---
func TestPoke_Good_NilChannel(t *testing.T) {
s := &PrepSubsystem{pokeCh: nil}
// Must not panic when pokeCh is nil
assert.NotPanics(t, func() {
s.Poke()
})
}
func TestPoke_Good_ChannelReceivesSignal(t *testing.T) {
s := &PrepSubsystem{}
s.pokeCh = make(chan struct{}, 1)
s.Poke()
assert.Len(t, s.pokeCh, 1, "poke should enqueue one signal")
}
func TestPoke_Good_NonBlockingWhenFull(t *testing.T) {
s := &PrepSubsystem{}
s.pokeCh = make(chan struct{}, 1)
// Pre-fill the channel
s.pokeCh <- struct{}{}
// Second poke must not block or panic
assert.NotPanics(t, func() {
s.Poke()
})
assert.Len(t, s.pokeCh, 1, "channel length should remain 1")
}
// --- StartRunner ---
func TestStartRunner_Good_CreatesPokeCh(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "")
s := NewPrep()
assert.Nil(t, s.pokeCh)
s.StartRunner()
assert.NotNil(t, s.pokeCh, "StartRunner should initialise pokeCh")
}
func TestStartRunner_Good_FrozenByDefault(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "")
s := NewPrep()
s.StartRunner()
assert.True(t, s.frozen, "queue should be frozen by default")
}
func TestStartRunner_Good_AutoStartEnvVar(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "1")
s := NewPrep()
s.StartRunner()
assert.False(t, s.frozen, "CORE_AGENT_DISPATCH=1 should unfreeze the queue")
}
// --- DefaultBranch ---
func TestDefaultBranch_Good_DefaultsToMain(t *testing.T) {
// Non-git temp dir — git commands fail, fallback is "main"
dir := t.TempDir()
branch := DefaultBranch(dir)
assert.Equal(t, "main", branch)
}
func TestDefaultBranch_Good_RealGitRepo(t *testing.T) {
dir := t.TempDir()
// Init a real git repo with a main branch
require.NoError(t, runGitInit(dir))
branch := DefaultBranch(dir)
// Any valid branch name — just must not panic or be empty
assert.NotEmpty(t, branch)
}
// --- LocalFs ---
func TestLocalFs_Good_NonNil(t *testing.T) {
f := LocalFs()
assert.NotNil(t, f, "LocalFs should return a non-nil *core.Fs")
}
func TestLocalFs_Good_CanRead(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "hello.txt")
require.True(t, fs.Write(path, "hello").OK)
f := LocalFs()
r := f.Read(path)
assert.True(t, r.OK)
assert.Equal(t, "hello", r.Value.(string))
}
// --- helpers ---
// runGitInit initialises a bare git repo with one commit so branch detection works.
func runGitInit(dir string) error {
cmds := [][]string{
{"git", "init", "-b", "main"},
{"git", "config", "user.email", "test@test.com"},
{"git", "config", "user.name", "Test"},
{"git", "commit", "--allow-empty", "-m", "init"},
}
for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,131 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Register ---
func TestRegister_Good_ServiceRegistered(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
t.Setenv("CORE_BRAIN_KEY", "")
t.Setenv("CORE_BRAIN_URL", "")
c := core.New(core.WithService(Register))
require.NotNil(t, c)
// Service auto-registered under the last segment of the package path: "agentic"
prep, ok := core.ServiceFor[*PrepSubsystem](c, "agentic")
assert.True(t, ok, "PrepSubsystem must be registered as \"agentic\"")
assert.NotNil(t, prep)
}
func TestRegister_Good_CoreWired(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
c := core.New(core.WithService(Register))
prep, ok := core.ServiceFor[*PrepSubsystem](c, "agentic")
require.True(t, ok)
// Register must wire s.core — service needs it for config access
assert.NotNil(t, prep.core, "Register must set prep.core")
assert.Equal(t, c, prep.core)
}
func TestRegister_Good_AgentsConfigLoaded(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
c := core.New(core.WithService(Register))
// Register stores agents.concurrency into Core Config — verify it is present
concurrency := core.ConfigGet[map[string]ConcurrencyLimit](c.Config(), "agents.concurrency")
assert.NotNil(t, concurrency, "Register must store agents.concurrency in Core Config")
}
// --- OnStartup ---
func TestOnStartup_Good_CreatesPokeCh(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
assert.Nil(t, s.pokeCh, "pokeCh should be nil before OnStartup")
err := s.OnStartup(context.Background())
require.NoError(t, err)
assert.NotNil(t, s.pokeCh, "OnStartup must initialise pokeCh via StartRunner")
}
func TestOnStartup_Good_FrozenByDefault(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
require.NoError(t, s.OnStartup(context.Background()))
assert.True(t, s.frozen, "queue must be frozen after OnStartup without CORE_AGENT_DISPATCH=1")
}
func TestOnStartup_Good_NoError(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
err := s.OnStartup(context.Background())
assert.NoError(t, err)
}
// --- OnShutdown ---
func TestOnShutdown_Good_FreezesQueue(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
s := &PrepSubsystem{frozen: false}
err := s.OnShutdown(context.Background())
require.NoError(t, err)
assert.True(t, s.frozen, "OnShutdown must set frozen=true")
}
func TestOnShutdown_Good_AlreadyFrozen(t *testing.T) {
// Calling OnShutdown twice must be idempotent
s := &PrepSubsystem{frozen: true}
err := s.OnShutdown(context.Background())
require.NoError(t, err)
assert.True(t, s.frozen)
}
func TestOnShutdown_Good_NoError(t *testing.T) {
s := &PrepSubsystem{}
assert.NoError(t, s.OnShutdown(context.Background()))
}
func TestOnShutdown_Ugly_NilCore(t *testing.T) {
// OnShutdown must not panic even if s.core is nil
s := &PrepSubsystem{core: nil, frozen: false}
assert.NotPanics(t, func() {
_ = s.OnShutdown(context.Background())
})
assert.True(t, s.frozen)
}

282
pkg/agentic/scan_test.go Normal file
View file

@ -0,0 +1,282 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockScanServer creates a server that handles repo listing and issue listing.
func mockScanServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
// List org repos
mux.HandleFunc("/api/v1/orgs/core/repos", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{"name": "go-io", "full_name": "core/go-io"},
{"name": "go-log", "full_name": "core/go-log"},
{"name": "agent", "full_name": "core/agent"},
})
})
// List issues for repos
mux.HandleFunc("/api/v1/repos/core/go-io/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 10,
"title": "Replace fmt.Errorf with E()",
"labels": []map[string]any{{"name": "agentic"}},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-io/issues/10",
},
{
"number": 11,
"title": "Add missing tests",
"labels": []map[string]any{{"name": "agentic"}, {"name": "help-wanted"}},
"assignee": map[string]any{"login": "virgil"},
"html_url": "https://forge.lthn.ai/core/go-io/issues/11",
},
})
})
mux.HandleFunc("/api/v1/repos/core/go-log/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 5,
"title": "Fix log rotation",
"labels": []map[string]any{{"name": "bug"}},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-log/issues/5",
},
})
})
mux.HandleFunc("/api/v1/repos/core/agent/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
// --- scan ---
func TestScan_Good_AllRepos(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Greater(t, out.Count, 0)
}
func TestScan_Good_WithLimit(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{Limit: 1})
require.NoError(t, err)
assert.True(t, out.Success)
assert.LessOrEqual(t, out.Count, 1)
}
func TestScan_Good_DefaultLabels(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Default labels: agentic, help-wanted, bug
_, out, err := s.scan(context.Background(), nil, ScanInput{})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestScan_Good_CustomLabels(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{
Labels: []string{"bug"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestScan_Good_Deduplicates(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Two labels that return the same issues — should be deduped
_, out, err := s.scan(context.Background(), nil, ScanInput{
Labels: []string{"agentic", "help-wanted"},
Limit: 50,
})
require.NoError(t, err)
assert.True(t, out.Success)
// Check no duplicates (same repo+number)
seen := make(map[string]bool)
for _, issue := range out.Issues {
key := issue.Repo + "#" + itoa(issue.Number)
assert.False(t, seen[key], "duplicate issue: %s", key)
seen[key] = true
}
}
func TestScan_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.scan(context.Background(), nil, ScanInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
// --- listRepoIssues ---
func TestListRepoIssues_Good_ReturnsIssues(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
require.NoError(t, err)
assert.Len(t, issues, 2)
assert.Equal(t, "go-io", issues[0].Repo)
assert.Equal(t, 10, issues[0].Number)
assert.Contains(t, issues[0].Labels, "agentic")
}
func TestListRepoIssues_Good_EmptyResult(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "agent", "agentic")
require.NoError(t, err)
assert.Empty(t, issues)
}
func TestListRepoIssues_Good_AssigneeExtracted(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
require.NoError(t, err)
require.Len(t, issues, 2)
assert.Equal(t, "", issues[0].Assignee)
assert.Equal(t, "virgil", issues[1].Assignee)
}
func TestListRepoIssues_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
assert.Error(t, err)
}
func TestListRepoIssues_Good_URLRewrite(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 1,
"title": "Test",
"labels": []map[string]any{},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-io/issues/1",
},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "")
require.NoError(t, err)
require.Len(t, issues, 1)
// URL should be rewritten to use the mock server URL
assert.Contains(t, issues[0].URL, srv.URL)
}

View file

@ -0,0 +1,535 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- status tool ---
func TestStatus_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.status(context.Background(), nil, StatusInput{})
require.NoError(t, err)
assert.Equal(t, 0, out.Total)
assert.Equal(t, 0, out.Running)
assert.Equal(t, 0, out.Completed)
}
func TestStatus_Good_MixedWorkspaces(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsRoot := filepath.Join(root, "workspace")
// Create completed workspace (old layout)
ws1 := filepath.Join(wsRoot, "task-1")
require.True(t, fs.EnsureDir(ws1).OK)
require.NoError(t, writeStatus(ws1, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Agent: "codex",
}))
// Create failed workspace (old layout)
ws2 := filepath.Join(wsRoot, "task-2")
require.True(t, fs.EnsureDir(ws2).OK)
require.NoError(t, writeStatus(ws2, &WorkspaceStatus{
Status: "failed",
Repo: "go-log",
Agent: "claude",
}))
// Create blocked workspace (old layout)
ws3 := filepath.Join(wsRoot, "task-3")
require.True(t, fs.EnsureDir(ws3).OK)
require.NoError(t, writeStatus(ws3, &WorkspaceStatus{
Status: "blocked",
Repo: "agent",
Agent: "gemini",
Question: "Which API version?",
}))
// Create queued workspace (old layout)
ws4 := filepath.Join(wsRoot, "task-4")
require.True(t, fs.EnsureDir(ws4).OK)
require.NoError(t, writeStatus(ws4, &WorkspaceStatus{
Status: "queued",
Repo: "go-mcp",
Agent: "codex",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.status(context.Background(), nil, StatusInput{})
require.NoError(t, err)
assert.Equal(t, 4, out.Total)
assert.Equal(t, 1, out.Completed)
assert.Equal(t, 1, out.Failed)
assert.Equal(t, 1, out.Queued)
assert.Len(t, out.Blocked, 1)
assert.Equal(t, "Which API version?", out.Blocked[0].Question)
assert.Equal(t, "agent", out.Blocked[0].Repo)
}
func TestStatus_Good_DeepLayout(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsRoot := filepath.Join(root, "workspace")
// Create workspace in deep layout (org/repo/task)
ws := filepath.Join(wsRoot, "core", "go-io", "task-15")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Agent: "codex",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.status(context.Background(), nil, StatusInput{})
require.NoError(t, err)
assert.Equal(t, 1, out.Total)
assert.Equal(t, 1, out.Completed)
}
func TestStatus_Good_CorruptStatusFile(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsRoot := filepath.Join(root, "workspace")
ws := filepath.Join(wsRoot, "corrupt-ws")
require.True(t, fs.EnsureDir(ws).OK)
require.True(t, fs.Write(filepath.Join(ws, "status.json"), "invalid-json{{{").OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.status(context.Background(), nil, StatusInput{})
require.NoError(t, err)
assert.Equal(t, 1, out.Total)
assert.Equal(t, 1, out.Failed) // corrupt status counts as failed
}
// --- shutdown tools ---
func TestDispatchStart_Good(t *testing.T) {
s := &PrepSubsystem{
frozen: true,
pokeCh: make(chan struct{}, 1),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.dispatchStart(context.Background(), nil, ShutdownInput{})
require.NoError(t, err)
assert.True(t, out.Success)
assert.False(t, s.frozen)
assert.Contains(t, out.Message, "started")
}
func TestShutdownGraceful_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
frozen: false,
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.shutdownGraceful(context.Background(), nil, ShutdownInput{})
require.NoError(t, err)
assert.True(t, out.Success)
assert.True(t, s.frozen)
assert.Contains(t, out.Message, "frozen")
}
func TestShutdownNow_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.True(t, fs.EnsureDir(filepath.Join(root, "workspace")).OK)
s := &PrepSubsystem{
frozen: false,
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.shutdownNow(context.Background(), nil, ShutdownInput{})
require.NoError(t, err)
assert.True(t, out.Success)
assert.True(t, s.frozen)
assert.Contains(t, out.Message, "killed 0")
}
func TestShutdownNow_Good_ClearsQueued(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsRoot := filepath.Join(root, "workspace")
// Create queued workspaces
for i := 1; i <= 3; i++ {
ws := filepath.Join(wsRoot, "task-"+itoa(i))
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "queued",
Repo: "go-io",
Agent: "codex",
}))
}
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.shutdownNow(context.Background(), nil, ShutdownInput{})
require.NoError(t, err)
assert.Contains(t, out.Message, "cleared 3")
// Verify queued workspaces are now failed
for i := 1; i <= 3; i++ {
ws := filepath.Join(wsRoot, "task-"+itoa(i))
st, err := ReadStatus(ws)
require.NoError(t, err)
assert.Equal(t, "failed", st.Status)
assert.Contains(t, st.Question, "cleared by shutdown_now")
}
}
// --- brainRecall ---
func TestBrainRecall_Good_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Contains(t, r.URL.Path, "/v1/brain/recall")
json.NewEncoder(w).Encode(map[string]any{
"memories": []map[string]any{
{"type": "architecture", "content": "Core uses DI pattern", "project": "go-core"},
{"type": "convention", "content": "Use E() for errors", "project": "go-core"},
},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result, count := s.brainRecall(context.Background(), "go-core")
assert.Equal(t, 2, count)
assert.Contains(t, result, "Core uses DI pattern")
assert.Contains(t, result, "Use E() for errors")
}
func TestBrainRecall_Good_NoMemories(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"memories": []map[string]any{},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result, count := s.brainRecall(context.Background(), "go-core")
assert.Equal(t, 0, count)
assert.Empty(t, result)
}
func TestBrainRecall_Bad_NoBrainKey(t *testing.T) {
s := &PrepSubsystem{
brainKey: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result, count := s.brainRecall(context.Background(), "go-core")
assert.Equal(t, 0, count)
assert.Empty(t, result)
}
func TestBrainRecall_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
brainURL: srv.URL,
brainKey: "test-brain-key",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result, count := s.brainRecall(context.Background(), "go-core")
assert.Equal(t, 0, count)
assert.Empty(t, result)
}
// --- prepWorkspace ---
func TestPrepWorkspace_Bad_NoRepo(t *testing.T) {
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.prepWorkspace(context.Background(), nil, PrepInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo is required")
}
func TestPrepWorkspace_Bad_NoIdentifier(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
codePath: t.TempDir(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.prepWorkspace(context.Background(), nil, PrepInput{
Repo: "go-io",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "one of issue, pr, branch, or tag is required")
}
func TestPrepWorkspace_Bad_InvalidRepoName(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
codePath: t.TempDir(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.prepWorkspace(context.Background(), nil, PrepInput{
Repo: "..",
Issue: 1,
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid repo name")
}
// --- listPRs ---
func TestListPRs_Good_SpecificRepo(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Return mock PRs
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 1,
"title": "Fix tests",
"state": "open",
"html_url": "https://forge.test/core/go-io/pulls/1",
"mergeable": true,
"user": map[string]any{"login": "virgil"},
"head": map[string]any{"ref": "agent/fix-tests"},
"base": map[string]any{"ref": "dev"},
"labels": []map[string]any{{"name": "agentic"}},
},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.listPRs(context.Background(), nil, ListPRsInput{
Repo: "go-io",
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Equal(t, 1, out.Count)
assert.Equal(t, "Fix tests", out.PRs[0].Title)
assert.Equal(t, "virgil", out.PRs[0].Author)
assert.Equal(t, "agent/fix-tests", out.PRs[0].Branch)
assert.Contains(t, out.PRs[0].Labels, "agentic")
}
// --- Poke ---
func TestPoke_Good_SendsSignal(t *testing.T) {
s := &PrepSubsystem{
pokeCh: make(chan struct{}, 1),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.Poke()
// Should have something in the channel
select {
case <-s.pokeCh:
// ok
default:
t.Fatal("expected poke signal in channel")
}
}
func TestPoke_Good_NonBlocking(t *testing.T) {
s := &PrepSubsystem{
pokeCh: make(chan struct{}, 1),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Fill the channel
s.pokeCh <- struct{}{}
// Second poke should not block
assert.NotPanics(t, func() {
s.Poke()
})
}
func TestPoke_Bad_NilChannel(t *testing.T) {
s := &PrepSubsystem{
pokeCh: nil,
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should not panic with nil channel
assert.NotPanics(t, func() {
s.Poke()
})
}
// --- ReadStatus / writeStatus (extended) ---
func TestWriteReadStatus_Good_WithPID(t *testing.T) {
dir := t.TempDir()
st := &WorkspaceStatus{
Status: "running",
Agent: "codex",
Repo: "go-io",
Task: "Fix it",
PID: 12345,
}
err := writeStatus(dir, st)
require.NoError(t, err)
// Read it back
got, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "running", got.Status)
assert.Equal(t, "codex", got.Agent)
assert.Equal(t, "go-io", got.Repo)
assert.Equal(t, 12345, got.PID)
assert.False(t, got.UpdatedAt.IsZero())
}
func TestWriteReadStatus_Good_AllFields(t *testing.T) {
dir := t.TempDir()
now := time.Now()
st := &WorkspaceStatus{
Status: "blocked",
Agent: "claude",
Repo: "go-log",
Org: "core",
Task: "Add structured logging",
Branch: "agent/add-logging",
Issue: 42,
PID: 99999,
StartedAt: now,
Question: "Which log format?",
Runs: 3,
PRURL: "https://forge.test/core/go-log/pulls/5",
}
err := writeStatus(dir, st)
require.NoError(t, err)
got, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "blocked", got.Status)
assert.Equal(t, "claude", got.Agent)
assert.Equal(t, "core", got.Org)
assert.Equal(t, 42, got.Issue)
assert.Equal(t, "Which log format?", got.Question)
assert.Equal(t, 3, got.Runs)
assert.Equal(t, "https://forge.test/core/go-log/pulls/5", got.PRURL)
}
// --- OnStartup / OnShutdown ---
func TestOnShutdown_Good(t *testing.T) {
s := &PrepSubsystem{
frozen: false,
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.OnShutdown(context.Background())
assert.NoError(t, err)
assert.True(t, s.frozen)
}
// --- drainQueue ---
func TestDrainQueue_Good_FrozenDoesNothing(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{
frozen: true,
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return immediately when frozen
assert.NotPanics(t, func() {
s.drainQueue()
})
}

View file

@ -0,0 +1,176 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"encoding/json"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- ReadStatus ---
func TestReadStatus_Good_AllFields(t *testing.T) {
dir := t.TempDir()
now := time.Now().Truncate(time.Second)
original := WorkspaceStatus{
Status: "running",
Agent: "claude:opus",
Repo: "go-io",
Org: "core",
Task: "add observability",
Branch: "agent/add-observability",
Issue: 7,
PID: 42100,
StartedAt: now,
UpdatedAt: now,
Question: "",
Runs: 2,
PRURL: "",
}
data, err := json.MarshalIndent(original, "", " ")
require.NoError(t, err)
require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK)
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, original.Status, st.Status)
assert.Equal(t, original.Agent, st.Agent)
assert.Equal(t, original.Repo, st.Repo)
assert.Equal(t, original.Org, st.Org)
assert.Equal(t, original.Task, st.Task)
assert.Equal(t, original.Branch, st.Branch)
assert.Equal(t, original.Issue, st.Issue)
assert.Equal(t, original.PID, st.PID)
assert.Equal(t, original.Runs, st.Runs)
}
func TestReadStatus_Bad_MissingFile(t *testing.T) {
dir := t.TempDir()
_, err := ReadStatus(dir)
assert.Error(t, err, "missing status.json must return an error")
}
func TestReadStatus_Bad_CorruptJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "status.json"), `{"status": "running", broken`).OK)
_, err := ReadStatus(dir)
assert.Error(t, err, "corrupt JSON must return an error")
}
func TestReadStatus_Bad_NullJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "status.json"), "null").OK)
// null is valid JSON — ReadStatus returns a zero-value struct, not an error
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "", st.Status)
}
// --- writeStatus ---
func TestWriteStatus_Good_WritesAndReadsBack(t *testing.T) {
dir := t.TempDir()
st := &WorkspaceStatus{
Status: "queued",
Agent: "gemini:pro",
Repo: "go-log",
Task: "improve logging",
Runs: 0,
}
err := writeStatus(dir, st)
require.NoError(t, err)
read, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "queued", read.Status)
assert.Equal(t, "gemini:pro", read.Agent)
assert.Equal(t, "go-log", read.Repo)
assert.Equal(t, "improve logging", read.Task)
}
func TestWriteStatus_Good_SetsUpdatedAt(t *testing.T) {
dir := t.TempDir()
before := time.Now().Add(-time.Millisecond)
st := &WorkspaceStatus{Status: "failed", Agent: "codex"}
err := writeStatus(dir, st)
require.NoError(t, err)
assert.True(t, st.UpdatedAt.After(before), "writeStatus must set UpdatedAt to a recent time")
}
func TestWriteStatus_Good_Overwrites(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeStatus(dir, &WorkspaceStatus{Status: "running", Agent: "gemini"}))
require.NoError(t, writeStatus(dir, &WorkspaceStatus{Status: "completed", Agent: "gemini"}))
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "completed", st.Status)
}
// --- WorkspaceStatus JSON round-trip ---
func TestWorkspaceStatus_Good_JSONRoundTrip(t *testing.T) {
now := time.Now().Truncate(time.Second)
original := WorkspaceStatus{
Status: "blocked",
Agent: "codex:gpt-5.4",
Repo: "agent",
Org: "core",
Task: "write more tests",
Branch: "agent/write-more-tests",
Issue: 15,
PID: 99001,
StartedAt: now,
UpdatedAt: now,
Question: "Which pattern should I use?",
Runs: 3,
PRURL: "https://forge.lthn.ai/core/agent/pulls/10",
}
data, err := json.Marshal(original)
require.NoError(t, err)
var decoded WorkspaceStatus
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Agent, decoded.Agent)
assert.Equal(t, original.Repo, decoded.Repo)
assert.Equal(t, original.Org, decoded.Org)
assert.Equal(t, original.Task, decoded.Task)
assert.Equal(t, original.Branch, decoded.Branch)
assert.Equal(t, original.Issue, decoded.Issue)
assert.Equal(t, original.PID, decoded.PID)
assert.Equal(t, original.Question, decoded.Question)
assert.Equal(t, original.Runs, decoded.Runs)
assert.Equal(t, original.PRURL, decoded.PRURL)
}
func TestWorkspaceStatus_Good_OmitemptyFields(t *testing.T) {
st := WorkspaceStatus{Status: "queued", Agent: "claude"}
data, err := json.Marshal(st)
require.NoError(t, err)
// Optional fields with omitempty must be absent when zero
jsonStr := string(data)
assert.NotContains(t, jsonStr, `"org"`)
assert.NotContains(t, jsonStr, `"branch"`)
assert.NotContains(t, jsonStr, `"question"`)
assert.NotContains(t, jsonStr, `"pr_url"`)
assert.NotContains(t, jsonStr, `"pid"`)
assert.NotContains(t, jsonStr, `"issue"`)
}

509
pkg/agentic/verify_test.go Normal file
View file

@ -0,0 +1,509 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- forgeMergePR ---
func TestForgeMergePR_Good_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Contains(t, r.URL.Path, "/pulls/42/merge")
assert.Equal(t, "token test-forge-token", r.Header.Get("Authorization"))
var body map[string]any
json.NewDecoder(r.Body).Decode(&body)
assert.Equal(t, "merge", body["Do"])
assert.Equal(t, true, body["delete_branch_after_merge"])
w.WriteHeader(200)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-forge-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.forgeMergePR(context.Background(), "core", "test-repo", 42)
assert.NoError(t, err)
}
func TestForgeMergePR_Good_204Response(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204) // No Content — also valid success
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.forgeMergePR(context.Background(), "core", "test-repo", 1)
assert.NoError(t, err)
}
func TestForgeMergePR_Bad_ConflictResponse(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(409)
json.NewEncoder(w).Encode(map[string]any{
"message": "merge conflict",
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.forgeMergePR(context.Background(), "core", "test-repo", 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "409")
assert.Contains(t, err.Error(), "merge conflict")
}
func TestForgeMergePR_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]any{
"message": "internal server error",
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.forgeMergePR(context.Background(), "core", "test-repo", 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "500")
}
func TestForgeMergePR_Bad_NetworkError(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close() // close immediately to cause connection error
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
err := s.forgeMergePR(context.Background(), "core", "test-repo", 1)
assert.Error(t, err)
}
// --- extractPRNumber (additional _Ugly cases) ---
func TestExtractPRNumber_Ugly_DoubleSlashEnd(t *testing.T) {
assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/42/"))
}
func TestExtractPRNumber_Ugly_VeryLargeNumber(t *testing.T) {
assert.Equal(t, 999999, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/999999"))
}
func TestExtractPRNumber_Ugly_NegativeNumber(t *testing.T) {
// atoi of "-5" is -5, parseInt wraps atoi
assert.Equal(t, -5, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/-5"))
}
func TestExtractPRNumber_Ugly_ZeroExplicit(t *testing.T) {
assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/0"))
}
// --- ensureLabel ---
func TestEnsureLabel_Good_CreatesLabel(t *testing.T) {
called := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Contains(t, r.URL.Path, "/labels")
called = true
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
assert.Equal(t, "needs-review", body["name"])
assert.Equal(t, "#e11d48", body["color"])
w.WriteHeader(201)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.ensureLabel(context.Background(), "core", "test-repo", "needs-review", "e11d48")
assert.True(t, called)
}
func TestEnsureLabel_Bad_NetworkError(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close()
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should not panic
assert.NotPanics(t, func() {
s.ensureLabel(context.Background(), "core", "test-repo", "test-label", "abc123")
})
}
// --- getLabelID ---
func TestGetLabelID_Good_Found(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{"id": 10, "name": "agentic"},
{"id": 20, "name": "needs-review"},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
id := s.getLabelID(context.Background(), "core", "test-repo", "needs-review")
assert.Equal(t, 20, id)
}
func TestGetLabelID_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{"id": 10, "name": "agentic"},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
id := s.getLabelID(context.Background(), "core", "test-repo", "missing-label")
assert.Equal(t, 0, id)
}
func TestGetLabelID_Bad_NetworkError(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close()
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: &http.Client{},
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
id := s.getLabelID(context.Background(), "core", "test-repo", "any")
assert.Equal(t, 0, id)
}
// --- runVerification ---
func TestRunVerification_Good_NoProjectFile(t *testing.T) {
dir := t.TempDir() // No go.mod, composer.json, or package.json
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runVerification(dir)
assert.True(t, result.passed)
assert.Equal(t, "none", result.testCmd)
}
func TestRunVerification_Good_GoProject(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "go.mod"), "module test").OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runVerification(dir)
assert.Equal(t, "go test ./...", result.testCmd)
// It will fail because there's no real Go code, but we test the detection path
}
func TestRunVerification_Good_PHPProject(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "composer.json"), `{"require":{}}`).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runVerification(dir)
// Will fail (no composer) but detection path is covered
assert.Contains(t, []string{"composer test", "vendor/bin/pest", "none"}, result.testCmd)
}
func TestRunVerification_Good_NodeProject(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "package.json"), `{"scripts":{"test":"echo ok"}}`).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runVerification(dir)
assert.Equal(t, "npm test", result.testCmd)
}
func TestRunVerification_Good_NodeNoTestScript(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "package.json"), `{"scripts":{}}`).OK)
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
result := s.runVerification(dir)
assert.True(t, result.passed)
assert.Equal(t, "none", result.testCmd)
}
// --- fileExists ---
func TestFileExists_Good_Exists(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
require.True(t, fs.Write(path, "hello").OK)
assert.True(t, fileExists(path))
}
func TestFileExists_Bad_NotExists(t *testing.T) {
assert.False(t, fileExists("/nonexistent/path/file.txt"))
}
func TestFileExists_Bad_IsDirectory(t *testing.T) {
dir := t.TempDir()
assert.False(t, fileExists(dir)) // directories are not files
}
// --- autoVerifyAndMerge ---
func TestAutoVerifyAndMerge_Bad_NoStatus(t *testing.T) {
dir := t.TempDir()
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should not panic when status.json is missing
assert.NotPanics(t, func() {
s.autoVerifyAndMerge(dir)
})
}
func TestAutoVerifyAndMerge_Bad_NoPRURL(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeStatus(dir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/fix",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Should return early — no PR URL
assert.NotPanics(t, func() {
s.autoVerifyAndMerge(dir)
})
}
func TestAutoVerifyAndMerge_Bad_EmptyRepo(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeStatus(dir, &WorkspaceStatus{
Status: "completed",
PRURL: "https://forge.test/core/go-io/pulls/1",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
assert.NotPanics(t, func() {
s.autoVerifyAndMerge(dir)
})
}
func TestAutoVerifyAndMerge_Bad_InvalidPRURL(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeStatus(dir, &WorkspaceStatus{
Status: "completed",
Repo: "go-io",
Branch: "agent/fix",
PRURL: "not-a-url",
}))
s := &PrepSubsystem{
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// extractPRNumber returns 0 for invalid URL, so autoVerifyAndMerge returns early
assert.NotPanics(t, func() {
s.autoVerifyAndMerge(dir)
})
}
// --- flagForReview ---
func TestFlagForReview_Good_AddsLabel(t *testing.T) {
labelCalled := false
commentCalled := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && containsStr(r.URL.Path, "/labels") {
labelCalled = true
if containsStr(r.URL.Path, "/issues/") {
w.WriteHeader(200) // add label to issue
} else {
w.WriteHeader(201) // create label
}
return
}
if r.Method == "GET" && containsStr(r.URL.Path, "/labels") {
json.NewEncoder(w).Encode([]map[string]any{
{"id": 99, "name": "needs-review"},
})
return
}
if r.Method == "POST" && containsStr(r.URL.Path, "/comments") {
commentCalled = true
w.WriteHeader(201)
return
}
w.WriteHeader(200)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.flagForReview("core", "test-repo", 42, testFailed)
assert.True(t, labelCalled)
assert.True(t, commentCalled)
}
func TestFlagForReview_Good_MergeConflictMessage(t *testing.T) {
var commentBody string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && containsStr(r.URL.Path, "/labels") {
json.NewEncoder(w).Encode([]map[string]any{})
return
}
if r.Method == "POST" && containsStr(r.URL.Path, "/comments") {
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
commentBody = body["body"]
w.WriteHeader(201)
return
}
w.WriteHeader(201) // default for label creation etc
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
s.flagForReview("core", "test-repo", 1, mergeConflict)
assert.Contains(t, commentBody, "Merge conflict")
}
// --- truncate ---
func TestTruncate_Good_Short(t *testing.T) {
assert.Equal(t, "hello", truncate("hello", 10))
}
func TestTruncate_Good_Exact(t *testing.T) {
assert.Equal(t, "hello", truncate("hello", 5))
}
func TestTruncate_Good_Long(t *testing.T) {
assert.Equal(t, "hel...", truncate("hello world", 3))
}
func TestTruncate_Bad_ZeroMax(t *testing.T) {
assert.Equal(t, "...", truncate("hello", 0))
}
func TestTruncate_Ugly_EmptyString(t *testing.T) {
assert.Equal(t, "", truncate("", 10))
}

414
pkg/monitor/logic_test.go Normal file
View file

@ -0,0 +1,414 @@
// SPDX-License-Identifier: EUPL-1.2
package monitor
import (
"context"
"os"
"path/filepath"
"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 TestHandleAgentStarted_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 TestHandleAgentStarted_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 TestHandleAgentCompleted_Good_NilNotifier(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New()
// notifier 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 TestHandleAgentCompleted_Good_WithNotifier(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New()
notifier := &mockNotifier{}
mon.SetNotifier(notifier)
ev := messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "ws-2", Status: "completed"}
mon.handleAgentCompleted(ev)
// Give the goroutine spawned by checkIdleAfterDelay time to not fire within test
// (it has a 5s sleep inside, so we just verify the notifier got the immediate event)
events := notifier.Events()
require.GreaterOrEqual(t, len(events), 1)
assert.Equal(t, "agent.completed", events[0].channel)
data := events[0].data.(map[string]any)
assert.Equal(t, "go-io", data["repo"])
assert.Equal(t, "codex", data["agent"])
assert.Equal(t, "ws-2", data["workspace"])
assert.Equal(t, "completed", data["status"])
}
func TestHandleAgentCompleted_Bad_EmptyFields(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New()
notifier := &mockNotifier{}
mon.SetNotifier(notifier)
// All fields empty — must not panic.
ev := messages.AgentCompleted{}
assert.NotPanics(t, func() { mon.handleAgentCompleted(ev) })
events := notifier.Events()
require.GreaterOrEqual(t, len(events), 1)
assert.Equal(t, "agent.completed", events[0].channel)
}
// --- checkIdleAfterDelay ---
func TestCheckIdleAfterDelay_Bad_NilNotifier(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New() // notifier is nil
// Should return immediately without panic after the 5s sleep.
// We override the sleep by calling it via a short-circuit: replace the
// notifier check path — we just verify it doesn't panic and returns.
done := make(chan struct{})
go func() {
// checkIdleAfterDelay has a time.Sleep(5s) — call with nil notifier path.
// To avoid a 5-second wait we test the "notifier == nil" return branch
// by only exercising the guard directly.
if mon.notifier == nil {
close(done)
return
}
mon.checkIdleAfterDelay()
close(done)
}()
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("checkIdleAfterDelay nil-notifier guard did not return quickly")
}
}
func TestCheckIdleAfterDelay_Good_EmptyWorkspace(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New()
notifier := &mockNotifier{}
mon.SetNotifier(notifier)
// With empty workspace, running=0 and queued=0, so queue.drained fires.
// We run countLiveWorkspaces + the notifier call path directly to avoid the
// 5s sleep in checkIdleAfterDelay.
running, queued := mon.countLiveWorkspaces()
assert.Equal(t, 0, running)
assert.Equal(t, 0, queued)
if running == 0 && queued == 0 {
mon.notifier.ChannelSend(context.Background(), "queue.drained", map[string]any{
"running": running,
"queued": queued,
})
}
events := notifier.Events()
require.Len(t, events, 1)
assert.Equal(t, "queue.drained", events[0].channel)
}
// --- countLiveWorkspaces ---
func TestCountLiveWorkspaces_Good_EmptyWorkspace(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New()
running, queued := mon.countLiveWorkspaces()
assert.Equal(t, 0, running)
assert.Equal(t, 0, queued)
}
func TestCountLiveWorkspaces_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 TestCountLiveWorkspaces_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 TestCountLiveWorkspaces_Good_RunningLivePID(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
// Current process is definitely alive.
pid := os.Getpid()
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 TestPidAlive_Good_CurrentProcess(t *testing.T) {
pid := os.Getpid()
assert.True(t, pidAlive(pid), "current process must be alive")
}
func TestPidAlive_Bad_DeadPID(t *testing.T) {
// PID 99999999 is virtually guaranteed to not exist.
assert.False(t, pidAlive(99999999))
}
func TestPidAlive_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 TestPidAlive_Ugly_NegativePID(t *testing.T) {
// Negative PID is invalid. Must not panic.
assert.NotPanics(t, func() { pidAlive(-1) })
}
// --- SetCore ---
func TestSetCore_Good_RegistersIPCHandler(t *testing.T) {
c := core.New()
mon := New()
// SetCore must not panic and must wire mon.core.
assert.NotPanics(t, func() { mon.SetCore(c) })
assert.Equal(t, c, mon.core)
}
func TestSetCore_Good_IPCHandlerFires(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
c := core.New()
mon := New()
mon.SetCore(c)
// 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 TestSetCore_Good_CompletedIPCHandler(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
c := core.New()
mon := New()
mon.SetCore(c)
// 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 TestOnStartup_Good_StartsLoop(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
home := t.TempDir()
t.Setenv("HOME", home)
mon := New(Options{Interval: 1 * time.Hour})
err := mon.OnStartup(context.Background())
require.NoError(t, err)
// cancel must be non-nil after startup (loop running)
assert.NotNil(t, mon.cancel)
// Cleanup.
require.NoError(t, mon.OnShutdown(context.Background()))
}
func TestOnStartup_Good_NoError(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
mon := New(Options{Interval: 1 * time.Hour})
assert.NoError(t, mon.OnStartup(context.Background()))
_ = mon.OnShutdown(context.Background())
}
func TestOnShutdown_Good_NoError(t *testing.T) {
mon := New(Options{Interval: 1 * time.Hour})
assert.NoError(t, mon.OnShutdown(context.Background()))
}
func TestOnShutdown_Good_StopsLoop(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
home := t.TempDir()
t.Setenv("HOME", home)
mon := New(Options{Interval: 1 * time.Hour})
require.NoError(t, mon.OnStartup(context.Background()))
done := make(chan error, 1)
go func() {
done <- mon.OnShutdown(context.Background())
}()
select {
case err := <-done:
assert.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("OnShutdown did not return in time")
}
}
func TestOnShutdown_Ugly_NilCancel(t *testing.T) {
// OnShutdown without prior OnStartup must not panic.
mon := New()
assert.NotPanics(t, func() {
_ = mon.OnShutdown(context.Background())
})
}
// --- Register ---
func TestRegister_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 TestRegister_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 mon.core to the Core instance.
assert.Equal(t, c, svc.core)
}
func TestRegister_Good_IPCHandlerActive(t *testing.T) {
wsRoot := t.TempDir()
t.Setenv("CORE_WORKSPACE", wsRoot)
require.NoError(t, os.MkdirAll(filepath.Join(wsRoot, "workspace"), 0755))
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"])
}