diff --git a/codeagent-wrapper/internal/app/cli.go b/codeagent-wrapper/internal/app/cli.go index bb64d66..ef3f69a 100644 --- a/codeagent-wrapper/internal/app/cli.go +++ b/codeagent-wrapper/internal/app/cli.go @@ -198,10 +198,9 @@ func runWithLoggerAndCleanup(fn func() int) (exitCode int) { for _, entry := range entries { fmt.Fprintln(os.Stderr, entry) } - fmt.Fprintf(os.Stderr, "Log file: %s (deleted)\n", logger.Path()) + fmt.Fprintf(os.Stderr, "Log file: %s\n", logger.Path()) } } - _ = logger.RemoveLogFile() }() defer runCleanupHook() @@ -689,6 +688,13 @@ func runSingleMode(cfg *Config, name string) int { result := runTaskFn(taskSpec, false, cfg.Timeout) if result.ExitCode != 0 { + // Surface any parsed backend output even on non-zero exit to avoid "(no output)" in tool runners. + if strings.TrimSpace(result.Message) != "" { + fmt.Println(result.Message) + if result.SessionID != "" { + fmt.Printf("\n---\nSESSION_ID: %s\n", result.SessionID) + } + } return result.ExitCode } diff --git a/codeagent-wrapper/internal/app/main_test.go b/codeagent-wrapper/internal/app/main_test.go index 3a58bdb..4f597ce 100644 --- a/codeagent-wrapper/internal/app/main_test.go +++ b/codeagent-wrapper/internal/app/main_test.go @@ -4342,9 +4342,9 @@ func TestRun_ExplicitStdinReadError(t *testing.T) { if !strings.Contains(logOutput, "Failed to read stdin: broken stdin") { t.Fatalf("log missing read error entry, got %q", logOutput) } - // Log file is always removed after completion (new behavior) - if _, err := os.Stat(logPath); !os.IsNotExist(err) { - t.Fatalf("log file should be removed after completion") + // Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`. + if _, err := os.Stat(logPath); err != nil { + t.Fatalf("expected log file to exist after completion: %v", err) } } @@ -4360,6 +4360,51 @@ func TestRun_CommandFails(t *testing.T) { } } +func TestRun_NonZeroExitPrintsParsedMessage(t *testing.T) { + defer resetTestHooks() + + tempDir := t.TempDir() + var scriptPath string + if runtime.GOOS == "windows" { + scriptPath = filepath.Join(tempDir, "codex.bat") + script := "@echo off\r\n" + + "echo {\"type\":\"thread.started\",\"thread_id\":\"tid\"}\r\n" + + "echo {\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\",\"text\":\"parsed-error\"}}\r\n" + + "exit /b 1\r\n" + if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { + t.Fatalf("failed to write script: %v", err) + } + } else { + scriptPath = filepath.Join(tempDir, "codex.sh") + script := `#!/bin/sh +printf '%s\n' '{"type":"thread.started","thread_id":"tid"}' +printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"parsed-error"}}' +sleep 0.05 +exit 1 +` + if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { + t.Fatalf("failed to write script: %v", err) + } + } + + restore := withBackend(scriptPath, func(cfg *Config, targetArg string) []string { return []string{} }) + defer restore() + + os.Args = []string{"codeagent-wrapper", "task"} + stdinReader = strings.NewReader("") + isTerminalFn = func() bool { return true } + + var exitCode int + output := captureOutput(t, func() { exitCode = run() }) + if exitCode != 1 { + t.Fatalf("exit=%d, want 1", exitCode) + } + + if !strings.Contains(output, "parsed-error") { + t.Fatalf("stdout=%q, want parsed backend message", output) + } +} + func TestRun_InvalidBackend(t *testing.T) { defer resetTestHooks() os.Args = []string{"codeagent-wrapper", "--backend", "unknown", "task"} @@ -4439,9 +4484,9 @@ func TestRun_PipedTaskReadError(t *testing.T) { if !strings.Contains(logOutput, "Failed to read piped stdin: read stdin: pipe failure") { t.Fatalf("log missing piped read error, got %q", logOutput) } - // Log file is always removed after completion (new behavior) - if _, err := os.Stat(logPath); !os.IsNotExist(err) { - t.Fatalf("log file should be removed after completion") + // Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`. + if _, err := os.Stat(logPath); err != nil { + t.Fatalf("expected log file to exist after completion: %v", err) } } @@ -4495,12 +4540,12 @@ func TestRun_LoggerLifecycle(t *testing.T) { if !fileExisted { t.Fatalf("log file was not present during run") } - if _, err := os.Stat(logPath); !os.IsNotExist(err) { - t.Fatalf("log file should be removed on success, but it exists") + if _, err := os.Stat(logPath); err != nil { + t.Fatalf("expected log file to exist on success: %v", err) } } -func TestRun_LoggerRemovedOnSignal(t *testing.T) { +func TestRun_LoggerKeptOnSignal(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("signal-based test is not supported on Windows") } @@ -4537,9 +4582,10 @@ printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"l exitCh := make(chan int, 1) go func() { exitCh <- run() }() - deadline := time.Now().Add(1 * time.Second) + deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { - if _, err := os.Stat(logPath); err == nil { + data, err := os.ReadFile(logPath) + if err == nil && strings.Contains(string(data), "Starting ") { break } time.Sleep(10 * time.Millisecond) @@ -4559,9 +4605,9 @@ printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"l if exitCode != 130 { t.Fatalf("exit code = %d, want 130", exitCode) } - // Log file is always removed after completion (new behavior) - if _, err := os.Stat(logPath); !os.IsNotExist(err) { - t.Fatalf("log file should be removed after completion") + // Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`. + if _, err := os.Stat(logPath); err != nil { + t.Fatalf("expected log file to exist after completion: %v", err) } } @@ -4822,6 +4868,34 @@ func TestParallelLogPathInSerialMode(t *testing.T) { } } +func TestRun_KeptLogFileOnSuccess(t *testing.T) { + defer resetTestHooks() + + tempDir := setTempDirEnv(t, t.TempDir()) + + os.Args = []string{"codeagent-wrapper", "do-stuff"} + stdinReader = strings.NewReader("") + isTerminalFn = func() bool { return true } + codexCommand = createFakeCodexScript(t, "cli-session", "ok") + buildCodexArgsFn = func(cfg *Config, targetArg string) []string { return []string{} } + cleanupLogsFn = nil + + var exitCode int + _ = captureStderr(t, func() { + _ = captureOutput(t, func() { + exitCode = run() + }) + }) + if exitCode != 0 { + t.Fatalf("run() exit = %d, want 0", exitCode) + } + + expectedLog := filepath.Join(tempDir, fmt.Sprintf("codeagent-wrapper-%d.log", os.Getpid())) + if _, err := os.Stat(expectedLog); err != nil { + t.Fatalf("expected log file to exist: %v", err) + } +} + func TestRun_CLI_Success(t *testing.T) { defer resetTestHooks() os.Args = []string{"codeagent-wrapper", "do-things"} diff --git a/codeagent-wrapper/internal/executor/executor.go b/codeagent-wrapper/internal/executor/executor.go index 1243b21..1f95301 100644 --- a/codeagent-wrapper/internal/executor/executor.go +++ b/codeagent-wrapper/internal/executor/executor.go @@ -1435,6 +1435,15 @@ waitLoop: logErrorFn(fmt.Sprintf("%s exited with status %d", commandName, code)) result.ExitCode = code result.Error = attachStderr(fmt.Sprintf("%s exited with status %d", commandName, code)) + // Preserve parsed output when the backend exits non-zero (e.g. API error with stream-json output). + result.Message = parsed.message + result.SessionID = parsed.threadID + if stdoutLogger != nil { + stdoutLogger.Flush() + } + if stderrLogger != nil { + stderrLogger.Flush() + } return result } logErrorFn(commandName + " error: " + waitErr.Error())