Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions internal/agent/opencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ")
}

Expand All @@ -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...)
Expand Down
86 changes: 86 additions & 0 deletions internal/agent/opencode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package agent

import (
"context"
"os"
"strings"
"testing"
)

Expand Down Expand Up @@ -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().
Expand Down
56 changes: 16 additions & 40 deletions internal/daemon/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -108,73 +110,47 @@ 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 {
t.Fatalf("CancelJob failed: %v", err)
}
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")
}
Expand Down
Loading