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
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,28 @@ jobs:

- name: Test (Windows)
if: matrix.os == 'windows-latest'
run: go test -tags integration ./...
shell: pwsh
run: |
# go test on Windows can exit 1 due to temp .exe cleanup
# ("go: remove ...\.exe: Access is denied") even when all
# tests pass. Only suppress that specific pattern.
go test -tags integration ./... 2>&1 | Tee-Object -Variable testOut
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
$failed = $testOut | Select-String "^FAIL\s"
if ($failed) {
Write-Host "Tests failed:"
Write-Host ($failed -join "`n")
exit 1
}
$cleanup = $testOut | Select-String "go: remove .+\.exe: Access is denied"
if (-not $cleanup) {
Write-Host "go test failed with unknown error"
exit $exitCode
}
Write-Host "All tests passed (ignoring temp .exe cleanup error)"
exit 0
}

- name: Build with CGO disabled
run: go build ./...
Expand Down
8 changes: 4 additions & 4 deletions cmd/roborev/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -1631,8 +1631,8 @@ func queueHelpLines(width int) int {
if width <= 0 {
return 2
}
h1 := (len(queueHelpLine1) + width - 1) / width
h2 := (len(queueHelpLine2) + width - 1) / width
h1 := (runewidth.StringWidth(queueHelpLine1) + width - 1) / width
h2 := (runewidth.StringWidth(queueHelpLine2) + width - 1) / width
return h1 + h2
}

Expand Down Expand Up @@ -2620,8 +2620,8 @@ func (m tuiModel) renderReviewView() string {
const helpLine2 = "↑/↓: scroll | ←/→: prev/next | ?: commands | esc: back"
helpLines := 2
if m.width > 0 {
h1 := (len(helpLine1) + m.width - 1) / m.width
h2 := (len(helpLine2) + m.width - 1) / m.width
h1 := (runewidth.StringWidth(helpLine1) + m.width - 1) / m.width
h2 := (runewidth.StringWidth(helpLine2) + m.width - 1) / m.width
helpLines = h1 + h2
}

Expand Down
24 changes: 15 additions & 9 deletions internal/agent/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,13 @@
cmd.Dir = repoPath
cmd.WaitDelay = 5 * time.Second

// Handle API key: use configured key if set, otherwise filter out env var
// to ensure Claude uses subscription auth instead of unexpected API charges
// Strip CLAUDECODE to prevent nested-session detection (#270),
// and handle API key (configured key or subscription auth).
stripKeys := []string{"ANTHROPIC_API_KEY", "CLAUDECODE"}
if apiKey := AnthropicAPIKey(); apiKey != "" {
// Use explicitly configured API key from roborev config
cmd.Env = append(filterEnv(os.Environ(), "ANTHROPIC_API_KEY"), "ANTHROPIC_API_KEY="+apiKey)
cmd.Env = append(filterEnv(os.Environ(), stripKeys...), "ANTHROPIC_API_KEY="+apiKey)
} else {
// Clear env var so Claude uses subscription auth
cmd.Env = filterEnv(os.Environ(), "ANTHROPIC_API_KEY")
cmd.Env = filterEnv(os.Environ(), stripKeys...)
}
// Suppress sounds from Claude Code (notification/completion sounds)
cmd.Env = append(cmd.Env, "CLAUDE_NO_SOUND=1")
Expand Down Expand Up @@ -209,7 +208,7 @@
if line != "" {
// Stream raw line to the writer for progress visibility
if sw := newSyncWriter(output); sw != nil {
sw.Write([]byte(line + "\n"))

Check failure on line 211 in internal/agent/claude.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `sw.Write` is not checked (errcheck)
}

var msg claudeStreamMessage
Expand Down Expand Up @@ -253,11 +252,18 @@
}

// filterEnv returns a copy of env with the specified key removed
func filterEnv(env []string, key string) []string {
prefix := key + "="
func filterEnv(env []string, keys ...string) []string {
result := make([]string, 0, len(env))
for _, e := range env {
if !strings.HasPrefix(e, prefix) {
k, _, _ := strings.Cut(e, "=")
strip := false
for _, key := range keys {
if k == key {
strip = true
break
}
}
if !strip {
result = append(result, e)
}
}
Expand Down
44 changes: 44 additions & 0 deletions internal/agent/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,47 @@ func TestFilterEnv(t *testing.T) {
t.Fatalf("missing expected env vars in filtered result: %v", filtered)
}
}

func TestFilterEnvMultipleKeys(t *testing.T) {
env := []string{
"PATH=/usr/bin",
"ANTHROPIC_API_KEY=secret",
"CLAUDECODE=1",
"HOME=/home/test",
}

filtered := filterEnv(env, "ANTHROPIC_API_KEY", "CLAUDECODE")

if len(filtered) != 2 {
t.Fatalf("expected 2 env vars, got %d: %v", len(filtered), filtered)
}
for _, e := range filtered {
if strings.HasPrefix(e, "ANTHROPIC_API_KEY=") {
t.Fatal("ANTHROPIC_API_KEY should be stripped")
}
if strings.HasPrefix(e, "CLAUDECODE=") {
t.Fatal("CLAUDECODE should be stripped")
}
}
}

func TestFilterEnvExactMatchOnly(t *testing.T) {
env := []string{
"CLAUDE=1",
"CLAUDECODE=1",
"CLAUDE_NO_SOUND=1",
"PATH=/usr/bin",
}

filtered := filterEnv(env, "CLAUDE")

if len(filtered) != 3 {
t.Fatalf("expected 3 env vars, got %d: %v", len(filtered), filtered)
}
for _, e := range filtered {
k, _, _ := strings.Cut(e, "=")
if k == "CLAUDE" {
t.Fatal("CLAUDE should be stripped")
}
}
}
Loading