refactor(test): delete catch-all test files, rewrite dispatch_test.go
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 <virgil@lethean.io>
This commit is contained in:
parent
18eac65b70
commit
52f6031822
4 changed files with 411 additions and 1359 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue