summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-03 08:00:20 +0000
committerClaude <noreply@anthropic.com>2026-05-03 08:00:20 +0000
commite7b382bf177cbe518af3d86c3ee6c49344d225f4 (patch)
tree4dc822097b7f32a37ad655a550da1abada79ce85 /internal
parent1ec3f87c392145580a62858110d9fd10638203db (diff)
chore: close deferred work — real GeminiRunner, Local UI option, db.go cleanup
Closes the three items left on the deferred queue after the post-epic cleanup. GeminiRunner.execOnce now actually executes the gemini binary instead of writing hardcoded stream data. Mirrors ClaudeRunner.execOnce: - exec.CommandContext with the same env vars (CLAUDOMATOR_API_URL etc.) - process group SIGKILL on context cancel - stdout piped through parseGeminiStream → stdoutFile - stderr to file - exit codes captured, stderr tail surfaced on failure Test infrastructure bug uncovered in passing: testServerWithGeminiMockRunner's mock script used double-quoted echo with literal triple-backticks, which bash interpreted as command substitution. The script always produced empty output. The bug was invisible until now because GeminiRunner ignored the script entirely. Switched to a single-quoted heredoc. Frontend: index.html dropdown gains a "Local" option. No JS branching needed — the value flows through to agent.type verbatim and downstream display reads the type string as-is. storage/db.go: removed stale debug-comment scaffolding (the "TODO: Replace with proper logger" block) that was tracking a dead `fmt.Printf` call. The path it commented on is fine without logging — unmarshal errors are returned wrapped. Test status: `go test -race ./...` green across every package, zero skips, zero excluded tests. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
Diffstat (limited to 'internal')
-rw-r--r--internal/api/server_test.go23
-rw-r--r--internal/executor/gemini.go61
-rw-r--r--internal/storage/db.go5
3 files changed, 57 insertions, 32 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 516e289..2139e36 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -143,20 +143,21 @@ func testServerWithGeminiMockRunner(t *testing.T) (*Server, *storage.DB) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
- // Create the mock gemini binary script.
+ // Create the mock gemini binary script. Use single-quoted heredoc so
+ // bash does not try to evaluate the literal backticks as command
+ // substitution.
mockBinDir := t.TempDir()
mockGeminiPath := filepath.Join(mockBinDir, "mock-gemini-binary.sh")
mockScriptContent := `#!/bin/bash
-OUTPUT_FILE=$(mktemp)
-echo "` + "```json" + `" > "$OUTPUT_FILE"
-echo "{\"type\":\"content_block_start\",\"content_block\":{\"text\":\"Hello, Gemini!\",\"type\":\"text\"}}" >> "$OUTPUT_FILE"
-echo "{\"type\":\"content_block_delta\",\"content_block\":{\"text\":\" How are you?\"}}" >> "$OUTPUT_FILE"
-echo "{\"type\":\"content_block_end\"}" >> "$OUTPUT_FILE"
-echo "{\"type\":\"message_delta\",\"message\":{\"role\":\"model\"}}" >> "$OUTPUT_FILE"
-echo "{\"type\":\"message_end\"}" >> "$OUTPUT_FILE"
-echo "` + "```" + `" >> "$OUTPUT_FILE"
-cat "$OUTPUT_FILE"
-rm "$OUTPUT_FILE"
+cat <<'EOF'
+` + "```json" + `
+{"type":"content_block_start","content_block":{"text":"Hello, Gemini!","type":"text"}}
+{"type":"content_block_delta","content_block":{"text":" How are you?"}}
+{"type":"content_block_end"}
+{"type":"message_delta","message":{"role":"model"}}
+{"type":"message_end"}
+` + "```" + `
+EOF
exit 0
`
if err := os.WriteFile(mockGeminiPath, []byte(mockScriptContent), 0755); err != nil {
diff --git a/internal/executor/gemini.go b/internal/executor/gemini.go
index 7f2f54f..04382ae 100644
--- a/internal/executor/gemini.go
+++ b/internal/executor/gemini.go
@@ -7,9 +7,11 @@ import (
"io"
"log/slog"
"os"
+ "os/exec"
"path/filepath"
"strings"
"sync"
+ "syscall"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/task"
@@ -84,8 +86,18 @@ func (r *GeminiRunner) Run(ctx context.Context, t *task.Task, e *storage.Executi
}
func (r *GeminiRunner) execOnce(ctx context.Context, args []string, workingDir, projectDir string, e *storage.Execution) error {
- // Temporarily bypass external command execution to debug pipe.
- // We will simulate outputting to stdoutW directly.
+ cmd := exec.CommandContext(ctx, r.binaryPath(), args...)
+ cmd.Env = append(os.Environ(),
+ "CLAUDOMATOR_API_URL="+r.APIURL,
+ "CLAUDOMATOR_TASK_ID="+e.TaskID,
+ "CLAUDOMATOR_PROJECT_DIR="+projectDir,
+ "CLAUDOMATOR_QUESTION_FILE="+filepath.Join(e.ArtifactDir, "question.json"),
+ "CLAUDOMATOR_SUMMARY_FILE="+filepath.Join(e.ArtifactDir, "summary.txt"),
+ )
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+ if workingDir != "" {
+ cmd.Dir = workingDir
+ }
stdoutFile, err := os.Create(e.StdoutPath)
if err != nil {
@@ -103,22 +115,27 @@ func (r *GeminiRunner) execOnce(ctx context.Context, args []string, workingDir,
if err != nil {
return fmt.Errorf("creating stdout pipe: %w", err)
}
+ cmd.Stdout = stdoutW
+ cmd.Stderr = stderrFile
- // Simulate writing to stdoutW
+ if err := cmd.Start(); err != nil {
+ stdoutW.Close()
+ stdoutR.Close()
+ return fmt.Errorf("starting gemini: %w", err)
+ }
+ stdoutW.Close()
+
+ killDone := make(chan struct{})
go func() {
- defer stdoutW.Close() // Close the writer when done.
- fmt.Fprintf(stdoutW, "```json\n")
- fmt.Fprintf(stdoutW, "{\"type\":\"content_block_start\",\"content_block\":{\"text\":\"Hello, Gemini!\",\"type\":\"text\"}}\n")
- fmt.Fprintf(stdoutW, "{\"type\":\"content_block_delta\",\"content_block\":{\"text\":\" How are you?\"}}\n")
- fmt.Fprintf(stdoutW, "{\"type\":\"content_block_end\"}\n")
- fmt.Fprintf(stdoutW, "{\"type\":\"message_delta\",\"message\":{\"role\":\"model\"}}\n")
- fmt.Fprintf(stdoutW, "{\"type\":\"message_end\"}\n")
- fmt.Fprintf(stdoutW, "```\n")
+ select {
+ case <-ctx.Done():
+ syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+ case <-killDone:
+ }
}()
-
- var streamErr error
var streamCost float64
+ var streamErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
@@ -127,14 +144,26 @@ func (r *GeminiRunner) execOnce(ctx context.Context, args []string, workingDir,
stdoutR.Close()
}()
- wg.Wait() // Wait for parseGeminiStream to finish
+ waitErr := cmd.Wait()
+ close(killDone)
+ wg.Wait()
if streamCost > 0 {
e.CostUSD = streamCost
}
- // Set a dummy exit code for this simulated run
- e.ExitCode = 0
+ if waitErr != nil {
+ if exitErr, ok := waitErr.(*exec.ExitError); ok {
+ e.ExitCode = exitErr.ExitCode()
+ }
+ if streamErr != nil {
+ return streamErr
+ }
+ if tail := tailFile(e.StderrPath, 20); tail != "" {
+ return fmt.Errorf("gemini exited with error: %w\nstderr:\n%s", waitErr, tail)
+ }
+ return fmt.Errorf("gemini exited with error: %w", waitErr)
+ }
if streamErr != nil {
return streamErr
diff --git a/internal/storage/db.go b/internal/storage/db.go
index c871c77..ce60e2f 100644
--- a/internal/storage/db.go
+++ b/internal/storage/db.go
@@ -699,11 +699,6 @@ func scanTask(row scanner) (*task.Task, error) {
t.State = task.State(state)
t.Priority = task.Priority(priority)
t.Timeout.Duration = time.Duration(timeoutNS)
- // Add debug log for configJSON
- // The logger is not available directly in db.go, so I'll use fmt.Printf for now.
- // For production code, a logger should be injected.
- // fmt.Printf("DEBUG: configJSON from DB: %s\n", configJSON)
- // TODO: Replace with proper logger when available.
if err := json.Unmarshal([]byte(configJSON), &t.Agent); err != nil {
return nil, fmt.Errorf("unmarshaling agent config: %w", err)
}