diff --git a/internal/agent/runner/runner_test.go b/internal/agent/runner/runner_test.go index 239cdf7..f47d3a9 100644 --- a/internal/agent/runner/runner_test.go +++ b/internal/agent/runner/runner_test.go @@ -50,9 +50,16 @@ func (s *fakeSender) snapshot() []api.Envelope { // returns ETXTBSY ("text file busy"). Once the rename lands, the // final path is a fresh dirent pointing at an inode that has no // writable fd open anywhere — exec is safe. +// +// Temp dirs live under /dev/shm when it's writable (tmpfs). On a +// containerised CI runner, t.TempDir() lands on overlayfs, which +// has its own ETXTBSY surface that the rename pattern above does +// not fully close — the kernel's "writable inode" bookkeeping +// can lag the userspace close on overlay's upper layer. Falling +// back to a real tmpfs sidesteps the problem entirely. func setupScript(t *testing.T, body string) string { t.Helper() - dir := t.TempDir() + dir := tempDirForExec(t) final := filepath.Join(dir, "restic") tmp := final + ".tmp" if err := os.WriteFile(tmp, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil { @@ -64,6 +71,21 @@ func setupScript(t *testing.T, body string) string { return final } +// tempDirForExec returns a per-test temp dir that's safe to write +// then exec from. Uses /dev/shm (tmpfs) when available, otherwise +// falls back to t.TempDir(). See the setupScript comment for why. +func tempDirForExec(t *testing.T) string { + t.Helper() + if st, err := os.Stat("/dev/shm"); err == nil && st.IsDir() { + dir, err := os.MkdirTemp("/dev/shm", "rmrunner-") + if err == nil { + t.Cleanup(func() { _ = os.RemoveAll(dir) }) + return dir + } + } + return t.TempDir() +} + // firstEnvOfType returns the first envelope with the given type, or // fails the test if none is found. func firstEnvOfType(t *testing.T, envs []api.Envelope, mt api.MessageType) api.Envelope {