diff --git a/internal/agent/opencode.go b/internal/agent/opencode.go index 276184d6..22f93bc8 100644 --- a/internal/agent/opencode.go +++ b/internal/agent/opencode.go @@ -109,11 +109,9 @@ func isOpencodeToolCallLine(line string) bool { func (a *OpenCodeAgent) CommandLine() string { args := []string{"run", "--format", "default"} - model := a.Model - if model == "" { - model = "opencode/minimax-m2.1-free" + if a.Model != "" { + args = append(args, "--model", a.Model) } - args = append(args, "--model", model) return a.Command + " " + strings.Join(args, " ") } @@ -126,11 +124,9 @@ func (a *OpenCodeAgent) Review(ctx context.Context, repoPath, commitSHA, prompt // opencode --help // opencode run --help args := []string{"run", "--format", "default"} - model := a.Model - if model == "" { - model = "opencode/minimax-m2.1-free" + if a.Model != "" { + args = append(args, "--model", a.Model) } - args = append(args, "--model", model) args = append(args, prompt) cmd := exec.CommandContext(ctx, a.Command, args...) diff --git a/internal/agent/opencode_test.go b/internal/agent/opencode_test.go index 3008bb8a..6674110b 100644 --- a/internal/agent/opencode_test.go +++ b/internal/agent/opencode_test.go @@ -2,6 +2,8 @@ package agent import ( "context" + "os" + "strings" "testing" ) @@ -75,6 +77,90 @@ func TestFilterOpencodeToolCallLines(t *testing.T) { } } +func TestOpenCodeModelFlag(t *testing.T) { + tests := []struct { + name string + model string + wantModel bool // true = --model should appear + wantContains string + }{ + { + name: "no model omits flag", + model: "", + wantModel: false, + }, + { + name: "explicit model includes flag", + model: "anthropic/claude-sonnet-4-20250514", + wantModel: true, + wantContains: "anthropic/claude-sonnet-4-20250514", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := NewOpenCodeAgent("opencode") + a.Model = tt.model + cl := a.CommandLine() + if tt.wantModel { + assertContains(t, cl, "--model") + assertContains(t, cl, tt.wantContains) + } else { + assertNotContains(t, cl, "--model") + } + }) + } +} + +func TestOpenCodeReviewModelFlag(t *testing.T) { + skipIfWindows(t) + tests := []struct { + name string + model string + wantFlag bool + wantModel string + }{ + { + name: "no model omits --model from args", + model: "", + wantFlag: false, + }, + { + name: "explicit model passes --model to subprocess", + model: "openai/gpt-4o", + wantFlag: true, + wantModel: "openai/gpt-4o", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := mockAgentCLI(t, MockCLIOpts{ + CaptureArgs: true, + StdoutLines: []string{"ok"}, + }) + a := NewOpenCodeAgent(mock.CmdPath) + a.Model = tt.model + _, err := a.Review( + context.Background(), t.TempDir(), + "abc123", "review this", nil, + ) + if err != nil { + t.Fatalf("Review: %v", err) + } + raw, err := os.ReadFile(mock.ArgsFile) + if err != nil { + t.Fatalf("read captured args: %v", err) + } + args := strings.TrimSpace(string(raw)) + if tt.wantFlag { + assertContains(t, args, "--model") + assertContains(t, args, tt.wantModel) + } else { + assertNotContains(t, args, "--model") + } + }) + } +} + func TestOpenCodeReviewFiltersToolCallLines(t *testing.T) { skipIfWindows(t) script := NewScriptBuilder(). diff --git a/internal/daemon/worker_test.go b/internal/daemon/worker_test.go index bf547b17..9a01d24f 100644 --- a/internal/daemon/worker_test.go +++ b/internal/daemon/worker_test.go @@ -24,6 +24,7 @@ type workerTestContext struct { func newWorkerTestContext(t *testing.T, workers int) *workerTestContext { t.Helper() db, tmpDir := testutil.OpenTestDBWithDir(t) + testutil.InitTestGitRepo(t, tmpDir) cfg := config.DefaultConfig() if workers > 0 { @@ -83,7 +84,8 @@ func (c *workerTestContext) waitForJobStatus(t *testing.T, jobID int64, statuses func TestWorkerPoolE2E(t *testing.T) { tc := newWorkerTestContext(t, 2) - job := tc.createJob(t, "testsha123") + sha := testutil.GetHeadSHA(t, tc.TmpDir) + job := tc.createJob(t, sha) tc.Pool.Start() finalJob := tc.waitForJobStatus(t, job.ID, storage.JobStatusDone, storage.JobStatusFailed) @@ -94,7 +96,7 @@ func TestWorkerPoolE2E(t *testing.T) { } if finalJob.Status == storage.JobStatusDone { - review, err := tc.DB.GetReviewByCommitSHA("testsha123") + review, err := tc.DB.GetReviewByCommitSHA(sha) if err != nil { t.Fatalf("GetReviewByCommitSHA failed: %v", err) } @@ -108,55 +110,33 @@ func TestWorkerPoolE2E(t *testing.T) { } func TestWorkerPoolConcurrency(t *testing.T) { - db, tmpDir := testutil.OpenTestDBWithDir(t) - - cfg := config.DefaultConfig() - cfg.MaxWorkers = 4 - - repo, _ := db.GetOrCreateRepo(tmpDir) + tc := newWorkerTestContext(t, 4) + sha := testutil.GetHeadSHA(t, tc.TmpDir) for i := 0; i < 5; i++ { - sha := "concurrentsha" + string(rune('0'+i)) - commit, _ := db.GetOrCreateCommit(repo.ID, sha, "Author", "Subject", time.Now()) - db.EnqueueJob(storage.EnqueueOpts{RepoID: repo.ID, CommitID: commit.ID, GitRef: sha, Agent: "test"}) + tc.createJob(t, sha) } - broadcaster := NewBroadcaster() - pool := NewWorkerPool(db, NewStaticConfig(cfg), 4, broadcaster, nil) - pool.Start() + tc.Pool.Start() time.Sleep(500 * time.Millisecond) - activeWorkers := pool.ActiveWorkers() + activeWorkers := tc.Pool.ActiveWorkers() - pool.Stop() + tc.Pool.Stop() t.Logf("Peak active workers: %d", activeWorkers) } func TestWorkerPoolCancelRunningJob(t *testing.T) { tc := newWorkerTestContext(t, 1) - job := tc.createJob(t, "cancelsha") + sha := testutil.GetHeadSHA(t, tc.TmpDir) + job := tc.createJob(t, sha) tc.Pool.Start() defer tc.Pool.Stop() - // Wait for job to be claimed (poll quickly since running state may be brief) - deadline := time.Now().Add(5 * time.Second) - reachedRunning := false - for time.Now().Before(deadline) { - j, err := tc.DB.GetJobByID(job.ID) - if err != nil { - t.Fatalf("GetJobByID failed: %v", err) - } - if j.Status == storage.JobStatusRunning { - reachedRunning = true - break - } - time.Sleep(10 * time.Millisecond) - } - if !reachedRunning { - t.Fatal("Timeout: job never reached 'running' state") - } + // Wait for job to be claimed + tc.waitForJobStatus(t, job.ID, storage.JobStatusRunning) // Cancel the job if err := tc.DB.CancelJob(job.ID); err != nil { @@ -164,17 +144,13 @@ func TestWorkerPoolCancelRunningJob(t *testing.T) { } tc.Pool.CancelJob(job.ID) - time.Sleep(500 * time.Millisecond) + finalJob := tc.waitForJobStatus(t, job.ID, storage.JobStatusCanceled) - finalJob, err := tc.DB.GetJobByID(job.ID) - if err != nil { - t.Fatalf("GetJobByID failed: %v", err) - } if finalJob.Status != storage.JobStatusCanceled { t.Errorf("Expected status 'canceled', got '%s'", finalJob.Status) } - _, err = tc.DB.GetReviewByJobID(job.ID) + _, err := tc.DB.GetReviewByJobID(job.ID) if err == nil { t.Error("Expected no review for canceled job, but found one") }