diff --git a/pkg/agentic/auto_pr_test.go b/pkg/agentic/auto_pr_test.go new file mode 100644 index 0000000..9815b26 --- /dev/null +++ b/pkg/agentic/auto_pr_test.go @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAutoPR_AutoCreatePR_Good(t *testing.T) { + t.Skip("needs real git + forge integration") +} + +func TestAutoPR_AutoCreatePR_Bad(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + // No status file → early return (no panic) + wsNoStatus := filepath.Join(root, "ws-no-status") + require.NoError(t, os.MkdirAll(wsNoStatus, 0o755)) + assert.NotPanics(t, func() { + s.autoCreatePR(wsNoStatus) + }) + + // Empty branch → early return + wsNoBranch := filepath.Join(root, "ws-no-branch") + require.NoError(t, os.MkdirAll(wsNoBranch, 0o755)) + st := &WorkspaceStatus{ + Status: "completed", + Agent: "codex", + Repo: "go-io", + Branch: "", + } + data, err := json.MarshalIndent(st, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(wsNoBranch, "status.json"), data, 0o644)) + assert.NotPanics(t, func() { + s.autoCreatePR(wsNoBranch) + }) + + // Empty repo → early return + wsNoRepo := filepath.Join(root, "ws-no-repo") + require.NoError(t, os.MkdirAll(wsNoRepo, 0o755)) + st2 := &WorkspaceStatus{ + Status: "completed", + Agent: "codex", + Repo: "", + Branch: "agent/fix-tests", + } + data2, err := json.MarshalIndent(st2, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(wsNoRepo, "status.json"), data2, 0o644)) + assert.NotPanics(t, func() { + s.autoCreatePR(wsNoRepo) + }) +} + +func TestAutoPR_AutoCreatePR_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + // Set up a real git repo with no commits ahead of origin/dev + wsDir := filepath.Join(root, "ws-no-ahead") + repoDir := filepath.Join(wsDir, "repo") + require.NoError(t, os.MkdirAll(repoDir, 0o755)) + + // Init the repo + cmd := exec.Command("git", "init", "-b", "dev", repoDir) + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", repoDir, "config", "user.name", "Test") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", repoDir, "config", "user.email", "test@test.com") + require.NoError(t, cmd.Run()) + + require.NoError(t, os.WriteFile(filepath.Join(repoDir, "README.md"), []byte("# test"), 0o644)) + cmd = exec.Command("git", "-C", repoDir, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", repoDir, "commit", "-m", "init") + require.NoError(t, cmd.Run()) + + // Write status with valid branch + repo + st := &WorkspaceStatus{ + Status: "completed", + Agent: "codex", + Repo: "go-io", + Branch: "agent/fix-tests", + StartedAt: time.Now(), + } + data, err := json.MarshalIndent(st, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644)) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + // git log origin/dev..HEAD will fail (no origin remote) → early return + assert.NotPanics(t, func() { + s.autoCreatePR(wsDir) + }) +} diff --git a/pkg/agentic/paths_test.go b/pkg/agentic/paths_test.go index 1bf8216..68fb28e 100644 --- a/pkg/agentic/paths_test.go +++ b/pkg/agentic/paths_test.go @@ -4,10 +4,12 @@ package agentic import ( "os" + "os/exec" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCoreRoot_Good_EnvVar(t *testing.T) { @@ -142,3 +144,56 @@ func TestGeneratePlanID_Good(t *testing.T) { assert.True(t, len(id) > 0) assert.True(t, strings.Contains(id, "fix-the-login-bug")) } + +// --- DefaultBranch --- + +func TestPaths_DefaultBranch_Good(t *testing.T) { + dir := t.TempDir() + + // Init git repo with "main" branch + cmd := exec.Command("git", "init", "-b", "main", dir) + require.NoError(t, cmd.Run()) + + cmd = exec.Command("git", "-C", dir, "config", "user.name", "Test") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", dir, "config", "user.email", "test@test.com") + require.NoError(t, cmd.Run()) + + require.NoError(t, os.WriteFile(dir+"/README.md", []byte("# Test"), 0o644)) + cmd = exec.Command("git", "-C", dir, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", dir, "commit", "-m", "init") + require.NoError(t, cmd.Run()) + + branch := DefaultBranch(dir) + assert.Equal(t, "main", branch) +} + +func TestPaths_DefaultBranch_Bad(t *testing.T) { + // Non-git directory — should return "main" (default) + dir := t.TempDir() + branch := DefaultBranch(dir) + assert.Equal(t, "main", branch) +} + +func TestPaths_DefaultBranch_Ugly(t *testing.T) { + dir := t.TempDir() + + // Init git repo with "master" branch + cmd := exec.Command("git", "init", "-b", "master", dir) + require.NoError(t, cmd.Run()) + + cmd = exec.Command("git", "-C", dir, "config", "user.name", "Test") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", dir, "config", "user.email", "test@test.com") + require.NoError(t, cmd.Run()) + + require.NoError(t, os.WriteFile(dir+"/README.md", []byte("# Test"), 0o644)) + cmd = exec.Command("git", "-C", dir, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", dir, "commit", "-m", "init") + require.NoError(t, cmd.Run()) + + branch := DefaultBranch(dir) + assert.Equal(t, "master", branch) +} diff --git a/pkg/agentic/prep_extra_test.go b/pkg/agentic/prep_extra_test.go index 7ab8dff..12506ac 100644 --- a/pkg/agentic/prep_extra_test.go +++ b/pkg/agentic/prep_extra_test.go @@ -270,6 +270,93 @@ func TestBuildPrompt_Good_WithIssue(t *testing.T) { assert.Contains(t, prompt, "Steps to reproduce") } +// --- buildPrompt (naming convention tests) --- + +func TestPrep_BuildPrompt_Good(t *testing.T) { + dir := t.TempDir() + // Create go.mod to detect language as "go" + os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\ngo 1.22\n"), 0o644) + + s := &PrepSubsystem{ + codePath: t.TempDir(), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + prompt, memories, consumers := s.buildPrompt(context.Background(), PrepInput{ + Task: "Add unit tests", + Org: "core", + Repo: "go-io", + }, "dev", dir) + + assert.Contains(t, prompt, "TASK: Add unit tests") + assert.Contains(t, prompt, "REPO: core/go-io on branch dev") + assert.Contains(t, prompt, "LANGUAGE: go") + assert.Contains(t, prompt, "BUILD: go build ./...") + assert.Contains(t, prompt, "TEST: go test ./...") + assert.Contains(t, prompt, "CONSTRAINTS:") + assert.Equal(t, 0, memories) + assert.Equal(t, 0, consumers) +} + +func TestPrep_BuildPrompt_Bad(t *testing.T) { + // Empty repo path — still produces a prompt (no crash) + s := &PrepSubsystem{ + codePath: t.TempDir(), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + prompt, memories, consumers := s.buildPrompt(context.Background(), PrepInput{ + Task: "Do something", + Org: "core", + Repo: "go-io", + }, "main", "") + + assert.Contains(t, prompt, "TASK: Do something") + assert.Contains(t, prompt, "CONSTRAINTS:") + assert.Equal(t, 0, memories) + assert.Equal(t, 0, consumers) +} + +func TestPrep_BuildPrompt_Ugly(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\ngo 1.22\n"), 0o644) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ + "number": 99, + "title": "Critical bug", + "body": "Server crashes on startup", + }) + })) + t.Cleanup(srv.Close) + + s := &PrepSubsystem{ + forge: forge.NewForge(srv.URL, "test-token"), + codePath: t.TempDir(), + client: srv.Client(), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + prompt, _, _ := s.buildPrompt(context.Background(), PrepInput{ + Task: "Fix critical bug", + Org: "core", + Repo: "go-io", + Persona: "reviewer", + PlanTemplate: "nonexistent-plan", + Issue: 99, + }, "agent/fix-bug", dir) + + // Persona may or may not resolve, but prompt must still contain core fields + assert.Contains(t, prompt, "TASK: Fix critical bug") + assert.Contains(t, prompt, "REPO: core/go-io on branch agent/fix-bug") + assert.Contains(t, prompt, "ISSUE:") + assert.Contains(t, prompt, "Server crashes on startup") + assert.Contains(t, prompt, "CONSTRAINTS:") +} + // --- runQA --- func TestRunQA_Good_PHPNoComposer(t *testing.T) { diff --git a/pkg/agentic/status_test.go b/pkg/agentic/status_test.go index 94cff9d..b88133e 100644 --- a/pkg/agentic/status_test.go +++ b/pkg/agentic/status_test.go @@ -180,3 +180,77 @@ func TestReadStatus_Ugly_EmptyFile(t *testing.T) { _, err := ReadStatus(dir) assert.Error(t, err) } + +// --- status() dead PID detection --- + +func TestStatus_Status_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + wsRoot := filepath.Join(root, "workspace") + + // Case 1: running + dead PID + BLOCKED.md → should detect as blocked + ws1 := filepath.Join(wsRoot, "dead-blocked") + require.True(t, fs.EnsureDir(filepath.Join(ws1, "repo")).OK) + require.NoError(t, writeStatus(ws1, &WorkspaceStatus{ + Status: "running", + Repo: "go-io", + Agent: "codex", + PID: 999999, + })) + require.True(t, fs.Write(filepath.Join(ws1, "repo", "BLOCKED.md"), "Need API credentials").OK) + + // Case 2: running + dead PID + agent log → completed + ws2 := filepath.Join(wsRoot, "dead-completed") + require.True(t, fs.EnsureDir(filepath.Join(ws2, "repo")).OK) + require.NoError(t, writeStatus(ws2, &WorkspaceStatus{ + Status: "running", + Repo: "go-log", + Agent: "claude", + PID: 999999, + })) + require.True(t, fs.Write(filepath.Join(ws2, "agent-claude.log"), "agent finished ok").OK) + + // Case 3: running + dead PID + no log + no BLOCKED.md → failed + ws3 := filepath.Join(wsRoot, "dead-failed") + require.True(t, fs.EnsureDir(filepath.Join(ws3, "repo")).OK) + require.NoError(t, writeStatus(ws3, &WorkspaceStatus{ + Status: "running", + Repo: "agent", + Agent: "gemini", + PID: 999999, + })) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + _, out, err := s.status(nil, nil, StatusInput{}) + require.NoError(t, err) + assert.Equal(t, 3, out.Total) + + // Verify case 1: blocked + assert.Len(t, out.Blocked, 1) + assert.Equal(t, "go-io", out.Blocked[0].Repo) + assert.Equal(t, "Need API credentials", out.Blocked[0].Question) + + // Verify case 2: completed + assert.Equal(t, 1, out.Completed) + + // Verify case 3: failed + assert.Equal(t, 1, out.Failed) + + // Verify statuses were persisted to disk + st1, err := ReadStatus(ws1) + require.NoError(t, err) + assert.Equal(t, "blocked", st1.Status) + + st2, err := ReadStatus(ws2) + require.NoError(t, err) + assert.Equal(t, "completed", st2.Status) + + st3, err := ReadStatus(ws3) + require.NoError(t, err) + assert.Equal(t, "failed", st3.Status) + assert.Equal(t, "Agent process died (no output log)", st3.Question) +}