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
29 changes: 27 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ on:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

Expand All @@ -19,7 +22,29 @@ jobs:
- name: Build
run: go build ./...

- name: Test with race detection
- name: Test with race detection (Linux/macOS)
if: matrix.os != 'windows-latest'
run: go test -race ./...

- name: Test (Windows)
if: matrix.os == 'windows-latest'
run: go test ./...

- name: Build with CGO disabled
run: go build ./...
env:
CGO_ENABLED: 0

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Test with coverage
run: go test -race -coverprofile=coverage.out ./...

- name: Upload coverage
Expand Down
34 changes: 21 additions & 13 deletions cmd/roborev/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,11 @@ func TestWaitForReview(t *testing.T) {

func TestFindJobForCommit(t *testing.T) {
t.Run("finds matching job", func(t *testing.T) {
// Use a real temp dir so path normalization works cross-platform
repoDir := t.TempDir()
allJobs := []storage.ReviewJob{
{ID: 1, GitRef: "abc123", RepoPath: "/test/repo"},
{ID: 2, GitRef: "def456", RepoPath: "/test/repo"},
{ID: 1, GitRef: "abc123", RepoPath: repoDir},
{ID: 2, GitRef: "def456", RepoPath: repoDir},
}
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/jobs" || r.Method != "GET" {
Expand All @@ -201,7 +203,7 @@ func TestFindJobForCommit(t *testing.T) {
}))
defer cleanup()

job, err := findJobForCommit("/test/repo", "def456")
job, err := findJobForCommit(repoDir, "def456")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -214,8 +216,9 @@ func TestFindJobForCommit(t *testing.T) {
})

t.Run("returns nil when not found", func(t *testing.T) {
repoDir := t.TempDir()
allJobs := []storage.ReviewJob{
{ID: 1, GitRef: "abc123", RepoPath: "/test/repo"},
{ID: 1, GitRef: "abc123", RepoPath: repoDir},
}
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/jobs" || r.Method != "GET" {
Expand All @@ -238,7 +241,7 @@ func TestFindJobForCommit(t *testing.T) {
}))
defer cleanup()

job, err := findJobForCommit("/test/repo", "notfound")
job, err := findJobForCommit(repoDir, "notfound")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -249,9 +252,12 @@ func TestFindJobForCommit(t *testing.T) {

t.Run("fallback skips jobs from different repo", func(t *testing.T) {
// Primary query returns empty (repo mismatch), fallback returns job from different repo
otherRepo := t.TempDir()
anotherRepo := t.TempDir()
queryRepo := t.TempDir()
allJobs := []storage.ReviewJob{
{ID: 1, GitRef: "abc123", RepoPath: "/other/repo"},
{ID: 2, GitRef: "abc123", RepoPath: "/another/repo"},
{ID: 1, GitRef: "abc123", RepoPath: otherRepo},
{ID: 2, GitRef: "abc123", RepoPath: anotherRepo},
}
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/jobs" || r.Method != "GET" {
Expand All @@ -273,8 +279,8 @@ func TestFindJobForCommit(t *testing.T) {
}))
defer cleanup()

// Request job for /test/repo, but all jobs are for different repos
job, err := findJobForCommit("/test/repo", "abc123")
// Request job for queryRepo, but all jobs are for different repos
job, err := findJobForCommit(queryRepo, "abc123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -347,13 +353,14 @@ func TestFindJobForCommit(t *testing.T) {
})

t.Run("returns error on invalid JSON", func(t *testing.T) {
repoDir := t.TempDir()
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("not json"))
}))
defer cleanup()

_, err := findJobForCommit("/test/repo", "abc123")
_, err := findJobForCommit(repoDir, "abc123")
if err == nil {
t.Fatal("expected error on invalid JSON")
}
Expand All @@ -366,10 +373,11 @@ func TestFindJobForCommit(t *testing.T) {
// Jobs with empty or relative paths should be skipped in fallback to avoid
// false matches from cwd resolution. Job 3 has correct SHA but path stored
// differently (simulating path normalization mismatch).
repoDir := t.TempDir()
allJobs := []storage.ReviewJob{
{ID: 1, GitRef: "abc123", RepoPath: ""}, // Empty path - skip
{ID: 2, GitRef: "abc123", RepoPath: "relative/path"}, // Relative path - skip
{ID: 3, GitRef: "abc123", RepoPath: "/test/repo"}, // Absolute path - match via fallback
{ID: 3, GitRef: "abc123", RepoPath: repoDir}, // Absolute path - match via fallback
}
requestCount := 0
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -405,9 +413,9 @@ func TestFindJobForCommit(t *testing.T) {
}))
defer cleanup()

