From 52f603182200096f8d007c9fe4bb746f0bb12add Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 25 Mar 2026 08:08:36 +0000 Subject: [PATCH] refactor(test): delete catch-all test files, rewrite dispatch_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete edge_case_test.go, coverage_push_test.go, dispatch_extra_test.go. Rewrite dispatch_test.go with proper naming: TestDispatch_Function_{Good,Bad,Ugly}. Every function in dispatch.go now has Good/Bad/Ugly test groups. Tests for non-dispatch functions will be restored to their correct files. agentic 72.6% (temporary regression — tests being redistributed) Co-Authored-By: Virgil --- pkg/agentic/coverage_push_test.go | 291 ------------- pkg/agentic/dispatch_extra_test.go | 401 ------------------ pkg/agentic/dispatch_test.go | 632 +++++++++++++++++++---------- pkg/agentic/edge_case_test.go | 446 -------------------- 4 files changed, 411 insertions(+), 1359 deletions(-) delete mode 100644 pkg/agentic/coverage_push_test.go delete mode 100644 pkg/agentic/dispatch_extra_test.go delete mode 100644 pkg/agentic/edge_case_test.go diff --git a/pkg/agentic/coverage_push_test.go b/pkg/agentic/coverage_push_test.go deleted file mode 100644 index 1176cbb..0000000 --- a/pkg/agentic/coverage_push_test.go +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// Tests targeting partially covered functions to push toward 80%. - -package agentic - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - core "dappco.re/go/core" - "dappco.re/go/core/forge" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// --- statusRemote error parsing --- - -func TestStatusRemote_Good_ErrorResponse(t *testing.T) { - callCount := 0 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount++ - w.Header().Set("Mcp-Session-Id", "s") - w.Header().Set("Content-Type", "text/event-stream") - switch callCount { - case 1: - fmt.Fprintf(w, "data: {\"result\":{}}\n\n") - case 2: - w.WriteHeader(200) - case 3: - // JSON-RPC error - result := map[string]any{ - "error": map[string]any{"code": -32000, "message": "internal error"}, - } - data, _ := json.Marshal(result) - fmt.Fprintf(w, "data: %s\n\n", data) - } - })) - t.Cleanup(srv.Close) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, out, err := s.statusRemote(context.Background(), nil, RemoteStatusInput{ - Host: srv.Listener.Addr().String(), - }) - require.NoError(t, err) - assert.False(t, out.Success) - assert.Contains(t, out.Error, "internal error") -} - -func TestStatusRemote_Good_CallFails(t *testing.T) { - callCount := 0 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount++ - w.Header().Set("Mcp-Session-Id", "s") - w.Header().Set("Content-Type", "text/event-stream") - switch callCount { - case 1: - fmt.Fprintf(w, "data: {\"result\":{}}\n\n") - case 2: - w.WriteHeader(200) - case 3: - w.WriteHeader(500) - } - })) - t.Cleanup(srv.Close) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, out, err := s.statusRemote(context.Background(), nil, RemoteStatusInput{ - Host: srv.Listener.Addr().String(), - }) - require.NoError(t, err) - assert.Contains(t, out.Error, "call failed") -} - -// --- loadRateLimitState parse error --- - -func TestLoadRateLimitState_Bad_InvalidJSON(t *testing.T) { - // Write corrupt JSON at the expected path - home := core.Env("DIR_HOME") - path := filepath.Join(home, ".core", "coderabbit-ratelimit.json") - os.MkdirAll(filepath.Dir(path), 0o755) - - original, _ := os.ReadFile(path) - os.WriteFile(path, []byte("{invalid"), 0o644) - t.Cleanup(func() { - if len(original) > 0 { - os.WriteFile(path, original, 0o644) - } else { - os.Remove(path) - } - }) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - result := s.loadRateLimitState() - assert.Nil(t, result) -} - -// --- shutdownNow with deep layout --- - -func TestShutdownNow_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-5") - os.MkdirAll(ws, 0o755) - require.NoError(t, writeStatus(ws, &WorkspaceStatus{ - Status: "queued", Repo: "go-io", Agent: "codex", - })) - - 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.Contains(t, out.Message, "cleared 1") -} - -// --- findReviewCandidates --- - -func TestFindReviewCandidates_Good_NoGitHubRemote(t *testing.T) { - root := t.TempDir() - // Create a repo dir without github remote - repoDir := filepath.Join(root, "go-io") - os.MkdirAll(repoDir, 0o755) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - candidates := s.findReviewCandidates(root) - assert.Empty(t, candidates) -} - -// --- prepWorkspace invalid repo --- - -func TestPrepWorkspace_Bad_PathTraversal(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: "../../../etc", Issue: 1, - }) - assert.Error(t, err) -} - -// --- dispatch dry run --- - -func TestDispatch_Good_DryRun(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - // Create a local repo clone for prep to find - repoSrc := filepath.Join(t.TempDir(), "core", "go-io") - os.MkdirAll(repoSrc, 0o755) - - s := &PrepSubsystem{ - codePath: filepath.Dir(filepath.Dir(repoSrc)), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - _, _, err := s.dispatch(context.Background(), nil, DispatchInput{ - Repo: "go-io", - Task: "test dispatch", - Issue: 1, - DryRun: true, - }) - // May fail (no git repo to clone) — exercises the dry run path validation - _ = err -} - -// --- DefaultBranch with master --- - -func TestDefaultBranch_Good_MasterBranch(t *testing.T) { - dir := t.TempDir() - // Init with master - exec.Command("git", "init", "-b", "master", dir).Run() - exec.Command("git", "-C", dir, "config", "user.email", "t@t.com").Run() - exec.Command("git", "-C", dir, "config", "user.name", "T").Run() - os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x"), 0o644) - exec.Command("git", "-C", dir, "add", ".").Run() - exec.Command("git", "-C", dir, "commit", "-m", "init").Run() - - branch := DefaultBranch(dir) - assert.Equal(t, "master", branch) -} - -// --- extractPRNumber edge cases --- - -func TestExtractPRNumber_Good_SimpleNumber(t *testing.T) { - assert.Equal(t, 5, extractPRNumber("5")) -} - -func TestExtractPRNumber_Ugly_TrailingSlash(t *testing.T) { - assert.Equal(t, 0, extractPRNumber("https://forge.test/pulls/")) -} - -// --- attemptVerifyAndMerge with Go test failure --- - -func TestAttemptVerifyAndMerge_Bad_TestFails(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(map[string]any{"id": 1}) // comment - })) - t.Cleanup(srv.Close) - - dir := t.TempDir() - // Create broken Go project - os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\ngo 1.22\n"), 0o644) - os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nimport \"fmt\"\n"), 0o644) - - s := &PrepSubsystem{ - forge: forge.NewForge(srv.URL, "tok"), forgeURL: srv.URL, forgeToken: "tok", - client: srv.Client(), backoff: make(map[string]time.Time), failCount: make(map[string]int), - } - result := s.attemptVerifyAndMerge(dir, "core", "test", "fix", 1) - assert.Equal(t, testFailed, result) -} - -// --- autoVerifyAndMerge with invalid PR number --- - -func TestAutoVerifyAndMerge_Bad_ZeroPRNumber(t *testing.T) { - dir := t.TempDir() - st := &WorkspaceStatus{Status: "completed", Repo: "test", PRURL: "https://forge.test/pulls/0"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - s.autoVerifyAndMerge(dir) // PR number = 0 → early return -} - -// --- runQA with node project --- - -func TestRunQA_Good_NodeNoNPM(t *testing.T) { - dir := t.TempDir() - repoDir := filepath.Join(dir, "repo") - os.MkdirAll(repoDir, 0o755) - os.WriteFile(filepath.Join(repoDir, "package.json"), []byte(`{"name":"test","scripts":{"test":"echo ok"}}`), 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - // npm install may fail without node_modules — exercises the node path - _ = s.runQA(dir) -} - -// --- buildPRBody --- - -func TestBuildPRBody_Good_WithIssueRef(t *testing.T) { - st := &WorkspaceStatus{ - Task: "Fix auth", - Agent: "codex", - Issue: 42, - Branch: "agent/fix-auth", - } - s := &PrepSubsystem{} - body := s.buildPRBody(st) - assert.Contains(t, body, "Fix auth") - assert.Contains(t, body, "Closes #42") - assert.Contains(t, body, "codex") -} - -// --- resume with completed workspace --- - -func TestResume_Good_CompletedDryRun(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsRoot := WorkspaceRoot() - ws := filepath.Join(wsRoot, "ws-completed") - repoDir := filepath.Join(ws, "repo") - os.MkdirAll(repoDir, 0o755) - - exec.Command("git", "init", repoDir).Run() - - st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Agent: "codex", Task: "Review code"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(ws, "status.json"), data, 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, out, err := s.resume(context.Background(), nil, ResumeInput{ - Workspace: "ws-completed", - DryRun: true, - }) - require.NoError(t, err) - assert.True(t, out.Success) - assert.Contains(t, out.Prompt, "Review code") -} diff --git a/pkg/agentic/dispatch_extra_test.go b/pkg/agentic/dispatch_extra_test.go deleted file mode 100644 index 71f9e3d..0000000 --- a/pkg/agentic/dispatch_extra_test.go +++ /dev/null @@ -1,401 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package agentic - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - "time" - - core "dappco.re/go/core" - "dappco.re/go/core/forge" - "github.com/stretchr/testify/assert" -) - -// --- agentOutputFile --- - -func TestAgentOutputFile_Good(t *testing.T) { - assert.Contains(t, agentOutputFile("/ws", "codex"), ".meta/agent-codex.log") - assert.Contains(t, agentOutputFile("/ws", "claude:opus"), ".meta/agent-claude.log") - assert.Contains(t, agentOutputFile("/ws", "gemini:flash"), ".meta/agent-gemini.log") -} - -// --- detectFinalStatus --- - -func TestDetectFinalStatus_Good_Completed(t *testing.T) { - dir := t.TempDir() - status, question := detectFinalStatus(dir, 0, "completed") - assert.Equal(t, "completed", status) - assert.Empty(t, question) -} - -func TestDetectFinalStatus_Good_Blocked(t *testing.T) { - dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "BLOCKED.md"), []byte("Need API key for external service"), 0o644) - - status, question := detectFinalStatus(dir, 0, "completed") - assert.Equal(t, "blocked", status) - assert.Equal(t, "Need API key for external service", question) -} - -func TestDetectFinalStatus_Good_BlockedEmpty(t *testing.T) { - dir := t.TempDir() - // BLOCKED.md exists but is empty — should NOT be treated as blocked - os.WriteFile(filepath.Join(dir, "BLOCKED.md"), []byte(" \n "), 0o644) - - status, _ := detectFinalStatus(dir, 0, "completed") - assert.Equal(t, "completed", status) -} - -func TestDetectFinalStatus_Good_FailedExitCode(t *testing.T) { - dir := t.TempDir() - status, question := detectFinalStatus(dir, 1, "completed") - assert.Equal(t, "failed", status) - assert.Contains(t, question, "code 1") -} - -func TestDetectFinalStatus_Good_FailedKilled(t *testing.T) { - dir := t.TempDir() - status, _ := detectFinalStatus(dir, 0, "killed") - assert.Equal(t, "failed", status) -} - -func TestDetectFinalStatus_Good_FailedStatus(t *testing.T) { - dir := t.TempDir() - status, _ := detectFinalStatus(dir, 0, "failed") - assert.Equal(t, "failed", status) -} - -func TestDetectFinalStatus_Good_BlockedTakesPrecedence(t *testing.T) { - dir := t.TempDir() - // Agent wrote BLOCKED.md AND exited non-zero — blocked takes precedence - os.WriteFile(filepath.Join(dir, "BLOCKED.md"), []byte("Need help"), 0o644) - - status, question := detectFinalStatus(dir, 1, "failed") - assert.Equal(t, "blocked", status) - assert.Equal(t, "Need help", question) -} - -// --- trackFailureRate --- - -func TestTrackFailureRate_Good_SuccessResetsCount(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: map[string]int{"codex": 2}, - } - triggered := s.trackFailureRate("codex", "completed", time.Now().Add(-10*time.Second)) - assert.False(t, triggered) - assert.Equal(t, 0, s.failCount["codex"]) -} - -func TestTrackFailureRate_Good_SlowFailureResetsCount(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: map[string]int{"codex": 2}, - } - // Started 5 minutes ago = slow failure - triggered := s.trackFailureRate("codex", "failed", time.Now().Add(-5*time.Minute)) - assert.False(t, triggered) - assert.Equal(t, 0, s.failCount["codex"]) -} - -func TestTrackFailureRate_Good_FastFailureIncrementsCount(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - // Started 10 seconds ago = fast failure - triggered := s.trackFailureRate("codex", "failed", time.Now().Add(-10*time.Second)) - assert.False(t, triggered) - assert.Equal(t, 1, s.failCount["codex"]) -} - -func TestTrackFailureRate_Good_ThreeFailsTriggersBackoff(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: map[string]int{"codex": 2}, // already 2 fast failures - } - triggered := s.trackFailureRate("codex", "failed", time.Now().Add(-10*time.Second)) - assert.True(t, triggered) - assert.True(t, time.Now().Before(s.backoff["codex"])) -} - -func TestTrackFailureRate_Good_ModelVariantUsesPool(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.trackFailureRate("codex:gpt-5.4", "failed", time.Now().Add(-10*time.Second)) - assert.Equal(t, 1, s.failCount["codex"], "should track by base agent pool") -} - -// --- startIssueTracking / stopIssueTracking --- - -func TestStartIssueTracking_Good_NoForge(t *testing.T) { - s := &PrepSubsystem{ - forge: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.startIssueTracking(t.TempDir()) -} - -func TestStopIssueTracking_Good_NoForge(t *testing.T) { - s := &PrepSubsystem{ - forge: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.stopIssueTracking(t.TempDir()) -} - -func TestStartIssueTracking_Good_NoIssue(t *testing.T) { - dir := t.TempDir() - st := &WorkspaceStatus{Status: "running", Repo: "test"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - forge: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.startIssueTracking(dir) -} - -func TestStartIssueTracking_Good_NoStatusFile(t *testing.T) { - s := &PrepSubsystem{ - forge: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - // No status.json — should return early - s.startIssueTracking(t.TempDir()) -} - -func TestStartIssueTracking_Good_WithForge(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(201) // Forge stopwatch start returns 201 - })) - t.Cleanup(srv.Close) - - dir := t.TempDir() - st := &WorkspaceStatus{Status: "running", Repo: "go-io", Org: "core", Issue: 15} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - forge: forge.NewForge(srv.URL, "test-token"), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.startIssueTracking(dir) -} - -func TestStopIssueTracking_Good_WithForge(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(204) - })) - t.Cleanup(srv.Close) - - dir := t.TempDir() - st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Issue: 10} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - forge: forge.NewForge(srv.URL, "test-token"), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.stopIssueTracking(dir) -} - -// --- broadcastStart / broadcastComplete --- - -func TestBroadcastStart_Good_NoCore(t *testing.T) { - dir := t.TempDir() - t.Setenv("CORE_WORKSPACE", dir) - - s := &PrepSubsystem{ - core: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.broadcastStart("codex", dir) -} - -func TestBroadcastStart_Good_WithCore(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "workspace", "ws-test") - os.MkdirAll(wsDir, 0o755) - st := &WorkspaceStatus{Repo: "go-io", Agent: "codex"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - c := core.New() - s := &PrepSubsystem{ - core: c, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.broadcastStart("codex", wsDir) -} - -func TestBroadcastComplete_Good_NoCore(t *testing.T) { - dir := t.TempDir() - t.Setenv("CORE_WORKSPACE", dir) - - s := &PrepSubsystem{ - core: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.broadcastComplete("codex", dir, "completed") -} - -func TestBroadcastComplete_Good_WithCore(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "workspace", "ws-test") - os.MkdirAll(wsDir, 0o755) - st := &WorkspaceStatus{Repo: "go-io", Agent: "codex"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - c := core.New() - s := &PrepSubsystem{ - core: c, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - s.broadcastComplete("codex", wsDir, "completed") -} - -// --- onAgentComplete --- - -func TestOnAgentComplete_Good_Completed(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "ws-test") - repoDir := filepath.Join(wsDir, "repo") - metaDir := filepath.Join(wsDir, ".meta") - os.MkdirAll(repoDir, 0o755) - os.MkdirAll(metaDir, 0o755) - - // Write initial status - st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - core: nil, - forge: nil, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - outputFile := filepath.Join(metaDir, "agent-codex.log") - s.onAgentComplete("codex", wsDir, outputFile, 0, "completed", "test output") - - // Verify status was updated - updated, err := ReadStatus(wsDir) - assert.NoError(t, err) - assert.Equal(t, "completed", updated.Status) - assert.Equal(t, 0, updated.PID) - assert.Empty(t, updated.Question) - - // Verify output was written - content, _ := os.ReadFile(outputFile) - assert.Equal(t, "test output", string(content)) -} - -func TestOnAgentComplete_Good_Failed(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "ws-fail") - repoDir := filepath.Join(wsDir, "repo") - metaDir := filepath.Join(wsDir, ".meta") - os.MkdirAll(repoDir, 0o755) - os.MkdirAll(metaDir, 0o755) - - st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - s.onAgentComplete("codex", wsDir, filepath.Join(metaDir, "agent-codex.log"), 1, "failed", "error output") - - updated, _ := ReadStatus(wsDir) - assert.Equal(t, "failed", updated.Status) - assert.Contains(t, updated.Question, "code 1") -} - -func TestOnAgentComplete_Good_Blocked(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "ws-blocked") - repoDir := filepath.Join(wsDir, "repo") - metaDir := filepath.Join(wsDir, ".meta") - os.MkdirAll(repoDir, 0o755) - os.MkdirAll(metaDir, 0o755) - - // Create BLOCKED.md - os.WriteFile(filepath.Join(repoDir, "BLOCKED.md"), []byte("Need credentials"), 0o644) - - st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - s.onAgentComplete("codex", wsDir, filepath.Join(metaDir, "agent-codex.log"), 0, "completed", "") - - updated, _ := ReadStatus(wsDir) - assert.Equal(t, "blocked", updated.Status) - assert.Equal(t, "Need credentials", updated.Question) -} - -func TestOnAgentComplete_Good_EmptyOutput(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsDir := filepath.Join(root, "ws-empty") - repoDir := filepath.Join(wsDir, "repo") - metaDir := filepath.Join(wsDir, ".meta") - os.MkdirAll(repoDir, 0o755) - os.MkdirAll(metaDir, 0o755) - - st := &WorkspaceStatus{Status: "running", Repo: "test", Agent: "codex", StartedAt: time.Now()} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - outputFile := filepath.Join(metaDir, "agent-codex.log") - s.onAgentComplete("codex", wsDir, outputFile, 0, "completed", "") - - // Output file should NOT be created for empty output - _, err := os.Stat(outputFile) - assert.True(t, os.IsNotExist(err)) -} diff --git a/pkg/agentic/dispatch_test.go b/pkg/agentic/dispatch_test.go index 361d081..ab453dc 100644 --- a/pkg/agentic/dispatch_test.go +++ b/pkg/agentic/dispatch_test.go @@ -7,295 +7,485 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "os/exec" "path/filepath" "testing" "time" + core "dappco.re/go/core" "dappco.re/go/core/forge" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// --- dispatch (validation) --- +// --- agentCommand --- -func TestDispatch_Bad_NoRepo(t *testing.T) { - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } +// Good: tested in logic_test.go (TestAgentCommand_Good_*) +// Bad: tested in logic_test.go (TestAgentCommand_Bad_Unknown) +// Ugly: tested in logic_test.go (TestAgentCommand_Ugly_EmptyAgent) - _, _, err := s.dispatch(context.Background(), nil, DispatchInput{ - Task: "Fix the bug", - }) - assert.Error(t, err) - assert.Contains(t, err.Error(), "repo is required") +// --- containerCommand --- + +// Good: tested in logic_test.go (TestContainerCommand_Good_*) + +// --- agentOutputFile --- + +func TestDispatch_AgentOutputFile_Good(t *testing.T) { + assert.Contains(t, agentOutputFile("/ws", "codex"), ".meta/agent-codex.log") + assert.Contains(t, agentOutputFile("/ws", "claude:opus"), ".meta/agent-claude.log") + assert.Contains(t, agentOutputFile("/ws", "gemini:flash"), ".meta/agent-gemini.log") } -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_AgentOutputFile_Bad(t *testing.T) { + // Empty agent — still produces a path (no crash) + result := agentOutputFile("/ws", "") + assert.Contains(t, result, ".meta/agent-.log") } -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 +func TestDispatch_AgentOutputFile_Ugly(t *testing.T) { + // Agent with multiple colons — only splits on first + result := agentOutputFile("/ws", "claude:opus:latest") + assert.Contains(t, result, "agent-claude.log") +} + +// --- detectFinalStatus --- + +func TestDispatch_DetectFinalStatus_Good(t *testing.T) { + dir := t.TempDir() + + // Clean exit = completed + status, question := detectFinalStatus(dir, 0, "completed") + assert.Equal(t, "completed", status) + assert.Empty(t, question) +} + +func TestDispatch_DetectFinalStatus_Bad(t *testing.T) { + dir := t.TempDir() + + // Non-zero exit code + status, question := detectFinalStatus(dir, 1, "completed") + assert.Equal(t, "failed", status) + assert.Contains(t, question, "code 1") + + // Process killed + status2, _ := detectFinalStatus(dir, 0, "killed") + assert.Equal(t, "failed", status2) + + // Process status "failed" + status3, _ := detectFinalStatus(dir, 0, "failed") + assert.Equal(t, "failed", status3) +} + +func TestDispatch_DetectFinalStatus_Ugly(t *testing.T) { + dir := t.TempDir() + + // BLOCKED.md exists but is whitespace only — NOT blocked + os.WriteFile(filepath.Join(dir, "BLOCKED.md"), []byte(" \n "), 0o644) + status, _ := detectFinalStatus(dir, 0, "completed") + assert.Equal(t, "completed", status) + + // BLOCKED.md takes precedence over non-zero exit + os.WriteFile(filepath.Join(dir, "BLOCKED.md"), []byte("Need credentials"), 0o644) + status2, question2 := detectFinalStatus(dir, 1, "failed") + assert.Equal(t, "blocked", status2) + assert.Equal(t, "Need credentials", question2) +} + +// --- trackFailureRate --- + +func TestDispatch_TrackFailureRate_Good(t *testing.T) { + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: map[string]int{"codex": 2}} + + // Success resets count + triggered := s.trackFailureRate("codex", "completed", time.Now().Add(-10*time.Second)) + assert.False(t, triggered) + assert.Equal(t, 0, s.failCount["codex"]) +} + +func TestDispatch_TrackFailureRate_Bad(t *testing.T) { + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: map[string]int{"codex": 2}} + + // 3rd fast failure triggers backoff + triggered := s.trackFailureRate("codex", "failed", time.Now().Add(-10*time.Second)) + assert.True(t, triggered) + assert.True(t, time.Now().Before(s.backoff["codex"])) +} + +func TestDispatch_TrackFailureRate_Ugly(t *testing.T) { + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + + // Slow failure (>60s) resets count instead of incrementing + s.failCount["codex"] = 2 + s.trackFailureRate("codex", "failed", time.Now().Add(-5*time.Minute)) + assert.Equal(t, 0, s.failCount["codex"]) + + // Model variant tracks by base pool + s.trackFailureRate("codex:gpt-5.4", "failed", time.Now().Add(-10*time.Second)) + assert.Equal(t, 1, s.failCount["codex"]) +} + +// --- startIssueTracking --- + +func TestDispatch_StartIssueTracking_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(201) + })) + t.Cleanup(srv.Close) + + dir := t.TempDir() + st := &WorkspaceStatus{Status: "running", Repo: "go-io", Org: "core", Issue: 15} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) + + s := &PrepSubsystem{forge: forge.NewForge(srv.URL, "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.startIssueTracking(dir) +} + +func TestDispatch_StartIssueTracking_Bad(t *testing.T) { + // No forge — returns early + s := &PrepSubsystem{forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.startIssueTracking(t.TempDir()) + + // No status file + s2 := &PrepSubsystem{forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s2.startIssueTracking(t.TempDir()) +} + +func TestDispatch_StartIssueTracking_Ugly(t *testing.T) { + // Status has no issue — early return + dir := t.TempDir() + st := &WorkspaceStatus{Status: "running", Repo: "test"} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) + + s := &PrepSubsystem{forge: forge.NewForge("http://invalid", "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.startIssueTracking(dir) // no issue → skips API call +} + +// --- stopIssueTracking --- + +func TestDispatch_StopIssueTracking_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(204) + })) + t.Cleanup(srv.Close) + + dir := t.TempDir() + st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Issue: 10} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) + + s := &PrepSubsystem{forge: forge.NewForge(srv.URL, "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.stopIssueTracking(dir) +} + +func TestDispatch_StopIssueTracking_Bad(t *testing.T) { + s := &PrepSubsystem{forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.stopIssueTracking(t.TempDir()) +} + +func TestDispatch_StopIssueTracking_Ugly(t *testing.T) { + // Status has no issue + dir := t.TempDir() + st := &WorkspaceStatus{Status: "completed", Repo: "test"} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) + + s := &PrepSubsystem{forge: forge.NewForge("http://invalid", "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.stopIssueTracking(dir) +} + +// --- broadcastStart --- + +func TestDispatch_BroadcastStart_Good(t *testing.T) { 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) + wsDir := filepath.Join(root, "workspace", "ws-test") + os.MkdirAll(wsDir, 0o755) + data, _ := json.Marshal(WorkspaceStatus{Repo: "go-io", Agent: "codex"}) + os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) - // 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() + c := core.New() + s := &PrepSubsystem{core: c, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastStart("codex", wsDir) +} - 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), - } +func TestDispatch_BroadcastStart_Bad(t *testing.T) { + // No Core — should not panic + s := &PrepSubsystem{core: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastStart("codex", t.TempDir()) +} - _, out, err := s.dispatch(context.Background(), nil, DispatchInput{ - Repo: "go-io", - Task: "Fix stuff", - Issue: 42, - DryRun: true, - }) +func TestDispatch_BroadcastStart_Ugly(t *testing.T) { + // No status file — broadcasts with empty repo + c := core.New() + s := &PrepSubsystem{core: c, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastStart("codex", t.TempDir()) +} + +// --- broadcastComplete --- + +func TestDispatch_BroadcastComplete_Good(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + wsDir := filepath.Join(root, "workspace", "ws-test") + os.MkdirAll(wsDir, 0o755) + data, _ := json.Marshal(WorkspaceStatus{Repo: "go-io", Agent: "codex"}) + os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) + + c := core.New() + s := &PrepSubsystem{core: c, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastComplete("codex", wsDir, "completed") +} + +func TestDispatch_BroadcastComplete_Bad(t *testing.T) { + s := &PrepSubsystem{core: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastComplete("codex", t.TempDir(), "failed") +} + +func TestDispatch_BroadcastComplete_Ugly(t *testing.T) { + // No status file + c := core.New() + s := &PrepSubsystem{core: c, backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.broadcastComplete("codex", t.TempDir(), "completed") +} + +// --- onAgentComplete --- + +func TestDispatch_OnAgentComplete_Good(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + wsDir := filepath.Join(root, "ws-test") + repoDir := filepath.Join(wsDir, "repo") + metaDir := filepath.Join(wsDir, ".meta") + os.MkdirAll(repoDir, 0o755) + os.MkdirAll(metaDir, 0o755) + + st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) + + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + outputFile := filepath.Join(metaDir, "agent-codex.log") + s.onAgentComplete("codex", wsDir, outputFile, 0, "completed", "test output") + + updated, err := ReadStatus(wsDir) 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) + assert.Equal(t, "completed", updated.Status) + assert.Equal(t, 0, updated.PID) + + content, _ := os.ReadFile(outputFile) + assert.Equal(t, "test output", string(content)) +} + +func TestDispatch_OnAgentComplete_Bad(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + wsDir := filepath.Join(root, "ws-fail") + repoDir := filepath.Join(wsDir, "repo") + metaDir := filepath.Join(wsDir, ".meta") + os.MkdirAll(repoDir, 0o755) + os.MkdirAll(metaDir, 0o755) + + st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) + + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.onAgentComplete("codex", wsDir, filepath.Join(metaDir, "agent-codex.log"), 1, "failed", "error") + + updated, _ := ReadStatus(wsDir) + assert.Equal(t, "failed", updated.Status) + assert.Contains(t, updated.Question, "code 1") +} + +func TestDispatch_OnAgentComplete_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + wsDir := filepath.Join(root, "ws-blocked") + repoDir := filepath.Join(wsDir, "repo") + metaDir := filepath.Join(wsDir, ".meta") + os.MkdirAll(repoDir, 0o755) + os.MkdirAll(metaDir, 0o755) + + os.WriteFile(filepath.Join(repoDir, "BLOCKED.md"), []byte("Need credentials"), 0o644) + st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()} + data, _ := json.Marshal(st) + os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0o644) + + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + s.onAgentComplete("codex", wsDir, filepath.Join(metaDir, "agent-codex.log"), 0, "completed", "") + + updated, _ := ReadStatus(wsDir) + assert.Equal(t, "blocked", updated.Status) + assert.Equal(t, "Need credentials", updated.Question) + + // Empty output should NOT create log file + _, err := os.Stat(filepath.Join(metaDir, "agent-codex.log")) + assert.True(t, os.IsNotExist(err)) } // --- runQA --- -func TestRunQA_Good_GoProject(t *testing.T) { - // Create a minimal valid Go project +func TestDispatch_RunQA_Good(t *testing.T) { wsDir := t.TempDir() repoDir := filepath.Join(wsDir, "repo") - require.True(t, fs.EnsureDir(repoDir).OK) + os.MkdirAll(repoDir, 0o755) + os.WriteFile(filepath.Join(repoDir, "go.mod"), []byte("module testmod\n\ngo 1.22\n"), 0o644) + os.WriteFile(filepath.Join(repoDir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644) - 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) + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + assert.True(t, s.runQA(wsDir)) } -func TestRunQA_Bad_GoBrokenCode(t *testing.T) { +func TestDispatch_RunQA_Bad(t *testing.T) { wsDir := t.TempDir() repoDir := filepath.Join(wsDir, "repo") - require.True(t, fs.EnsureDir(repoDir).OK) + os.MkdirAll(repoDir, 0o755) - 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) + // Broken Go code + os.WriteFile(filepath.Join(repoDir, "go.mod"), []byte("module testmod\n\ngo 1.22\n"), 0o644) + os.WriteFile(filepath.Join(repoDir, "main.go"), []byte("package main\nfunc main( {\n}\n"), 0o644) - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + assert.False(t, s.runQA(wsDir)) - result := s.runQA(wsDir) - assert.False(t, result) + // PHP project — composer not available + wsDir2 := t.TempDir() + repoDir2 := filepath.Join(wsDir2, "repo") + os.MkdirAll(repoDir2, 0o755) + os.WriteFile(filepath.Join(repoDir2, "composer.json"), []byte(`{"name":"test"}`), 0o644) + + assert.False(t, s.runQA(wsDir2)) } -func TestRunQA_Good_UnknownLanguage(t *testing.T) { - // No go.mod, composer.json, or package.json → passes QA (no checks) +func TestDispatch_RunQA_Ugly(t *testing.T) { + // Unknown language — passes QA (no checks) wsDir := t.TempDir() - repoDir := filepath.Join(wsDir, "repo") - require.True(t, fs.EnsureDir(repoDir).OK) + os.MkdirAll(filepath.Join(wsDir, "repo"), 0o755) - s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + assert.True(t, s.runQA(wsDir)) - result := s.runQA(wsDir) - assert.True(t, result) + // Go vet failure (compiles but bad printf) + wsDir2 := t.TempDir() + repoDir2 := filepath.Join(wsDir2, "repo") + os.MkdirAll(repoDir2, 0o755) + os.WriteFile(filepath.Join(repoDir2, "go.mod"), []byte("module testmod\n\ngo 1.22\n"), 0o644) + os.WriteFile(filepath.Join(repoDir2, "main.go"), []byte("package main\nimport \"fmt\"\nfunc main() { fmt.Printf(\"%d\", \"x\") }\n"), 0o644) + assert.False(t, s.runQA(wsDir2)) + + // Node project — npm install likely fails + wsDir3 := t.TempDir() + repoDir3 := filepath.Join(wsDir3, "repo") + os.MkdirAll(repoDir3, 0o755) + os.WriteFile(filepath.Join(repoDir3, "package.json"), []byte(`{"name":"test","scripts":{"test":"echo ok"}}`), 0o644) + _ = s.runQA(wsDir3) // exercises the node path } -func TestRunQA_Good_GoVetFailure(t *testing.T) { - wsDir := t.TempDir() - repoDir := filepath.Join(wsDir, "repo") - require.True(t, fs.EnsureDir(repoDir).OK) +// --- dispatch --- - 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 +func TestDispatch_Dispatch_Good(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) -import "fmt" + forgeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{"title": "Issue", "body": "Fix"}) + })) + t.Cleanup(forgeSrv.Close) -func main() { - fmt.Printf("%d", "not a number") -} -` - require.True(t, fs.Write(filepath.Join(repoDir, "main.go"), code).OK) + srcRepo := filepath.Join(t.TempDir(), "core", "go-io") + exec.Command("git", "init", "-b", "main", srcRepo).Run() + exec.Command("git", "-C", srcRepo, "config", "user.name", "T").Run() + exec.Command("git", "-C", srcRepo, "config", "user.email", "t@t.com").Run() + os.WriteFile(filepath.Join(srcRepo, "go.mod"), []byte("module test\ngo 1.22\n"), 0o644) + exec.Command("git", "-C", srcRepo, "add", ".").Run() + exec.Command("git", "-C", srcRepo, "commit", "-m", "init").Run() s := &PrepSubsystem{ - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + forge: forge.NewForge(forgeSrv.URL, "tok"), codePath: filepath.Dir(filepath.Dir(srcRepo)), + client: forgeSrv.Client(), 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) + _, 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) + assert.NotEmpty(t, out.Prompt) +} + +func TestDispatch_Dispatch_Bad(t *testing.T) { + s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} + + // No repo + _, _, err := s.dispatch(context.Background(), nil, DispatchInput{Task: "do"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "repo is required") + + // No task + _, _, err = s.dispatch(context.Background(), nil, DispatchInput{Repo: "go-io"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "task is required") +} + +func TestDispatch_Dispatch_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + // Prep fails (no local clone) + s := &PrepSubsystem{codePath: t.TempDir(), backoff: make(map[string]time.Time), failCount: make(map[string]int)} + _, _, err := s.dispatch(context.Background(), nil, DispatchInput{ + Repo: "nonexistent", Task: "do", Issue: 1, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "prep workspace failed") } // --- workspaceDir --- -func TestWorkspaceDir_Good_Issue(t *testing.T) { +func TestDispatch_WorkspaceDir_Good(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") + + dir2, _ := workspaceDir("core", "go-io", PrepInput{PR: 7}) + assert.Contains(t, dir2, "pr-7") + + dir3, _ := workspaceDir("core", "go-io", PrepInput{Branch: "feat/new"}) + assert.Contains(t, dir3, "feat/new") + + dir4, _ := workspaceDir("core", "go-io", PrepInput{Tag: "v1.0.0"}) + assert.Contains(t, dir4, "v1.0.0") } -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) - +func TestDispatch_WorkspaceDir_Bad(t *testing.T) { _, err := workspaceDir("core", "go-io", PrepInput{}) assert.Error(t, err) - assert.Contains(t, err.Error(), "one of issue, pr, branch, or tag is required") + assert.Contains(t, err.Error(), "one of issue, pr, branch, or tag") } -// --- DispatchInput defaults --- +func TestDispatch_WorkspaceDir_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) -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") + // PR takes precedence when multiple set (first match) + dir, err := workspaceDir("core", "go-io", PrepInput{PR: 3, Issue: 5}) + require.NoError(t, err) + assert.Contains(t, dir, "pr-3") } // --- 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")) -} +// Good: tested in queue_test.go +// Bad: tested in queue_test.go +// Ugly: see queue_extra_test.go diff --git a/pkg/agentic/edge_case_test.go b/pkg/agentic/edge_case_test.go deleted file mode 100644 index 5b17d54..0000000 --- a/pkg/agentic/edge_case_test.go +++ /dev/null @@ -1,446 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// Edge-case tests to push partially covered functions toward 80%+. - -package agentic - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - core "dappco.re/go/core" - "dappco.re/go/core/forge" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// --- autoCreatePR --- - -func TestAutoCreatePR_Bad_NoStatus(t *testing.T) { - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - s.autoCreatePR(t.TempDir()) // should not panic -} - -func TestAutoCreatePR_Bad_EmptyBranch(t *testing.T) { - dir := t.TempDir() - st := &WorkspaceStatus{Status: "completed", Repo: "test", Branch: ""} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - s.autoCreatePR(dir) -} - -func TestAutoCreatePR_Bad_EmptyRepo(t *testing.T) { - dir := t.TempDir() - st := &WorkspaceStatus{Status: "completed", Branch: "agent/fix"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - s.autoCreatePR(dir) -} - -func TestAutoCreatePR_Bad_NoCommits(t *testing.T) { - dir := t.TempDir() - repoDir := filepath.Join(dir, "repo") - os.MkdirAll(repoDir, 0o755) - - // Init a real git repo with a commit - exec.Command("git", "init", repoDir).Run() - exec.Command("git", "-C", repoDir, "config", "user.email", "test@test.com").Run() - exec.Command("git", "-C", repoDir, "config", "user.name", "Test").Run() - os.WriteFile(filepath.Join(repoDir, "f.txt"), []byte("hi"), 0o644) - exec.Command("git", "-C", repoDir, "add", ".").Run() - exec.Command("git", "-C", repoDir, "commit", "-m", "init").Run() - exec.Command("git", "-C", repoDir, "checkout", "-b", "dev").Run() - - st := &WorkspaceStatus{Status: "completed", Repo: "test", Branch: "dev", Agent: "codex"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(dir, "status.json"), data, 0o644) - - s := &PrepSubsystem{backoff: make(map[string]time.Time), failCount: make(map[string]int)} - s.autoCreatePR(dir) // no commits ahead → early return -} - -// --- createPR --- - -func TestCreatePR_Bad_MissingWorkspace(t *testing.T) { - s := &PrepSubsystem{forgeToken: "tok", 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_NoForgeToken(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: "ws"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no Forge token") -} - -func TestCreatePR_Bad_NoStatusFile(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsRoot := filepath.Join(root, "workspace") - ws := filepath.Join(wsRoot, "ws-nostatus") - repoDir := filepath.Join(ws, "repo") - os.MkdirAll(repoDir, 0o755) - exec.Command("git", "init", repoDir).Run() - - s := &PrepSubsystem{forgeToken: "tok", backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, _, err := s.createPR(context.Background(), nil, CreatePRInput{Workspace: "ws-nostatus"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no status") -} - -func TestCreatePR_Good_DryRunNoBranch(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsRoot := filepath.Join(root, "workspace") - ws := filepath.Join(wsRoot, "ws-nobranch") - repoDir := filepath.Join(ws, "repo") - os.MkdirAll(repoDir, 0o755) - - // Init git with a commit so rev-parse works - exec.Command("git", "init", "-b", "agent-test", repoDir).Run() - exec.Command("git", "-C", repoDir, "config", "user.email", "t@t.com").Run() - exec.Command("git", "-C", repoDir, "config", "user.name", "T").Run() - os.WriteFile(filepath.Join(repoDir, "f.txt"), []byte("x"), 0o644) - exec.Command("git", "-C", repoDir, "add", ".").Run() - exec.Command("git", "-C", repoDir, "commit", "-m", "init").Run() - - // Status has no branch — createPR should detect from git - st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Task: "Fix", Agent: "codex"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(ws, "status.json"), data, 0o644) - - s := &PrepSubsystem{forgeToken: "tok", backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, out, err := s.createPR(context.Background(), nil, CreatePRInput{ - Workspace: "ws-nobranch", - DryRun: true, - }) - require.NoError(t, err) - assert.True(t, out.Success) - assert.Equal(t, "agent-test", out.Branch) -} - -func TestCreatePR_Good_DryRunDefaultTitle(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - wsRoot := filepath.Join(root, "workspace") - ws := filepath.Join(wsRoot, "ws-notitle") - repoDir := filepath.Join(ws, "repo") - os.MkdirAll(repoDir, 0o755) - exec.Command("git", "init", repoDir).Run() - - // Status with no Task — title defaults to branch name - st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Branch: "agent/fix", Agent: "codex"} - data, _ := json.Marshal(st) - os.WriteFile(filepath.Join(ws, "status.json"), data, 0o644) - - s := &PrepSubsystem{forgeToken: "tok", backoff: make(map[string]time.Time), failCount: make(map[string]int)} - _, out, err := s.createPR(context.Background(), nil, CreatePRInput{ - Workspace: "ws-notitle", - DryRun: true, - }) - require.NoError(t, err) - assert.Contains(t, out.Title, "agent/fix") -} - -// --- listPRs --- - -func TestListPRs_Bad_AllRepos(t *testing.T) { - // Test the "all repos" path — lists from all org repos - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/api/v1/orgs/core/repos": - json.NewEncoder(w).Encode([]map[string]any{ - {"name": "go-io", "owner": map[string]any{"login": "core"}}, - }) - default: - json.NewEncoder(w).Encode([]map[string]any{ - {"number": 1, "title": "PR", "state": "open", "html_url": "url", - "user": map[string]any{"login": "virgil"}, - "head": map[string]any{"ref": "fix"}, "base": map[string]any{"ref": "dev"}, - "labels": []map[string]any{}}, - }) - } - })) - 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{}) - require.NoError(t, err) - assert.True(t, out.Success) -} - -// --- status (more branches) --- - -func TestStatus_Good_RunningPIDDead_Blocked(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - wsRoot := filepath.Join(root, "workspace") - - ws := filepath.Join(wsRoot, "ws-dead") - repoDir := filepath.Join(ws, "repo") - os.MkdirAll(repoDir, 0o755) - - // Write BLOCKED.md — dead process with blocked file = blocked - os.WriteFile(filepath.Join(repoDir, "BLOCKED.md"), []byte("Need help with API"), 0o644) - - writeStatus(ws, &WorkspaceStatus{ - Status: "running", Repo: "test", Agent: "codex", PID: 999999, // non-existent PID - }) - - 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.Len(t, out.Blocked, 1) - assert.Contains(t, out.Blocked[0].Question, "Need help") -} - -func TestStatus_Good_RunningPIDDead_Completed(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - wsRoot := filepath.Join(root, "workspace") - - ws := filepath.Join(wsRoot, "ws-dead2") - os.MkdirAll(filepath.Join(ws, "repo"), 0o755) - - // Write agent log — dead process with log = completed - os.WriteFile(filepath.Join(ws, "agent-codex.log"), []byte("done"), 0o644) - - writeStatus(ws, &WorkspaceStatus{ - Status: "running", Repo: "test", Agent: "codex", PID: 999998, - }) - - 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.Completed) -} - -func TestStatus_Good_RunningPIDDead_Failed(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - wsRoot := filepath.Join(root, "workspace") - - ws := filepath.Join(wsRoot, "ws-dead3") - os.MkdirAll(filepath.Join(ws, "repo"), 0o755) - - // No BLOCKED.md, no agent log — dead process = failed - writeStatus(ws, &WorkspaceStatus{ - Status: "running", Repo: "test", Agent: "codex", PID: 999997, - }) - - 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.Failed) -} - -// --- DefaultBranch --- - -func TestDefaultBranch_Good_GitRepo(t *testing.T) { - dir := t.TempDir() - exec.Command("git", "init", "-b", "main", dir).Run() - exec.Command("git", "-C", dir, "config", "user.email", "t@t.com").Run() - exec.Command("git", "-C", dir, "config", "user.name", "T").Run() - os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x"), 0o644) - exec.Command("git", "-C", dir, "add", ".").Run() - exec.Command("git", "-C", dir, "commit", "-m", "init").Run() - - branch := DefaultBranch(dir) - assert.Equal(t, "main", branch) -} - -func TestDefaultBranch_Bad_NoGit(t *testing.T) { - dir := t.TempDir() - branch := DefaultBranch(dir) - assert.Equal(t, "main", branch, "should default to main for non-git dirs") -} - -// --- writeStatus edge cases --- - -func TestWriteStatus_Good_UpdatesTimestampOnWrite(t *testing.T) { - dir := t.TempDir() - before := time.Now().Add(-1 * time.Second) - - st := &WorkspaceStatus{Status: "running", Repo: "test"} - err := writeStatus(dir, st) - require.NoError(t, err) - - after := time.Now().Add(1 * time.Second) - read, _ := ReadStatus(dir) - assert.True(t, read.UpdatedAt.After(before)) - assert.True(t, read.UpdatedAt.Before(after)) -} - -func TestWriteStatus_Good_PreservesFields(t *testing.T) { - dir := t.TempDir() - st := &WorkspaceStatus{ - Status: "running", Repo: "go-io", Agent: "codex", Org: "core", - Task: "Fix it", Branch: "agent/fix", Issue: 42, PID: 12345, - Question: "need help", Runs: 3, PRURL: "https://forge.test/pulls/1", - } - require.NoError(t, writeStatus(dir, st)) - - read, err := ReadStatus(dir) - require.NoError(t, err) - assert.Equal(t, "running", read.Status) - assert.Equal(t, "go-io", read.Repo) - assert.Equal(t, 42, read.Issue) - assert.Equal(t, 12345, read.PID) - assert.Equal(t, "need help", read.Question) -} - -// --- reviewQueue edge cases --- - -func TestReviewQueue_Good_RespectLimit(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - - s := &PrepSubsystem{ - codePath: root, - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - _, out, err := s.reviewQueue(context.Background(), nil, ReviewQueueInput{Limit: 1}) - require.NoError(t, err) - assert.True(t, out.Success) -} - -// --- cmdPrep with branch/pr/tag/issue --- - -func TestCmdPrep_Good_WithIssue(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - r := s.cmdPrep(core.NewOptions( - core.Option{Key: "_arg", Value: "nonexistent"}, - core.Option{Key: "issue", Value: "42"}, - )) - // Will fail (no local clone) but exercises the issue parsing path - assert.False(t, r.OK) -} - -func TestCmdPrep_Good_WithPR(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - r := s.cmdPrep(core.NewOptions( - core.Option{Key: "_arg", Value: "nonexistent"}, - core.Option{Key: "pr", Value: "7"}, - )) - assert.False(t, r.OK) -} - -func TestCmdPrep_Good_WithBranch(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - r := s.cmdPrep(core.NewOptions( - core.Option{Key: "_arg", Value: "nonexistent"}, - core.Option{Key: "branch", Value: "feat/new"}, - )) - assert.False(t, r.OK) -} - -func TestCmdPrep_Good_WithTag(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - r := s.cmdPrep(core.NewOptions( - core.Option{Key: "_arg", Value: "nonexistent"}, - core.Option{Key: "tag", Value: "v1.0.0"}, - )) - assert.False(t, r.OK) -} - -// --- cmdRunTask with defaults --- - -func TestCmdRunTask_Good_DefaultsApplied(t *testing.T) { - s, _ := testPrepWithCore(t, nil) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Has repo+task but dispatch will fail (no local clone) — exercises default logic - r := s.cmdRunTask(ctx, core.NewOptions( - core.Option{Key: "repo", Value: "go-io"}, - core.Option{Key: "task", Value: "fix tests"}, - core.Option{Key: "issue", Value: "15"}, - )) - assert.False(t, r.OK) // dispatch fails, but exercises all defaults -} - -// --- canDispatchAgent with Core config --- - -func TestCanDispatchAgent_Good_WithCoreConfig(t *testing.T) { - root := t.TempDir() - t.Setenv("CORE_WORKSPACE", root) - os.MkdirAll(filepath.Join(root, "workspace"), 0o755) - - c := core.New() - // Set concurrency config on Core - c.Config().Set("agents.concurrency", map[string]ConcurrencyLimit{ - "claude": {Total: 5}, - }) - - s := &PrepSubsystem{ - core: c, - codePath: t.TempDir(), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - assert.True(t, s.canDispatchAgent("claude")) -} - -// --- buildPrompt with persona --- - -func TestBuildPrompt_Good_WithPersona(t *testing.T) { - dir := t.TempDir() - s := &PrepSubsystem{ - codePath: t.TempDir(), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - prompt, _, _ := s.buildPrompt(context.Background(), PrepInput{ - Task: "Fix tests", - Org: "core", - Repo: "go-io", - Persona: "engineering/engineering-security-engineer", - }, "dev", dir) - - assert.Contains(t, prompt, "TASK: Fix tests") - // Persona may or may not be found — just exercises the branch -} - -// --- buildPrompt with plan template --- - -func TestBuildPrompt_Good_WithPlanTemplate(t *testing.T) { - dir := t.TempDir() - s := &PrepSubsystem{ - codePath: t.TempDir(), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), - } - - prompt, _, _ := s.buildPrompt(context.Background(), PrepInput{ - Task: "Fix the auth bug", - Org: "core", - Repo: "go-io", - PlanTemplate: "bug-fix", - }, "dev", dir) - - assert.Contains(t, prompt, "TASK: Fix the auth bug") - // Plan template may render if embedded — exercises the branch -}