From 74331e2a9670d3466e85c0eeb3601bc9627c445a Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 23 Apr 2026 01:48:43 +0800 Subject: [PATCH] test(agent): serialize fake-executable writes to avoid ETXTBSY on CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestKimiBackendInvokesACPSubcommand (and its Kimi/Codex siblings) write a shell script to a per-test TempDir and then fork/exec it. With t.Parallel() enabled across the package, a concurrent goroutine's fork can inherit the still-open write fd to another test's new executable; Linux then rejects the subsequent exec with ETXTBSY (seen as fork/exec /tmp/.../kimi: text file busy on GitHub Actions). Introduce writeTestExecutable, which holds syscall.ForkLock.RLock across OpenFile→Write→Close. Fork (which takes ForkLock.Lock) cannot run while we hold RLock, so no sibling fork inherits our write fd. Ran the three callers with -count=10 under -p=1 and the full package with no failures. --- server/pkg/agent/codex_test.go | 5 +--- server/pkg/agent/exec_fixture_unix_test.go | 32 ++++++++++++++++++++++ server/pkg/agent/kimi_test.go | 8 ++---- 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 server/pkg/agent/exec_fixture_unix_test.go diff --git a/server/pkg/agent/codex_test.go b/server/pkg/agent/codex_test.go index 314af8d45..f379c7fa3 100644 --- a/server/pkg/agent/codex_test.go +++ b/server/pkg/agent/codex_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "log/slog" - "os" "path/filepath" "runtime" "strings" @@ -974,9 +973,7 @@ func TestCodexExecuteSurfacesStderrWhenChildExitsEarly(t *testing.T) { script := "#!/bin/sh\n" + "echo \"error: unexpected argument '-m' found\" >&2\n" + "exit 2\n" - if err := os.WriteFile(fakePath, []byte(script), 0o755); err != nil { - t.Fatalf("write fake codex: %v", err) - } + writeTestExecutable(t, fakePath, []byte(script)) backend, err := New("codex", Config{ExecutablePath: fakePath, Logger: slog.Default()}) if err != nil { diff --git a/server/pkg/agent/exec_fixture_unix_test.go b/server/pkg/agent/exec_fixture_unix_test.go new file mode 100644 index 000000000..f73bf3945 --- /dev/null +++ b/server/pkg/agent/exec_fixture_unix_test.go @@ -0,0 +1,32 @@ +//go:build unix + +package agent + +import ( + "os" + "syscall" + "testing" +) + +// writeTestExecutable writes content to path with exec perms while holding +// syscall.ForkLock.RLock, so no concurrent t.Parallel() sibling can fork +// between our OpenFile and Close. Without this, Linux ETXTBSY fires when +// the sibling's fork child inherits our still-open write fd and the +// subsequent exec of the file sees "text file busy" (seen on CI as +// TestKimiBackendInvokesACPSubcommand: fork/exec ... text file busy). +func writeTestExecutable(tb testing.TB, path string, content []byte) { + tb.Helper() + syscall.ForkLock.RLock() + defer syscall.ForkLock.RUnlock() + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) + if err != nil { + tb.Fatalf("write test executable %s: open: %v", path, err) + } + if _, err := f.Write(content); err != nil { + _ = f.Close() + tb.Fatalf("write test executable %s: write: %v", path, err) + } + if err := f.Close(); err != nil { + tb.Fatalf("write test executable %s: close: %v", path, err) + } +} diff --git a/server/pkg/agent/kimi_test.go b/server/pkg/agent/kimi_test.go index 707213a26..1630edf43 100644 --- a/server/pkg/agent/kimi_test.go +++ b/server/pkg/agent/kimi_test.go @@ -103,9 +103,7 @@ func TestKimiBackendSetModelFailureFailsTask(t *testing.T) { t.Parallel() fakePath := filepath.Join(t.TempDir(), "kimi") - if err := os.WriteFile(fakePath, []byte(fakeKimiACPScript()), 0o755); err != nil { - t.Fatalf("write fake kimi: %v", err) - } + writeTestExecutable(t, fakePath, []byte(fakeKimiACPScript())) backend, err := New("kimi", Config{ExecutablePath: fakePath, Logger: slog.Default()}) if err != nil { @@ -164,9 +162,7 @@ func TestKimiBackendInvokesACPSubcommand(t *testing.T) { tempDir := t.TempDir() argsFile := filepath.Join(tempDir, "argv.txt") fakePath := filepath.Join(tempDir, "kimi") - if err := os.WriteFile(fakePath, []byte(fakeKimiACPScript()), 0o755); err != nil { - t.Fatalf("write fake kimi: %v", err) - } + writeTestExecutable(t, fakePath, []byte(fakeKimiACPScript())) backend, err := New("kimi", Config{ ExecutablePath: fakePath,