// Request job for /test/repo - primary query returns empty, fallback should
// Request job for repoDir - primary query returns empty, fallback should
// skip empty/relative paths and find job ID 3 via path normalization
job, err := findJobForCommit("/test/repo", "abc123")
job, err := findJobForCommit(repoDir, "abc123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down
27 changes: 18 additions & 9 deletions cmd/roborev/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
Expand All @@ -40,23 +41,23 @@ func setupMockDaemon(t *testing.T, handler http.Handler) (*httptest.Server, func

ts := httptest.NewServer(handler)

// Override HOME
tmpHome := t.TempDir()
origHome := os.Getenv("HOME")
os.Setenv("HOME", tmpHome)
// Use ROBOREV_DATA_DIR to override data directory (works cross-platform)
// On Windows, HOME doesn't work since os.UserHomeDir() uses USERPROFILE
tmpDir := t.TempDir()
origDataDir := os.Getenv("ROBOREV_DATA_DIR")
os.Setenv("ROBOREV_DATA_DIR", tmpDir)

// Write fake daemon.json
roborevDir := filepath.Join(tmpHome, ".roborev")
if err := os.MkdirAll(roborevDir, 0755); err != nil {
t.Fatalf("failed to create roborev dir: %v", err)
if err := os.MkdirAll(tmpDir, 0755); err != nil {
t.Fatalf("failed to create data dir: %v", err)
}
mockAddr := ts.URL[7:] // strip "http://"
daemonInfo := daemon.RuntimeInfo{Addr: mockAddr, PID: os.Getpid(), Version: version.Version}
data, err := json.Marshal(daemonInfo)
if err != nil {
t.Fatalf("failed to marshal daemon.json: %v", err)
}
if err := os.WriteFile(filepath.Join(roborevDir, "daemon.json"), data, 0644); err != nil {
if err := os.WriteFile(filepath.Join(tmpDir, "daemon.json"), data, 0644); err != nil {
t.Fatalf("failed to write daemon.json: %v", err)
}

Expand All @@ -66,7 +67,11 @@ func setupMockDaemon(t *testing.T, handler http.Handler) (*httptest.Server, func

cleanup := func() {
ts.Close()
os.Setenv("HOME", origHome)
if origDataDir != "" {
os.Setenv("ROBOREV_DATA_DIR", origDataDir)
} else {
os.Unsetenv("ROBOREV_DATA_DIR")
}
serverAddr = origServerAddr
}

Expand Down Expand Up @@ -1426,6 +1431,10 @@ func TestShowJobFlagRequiresArgument(t *testing.T) {
// ============================================================================

func TestDaemonRunStartsAndShutdownsCleanly(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping daemon integration test on Windows due to file locking differences")
}

// Use temp directories for isolation
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
Expand Down
31 changes: 25 additions & 6 deletions cmd/roborev/refine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -217,9 +218,18 @@ func TestResolveAllowUnsafeAgents(t *testing.T) {

func TestSelectRefineAgentCodexUsesRequestedReasoning(t *testing.T) {
tmpDir := t.TempDir()
codexPath := filepath.Join(tmpDir, "codex")
if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
var codexPath string
if runtime.GOOS == "windows" {
// On Windows, create a batch file that exits successfully
codexPath = filepath.Join(tmpDir, "codex.bat")
if err := os.WriteFile(codexPath, []byte("@exit /b 0\r\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
}
} else {
codexPath = filepath.Join(tmpDir, "codex")
if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
}
}

t.Setenv("PATH", tmpDir)
Expand All @@ -240,9 +250,18 @@ func TestSelectRefineAgentCodexUsesRequestedReasoning(t *testing.T) {

func TestSelectRefineAgentCodexFallbackUsesRequestedReasoning(t *testing.T) {
tmpDir := t.TempDir()
codexPath := filepath.Join(tmpDir, "codex")
if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
var codexPath string
if runtime.GOOS == "windows" {
// On Windows, create a batch file that exits successfully
codexPath = filepath.Join(tmpDir, "codex.bat")
if err := os.WriteFile(codexPath, []byte("@exit /b 0\r\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
}
} else {
codexPath = filepath.Join(tmpDir, "codex")
if err := os.WriteFile(codexPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
t.Fatalf("write codex stub: %v", err)
}
}

t.Setenv("PATH", tmpDir)
Expand Down
Loading
Loading