diff --git a/pkg/agentic/queue_extra_test.go b/pkg/agentic/queue_extra_test.go index 2a176e9..023726f 100644 --- a/pkg/agentic/queue_extra_test.go +++ b/pkg/agentic/queue_extra_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -185,3 +186,49 @@ func TestDrainOne_Good_SkipsBackedOffPool(t *testing.T) { } assert.False(t, s.drainOne()) } + +// --- canDispatchAgent (Ugly — with Core.Config concurrency) --- + +func TestQueue_CanDispatchAgent_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + os.MkdirAll(filepath.Join(root, "workspace"), 0o755) + + c := core.New() + // Set concurrency on Core.Config() — same path that Register() uses + c.Config().Set("agents.concurrency", map[string]ConcurrencyLimit{ + "claude": {Total: 1}, + "gemini": {Total: 3}, + }) + + s := &PrepSubsystem{ + core: c, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + // No running workspaces → should be able to dispatch + assert.True(t, s.canDispatchAgent("claude")) + assert.True(t, s.canDispatchAgent("gemini:flash")) + // Agent with no limit configured → always allowed + assert.True(t, s.canDispatchAgent("codex:gpt-5.4")) +} + +// --- drainQueue (Ugly — with Core lock path) --- + +func TestQueue_DrainQueue_Ugly(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + os.MkdirAll(filepath.Join(root, "workspace"), 0o755) + + c := core.New() + s := &PrepSubsystem{ + core: c, + frozen: false, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + // Not frozen, Core is present, empty workspace → drainQueue runs the Core lock path without panic + assert.NotPanics(t, func() { s.drainQueue() }) +} diff --git a/pkg/agentic/review_queue_extra_test.go b/pkg/agentic/review_queue_extra_test.go index 9ce8532..9cbbdb2 100644 --- a/pkg/agentic/review_queue_extra_test.go +++ b/pkg/agentic/review_queue_extra_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -209,3 +210,36 @@ func TestFindWorkspaceByPR_Good_DeepLayout(t *testing.T) { result := findWorkspaceByPR("agent", "agent/tests") assert.Equal(t, ws, result) } + +// --- loadRateLimitState (Ugly — corrupt JSON) --- + +func TestReviewQueue_LoadRateLimitState_Ugly(t *testing.T) { + // core.Env("DIR_HOME") is cached at init, so we must write to the real path. + // Save original content, write corrupt JSON, test, then restore. + ratePath := filepath.Join(core.Env("DIR_HOME"), ".core", "coderabbit-ratelimit.json") + + // Save original content (may or may not exist) + original, readErr := os.ReadFile(ratePath) + hadFile := readErr == nil + + // Ensure parent dir exists + os.MkdirAll(filepath.Dir(ratePath), 0o755) + + // Write corrupt JSON + require.NoError(t, os.WriteFile(ratePath, []byte("not-valid-json{{{"), 0o644)) + t.Cleanup(func() { + if hadFile { + os.WriteFile(ratePath, original, 0o644) + } else { + os.Remove(ratePath) + } + }) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + result := s.loadRateLimitState() + assert.Nil(t, result, "corrupt JSON should return nil") +} diff --git a/pkg/agentic/status_extra_test.go b/pkg/agentic/status_extra_test.go index 5d81086..8d20993 100644 --- a/pkg/agentic/status_extra_test.go +++ b/pkg/agentic/status_extra_test.go @@ -533,3 +533,39 @@ func TestDrainQueue_Good_FrozenDoesNothing(t *testing.T) { s.drainQueue() }) } + +// --- shutdownNow (Ugly — deep layout with queued status) --- + +func TestShutdown_ShutdownNow_Ugly(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") + require.True(t, fs.EnsureDir(ws).OK) + require.NoError(t, writeStatus(ws, &WorkspaceStatus{ + Status: "queued", + Repo: "go-io", + Agent: "codex", + Task: "Add tests", + })) + + s := &PrepSubsystem{ + frozen: false, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + _, out, err := s.shutdownNow(context.Background(), nil, ShutdownInput{}) + require.NoError(t, err) + assert.True(t, out.Success) + assert.True(t, s.frozen) + assert.Contains(t, out.Message, "cleared 1") + + // Verify the queued workspace is now failed + st, err := ReadStatus(ws) + require.NoError(t, err) + assert.Equal(t, "failed", st.Status) + assert.Contains(t, st.Question, "cleared by shutdown_now") +} diff --git a/pkg/agentic/status_test.go b/pkg/agentic/status_test.go index b88133e..c5539af 100644 --- a/pkg/agentic/status_test.go +++ b/pkg/agentic/status_test.go @@ -254,3 +254,48 @@ func TestStatus_Status_Ugly(t *testing.T) { assert.Equal(t, "failed", st3.Status) assert.Equal(t, "Agent process died (no output log)", st3.Question) } + +// --- writeStatus (extended Ugly) --- + +func TestStatus_WriteStatus_Ugly(t *testing.T) { + // Write a status with all fields, read back, verify UpdatedAt is set and all fields preserved + dir := t.TempDir() + + original := &WorkspaceStatus{ + Status: "blocked", + Agent: "gemini:flash", + Repo: "go-mcp", + Org: "core", + Task: "Refactor IPC handler", + Branch: "agent/refactor-ipc", + Issue: 77, + PID: 999999, // dead PID — non-existent + StartedAt: time.Now().Add(-10 * time.Minute).Truncate(time.Second), + Question: "Should I break backward compat?", + Runs: 5, + PRURL: "https://forge.lthn.ai/core/go-mcp/pulls/12", + } + + err := writeStatus(dir, original) + require.NoError(t, err) + + // UpdatedAt should have been set by writeStatus + assert.False(t, original.UpdatedAt.IsZero(), "writeStatus must set UpdatedAt") + + // Read back and verify every field + read, err := ReadStatus(dir) + require.NoError(t, err) + + assert.Equal(t, "blocked", read.Status) + assert.Equal(t, "gemini:flash", read.Agent) + assert.Equal(t, "go-mcp", read.Repo) + assert.Equal(t, "core", read.Org) + assert.Equal(t, "Refactor IPC handler", read.Task) + assert.Equal(t, "agent/refactor-ipc", read.Branch) + assert.Equal(t, 77, read.Issue) + assert.Equal(t, 999999, read.PID) + assert.Equal(t, "Should I break backward compat?", read.Question) + assert.Equal(t, 5, read.Runs) + assert.Equal(t, "https://forge.lthn.ai/core/go-mcp/pulls/12", read.PRURL) + assert.False(t, read.UpdatedAt.IsZero(), "UpdatedAt must survive roundtrip") +} diff --git a/pkg/agentic/verify_test.go b/pkg/agentic/verify_test.go index 06e3524..ca9a953 100644 --- a/pkg/agentic/verify_test.go +++ b/pkg/agentic/verify_test.go @@ -507,3 +507,81 @@ func TestTruncate_Bad_ZeroMax(t *testing.T) { func TestTruncate_Ugly_EmptyString(t *testing.T) { assert.Equal(t, "", truncate("", 10)) } + +// --- autoVerifyAndMerge (extended Ugly) --- + +func TestVerify_AutoVerifyAndMerge_Ugly(t *testing.T) { + // Workspace with status=completed, repo=test, PRURL="not-a-url" + // extractPRNumber returns 0 for "not-a-url" → early return, no panic + dir := t.TempDir() + require.NoError(t, writeStatus(dir, &WorkspaceStatus{ + Status: "completed", + Repo: "test", + Branch: "agent/fix", + PRURL: "not-a-url", + })) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + // PR number is 0 → should return early without panicking + assert.NotPanics(t, func() { + s.autoVerifyAndMerge(dir) + }) + + // Status should remain unchanged (not "merged") + st, err := ReadStatus(dir) + require.NoError(t, err) + assert.Equal(t, "completed", st.Status) +} + +// --- attemptVerifyAndMerge (Ugly — Go project that fails build) --- + +func TestVerify_AttemptVerifyAndMerge_Ugly(t *testing.T) { + // Go project that fails build (go.mod but no valid Go code) + // with httptest Forge mock for comment API → returns testFailed + commentCalled := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" && containsStr(r.URL.Path, "/comments") { + commentCalled = true + json.NewEncoder(w).Encode(map[string]any{"id": 1}) + return + } + w.WriteHeader(200) + })) + t.Cleanup(srv.Close) + + dir := t.TempDir() + // Write a go.mod so runVerification detects Go and runs "go test ./..." + require.True(t, fs.Write(filepath.Join(dir, "go.mod"), "module broken-test\n\ngo 1.22").OK) + // Write invalid Go code to force test failure + require.True(t, fs.Write(filepath.Join(dir, "broken.go"), "package broken\n\nfunc Bad() { undeclared() }").OK) + + 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), + } + + result := s.attemptVerifyAndMerge(dir, "core", "test-repo", "agent/fix", 42) + assert.Equal(t, testFailed, result) + assert.True(t, commentCalled, "should have posted a comment about test failure") +} + +// --- extractPRNumber (extended Ugly) --- + +func TestVerify_ExtractPRNumber_Ugly(t *testing.T) { + // Just a bare number "5" → last segment is "5" → returns 5 + assert.Equal(t, 5, extractPRNumber("5")) + + // Trailing slash → last segment is empty string → parseInt("") → 0 + assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/42/")) + + // Non-numeric string → parseInt("abc") → 0 + assert.Equal(t, 0, extractPRNumber("abc")) +}