// SPDX-License-Identifier: EUPL-1.2 package agentic import ( "context" "strings" "testing" "time" core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- buildReviewCommand --- func TestReviewqueue_BuildReviewCommand_Good_CodeRabbit(t *testing.T) { s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)} cmd, args := s.buildReviewCommand("/tmp/repo", "coderabbit") assert.Equal(t, "coderabbit", cmd) assert.Contains(t, args, "review") assert.Contains(t, args, "--plain") assert.Contains(t, args, "github/main") } func TestReviewqueue_BuildReviewCommand_Good_Codex(t *testing.T) { s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)} cmd, args := s.buildReviewCommand("/tmp/repo", "codex") assert.Equal(t, "codex", cmd) assert.Contains(t, args, "review") assert.Contains(t, args, "github/main") } func TestReviewqueue_BuildReviewCommand_Good_DefaultReviewer(t *testing.T) { s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)} cmd, args := s.buildReviewCommand("/tmp/repo", "") assert.Equal(t, "coderabbit", cmd) assert.Contains(t, args, "--plain") } // --- saveRateLimitState / loadRateLimitState --- func TestReviewqueue_SaveLoadRateLimitState_Good_Roundtrip(t *testing.T) { dir := t.TempDir() t.Setenv("CORE_WORKSPACE", dir) // Ensure .core dir exists fs.EnsureDir(core.JoinPath(dir, ".core")) // Note: saveRateLimitState uses core.Env("DIR_HOME") which is pre-populated. // We need to work around this by using CORE_WORKSPACE for the load, // but save/load use DIR_HOME. Skip if not writable. s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } info := &RateLimitInfo{ Limited: true, RetryAt: time.Now().Add(5 * time.Minute).Truncate(time.Second), Message: "rate limited", } s.saveRateLimitState(info) loaded := s.loadRateLimitState() if loaded != nil { assert.True(t, loaded.Limited) assert.Equal(t, "rate limited", loaded.Message) } // If loaded is nil it means DIR_HOME path wasn't writable — acceptable in test } // --- storeReviewOutput --- func TestReviewqueue_StoreReviewOutput_Good(t *testing.T) { // storeReviewOutput uses core.Env("DIR_HOME") so we can't fully control the path // but we can verify it doesn't panic s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } assert.NotPanics(t, func() { s.storeReviewOutput(t.TempDir(), "test-repo", "coderabbit", "No findings — LGTM") }) } func TestReviewqueue_RunPRManageLoop_Good_StopsOnCancel(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { s.runPRManageLoop(ctx, time.Hour) close(done) }() cancel() require.Eventually(t, func() bool { select { case <-done: return true default: return false } }, time.Second, 5*time.Millisecond) } // --- reviewQueue --- func TestReviewqueue_NoCandidates_Good(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) // Create an empty core dir (no repos) coreDir := core.JoinPath(root, "core") fs.EnsureDir(coreDir) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), codePath: root, backoff: make(map[string]time.Time), failCount: make(map[string]int), } _, out, err := s.reviewQueue(context.Background(), nil, ReviewQueueInput{DryRun: true}) require.NoError(t, err) assert.True(t, out.Success) assert.Empty(t, out.Processed) } // --- status (extended) --- func TestReviewqueue_StatusFiltered_Good(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) wsRoot := core.JoinPath(root, "workspace") // Create workspaces with different statuses for _, ws := range []struct { name string status string }{ {"ws-1", "completed"}, {"ws-2", "failed"}, {"ws-3", "completed"}, {"ws-4", "queued"}, } { wsDir := core.JoinPath(wsRoot, ws.name) fs.EnsureDir(wsDir) st := &WorkspaceStatus{Status: ws.status, Repo: "test", Agent: "codex"} fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(st)) } s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } _, out, err := s.status(context.Background(), nil, StatusInput{}) require.NoError(t, err) assert.Equal(t, 4, out.Total) assert.Equal(t, 2, out.Completed) assert.Equal(t, 1, out.Failed) assert.Equal(t, 1, out.Queued) } // --- handlers helpers (resolveWorkspace, findWorkspaceByPR) --- func TestHandlers_ResolveWorkspace_Good_Exists(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) wsRoot := core.JoinPath(root, "workspace") // Create workspace dir ws := core.JoinPath(wsRoot, "core", "go-io", "task-15") fs.EnsureDir(ws) result := resolveWorkspace("core/go-io/task-15") assert.Equal(t, ws, result) } func TestHandlers_ResolveWorkspace_Bad_NotExists(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) result := resolveWorkspace("nonexistent") assert.Empty(t, result) } func TestHandlers_FindWorkspaceByPR_Good_Match(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) wsRoot := core.JoinPath(root, "workspace") ws := core.JoinPath(wsRoot, "ws-test") fs.EnsureDir(ws) st := &WorkspaceStatus{Repo: "go-io", Branch: "agent/fix", Status: "completed"} fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(st)) result := findWorkspaceByPR("go-io", "agent/fix") assert.Equal(t, ws, result) } func TestHandlers_FindWorkspaceByPR_Good_DeepLayout(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) wsRoot := core.JoinPath(root, "workspace") // Deep layout: org/repo/task ws := core.JoinPath(wsRoot, "core", "agent", "task-5") fs.EnsureDir(ws) st := &WorkspaceStatus{Repo: "agent", Branch: "agent/tests", Status: "completed"} fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(st)) 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 := core.JoinPath(core.Env("DIR_HOME"), ".core", "coderabbit-ratelimit.json") // Save original content (may or may not exist) origResult := fs.Read(ratePath) hadFile := origResult.OK var original string if hadFile { original = origResult.Value.(string) } // Ensure parent dir exists fs.EnsureDir(core.PathDir(ratePath)) // Write corrupt JSON fs.Write(ratePath, "not-valid-json{{{") t.Cleanup(func() { if hadFile { fs.Write(ratePath, original) } else { fs.Delete(ratePath) } }) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } result := s.loadRateLimitState() assert.Nil(t, result, "corrupt JSON should return nil") } // --- buildReviewCommand Bad/Ugly --- func TestReviewqueue_BuildReviewCommand_Bad(t *testing.T) { // Empty reviewer string — defaults to coderabbit s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } cmd, args := s.buildReviewCommand("/tmp/repo", "") assert.Equal(t, "coderabbit", cmd) assert.Contains(t, args, "--plain") } func TestReviewqueue_BuildReviewCommand_Ugly(t *testing.T) { s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)} cmd, args := s.buildReviewCommand("/tmp/repo", "unknown-reviewer") assert.Equal(t, "coderabbit", cmd) assert.Contains(t, args, "--plain") } // --- countFindings Bad/Ugly --- func TestReviewqueue_CountFindings_Bad(t *testing.T) { // Empty string count := countFindings("") // Empty string doesn't contain "No findings" so defaults to 1 assert.Equal(t, 1, count) } func TestReviewqueue_CountFindings_Ugly(t *testing.T) { // Only whitespace count := countFindings(" \n \n ") // No markers, no "No findings", so defaults to 1 assert.Equal(t, 1, count) } // --- parseRetryAfter Ugly --- func TestReviewqueue_ParseRetryAfter_Ugly(t *testing.T) { // Seconds only "try after 30 seconds" — no minutes match d := parseRetryAfter("try after 30 seconds") // Regex expects minutes first, so this won't match — defaults to 5 min assert.Equal(t, 5*time.Minute, d) } // --- storeReviewOutput Bad/Ugly --- func TestReviewqueue_StoreReviewOutput_Bad(t *testing.T) { // Empty output s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } assert.NotPanics(t, func() { s.storeReviewOutput(t.TempDir(), "test-repo", "coderabbit", "") }) } func TestReviewqueue_StoreReviewOutput_Ugly(t *testing.T) { // Very large output s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } largeOutput := strings.Repeat("Finding: something is wrong on this line\n", 10000) assert.NotPanics(t, func() { s.storeReviewOutput(t.TempDir(), "test-repo", "coderabbit", largeOutput) }) } // --- saveRateLimitState Good/Bad/Ugly --- func TestReviewqueue_SaveRateLimitState_Good(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } info := &RateLimitInfo{ Limited: true, RetryAt: time.Now().Add(5 * time.Minute).Truncate(time.Second), Message: "rate limited", } assert.NotPanics(t, func() { s.saveRateLimitState(info) }) } func TestReviewqueue_SaveRateLimitState_Bad(t *testing.T) { // Save nil info s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } assert.NotPanics(t, func() { s.saveRateLimitState(nil) }) } func TestReviewqueue_SaveRateLimitState_Bad_WriteFailure(t *testing.T) { t.Setenv("CORE_HOME", "/dev/null") s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } info := &RateLimitInfo{ Limited: true, RetryAt: time.Now().Add(5 * time.Minute).Truncate(time.Second), Message: "write failure", } assert.NotPanics(t, func() { s.saveRateLimitState(info) }) } func TestReviewqueue_SaveRateLimitState_Ugly(t *testing.T) { // Save with far-future RetryAt s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } info := &RateLimitInfo{ Limited: true, RetryAt: time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC), Message: "far future rate limit", } assert.NotPanics(t, func() { s.saveRateLimitState(info) }) } // --- loadRateLimitState Good --- func TestReviewqueue_LoadRateLimitState_Good(t *testing.T) { // Write then load valid state s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } info := &RateLimitInfo{ Limited: true, RetryAt: time.Now().Add(5 * time.Minute).Truncate(time.Second), Message: "test rate limit", } s.saveRateLimitState(info) loaded := s.loadRateLimitState() if loaded != nil { assert.True(t, loaded.Limited) assert.Equal(t, "test rate limit", loaded.Message) } // If loaded is nil, DIR_HOME path wasn't writable — acceptable in test } // --- loadRateLimitState Bad --- func TestReviewqueue_LoadRateLimitState_Bad(t *testing.T) { // File doesn't exist — should return nil s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int), } // loadRateLimitState reads from DIR_HOME/.core/coderabbit-ratelimit.json. // If the file doesn't exist, it should return nil without panic. result := s.loadRateLimitState() // May or may not be nil depending on whether the file exists in the real home dir. // The key invariant is: it must not panic. _ = result }