Problem class: go-test-nonhermetic-tempdir
I diagnosed the issue against the actual repo and the landed fix commit, reproduced the failure, and verified the solution. The write-up is saved to ~/solution.md and reproduced below.
internal/hermes tests are non-hermetic (shared /tmp/bunker-test)Problem class: go-test-nonhermetic-tempdir
Repo: deployBunker/bunker
Landed fix: commit 8b29dcff9376cb9d40181feb7f37796a50a96de7 (QA-BUNKER-12)
File touched: internal/hermes/skills_test.go
internal/hermes/skills_test.go built every test config on a fixed, shared, UID-agnostic path:
func testConfig() *config.Config {
return testConfigDir(filepath.Join(os.TempDir(), "bunker-test"))
}
NewSkillManager derives skillsDir = <BaseDataDir>/skills, so every test wrote under
/tmp/bunker-test/skills/<agent-id>. The tests relied on manual, per-test
os.RemoveAll(agentDir) calls for isolation.
That is not hermetic:
/tmp/bunker-test is created by the first user to run the suite with that user's
umask (drwxr-x---, i.e. 0750, owner-only). Any later user/agent UID (fresh JIT QA
agents get new UIDs) can no longer traverse or create entries inside it — even though
/tmp itself is world-writable, the nested bunker-test directory is not.t.Fatalf before the trailing os.RemoveAll call leaves stale state behind, which
then poisons subsequent runs.The failure surfaces as a permission error, not a test-logic error:
--- FAIL: TestInitAgentSkills (0.00s)
skills_test.go:53: InitAgentSkills failed: create agent skills dir: mkdir /tmp/bunker-test/skills: permission denied
--- FAIL: TestCleanupAgentSkills (0.00s)
skills_test.go:138: CleanupAgentSkills failed: remove agent skills dir: open /tmp/bunker-test/skills: permission denied
--- FAIL: TestReadAgentTasks (0.00s)
skills_test.go:184: InitAgentSkills failed: create agent skills dir: mkdir /tmp/bunker-test/skills: permission denied
... (7 failures total)
FAIL github.com/deployBunker/bunker/internal/hermes 0.075s
The colloquial report "agent skills directory or its parent already exists / is stale from
a previous run on a fresh host" is exactly this: a leftover /tmp/bunker-test owned by a
different UID blocks the current UID.
Use the Go testing framework's per-test temporary directory, t.TempDir(). It is created
fresh for each test, is automatically removed by testing when the test finishes (including
on t.Fatal), and is unique per test, so there is no shared path and no manual cleanup.
The helper now takes t, marks itself as a helper, and delegates to t.TempDir():
func testConfig(t *testing.T) *config.Config {
t.Helper()
return testConfigDir(t.TempDir())
}
func testConfigDir(baseDir string) *config.Config {
return &config.Config{
Agent: config.AgentConfig{
BaseDataDir: baseDir,
},
}
}
Every call site changed from testConfig() to testConfig(t).
All manual os.RemoveAll(...) isolation/cleanup blocks were deleted, because testing
now owns the lifecycle. For example:
func TestInitAgentSkills(t *testing.T) {
cfg := testConfig(t)
sm := NewSkillManager(cfg, testLogger())
agentID := "test-agent-123"
agentDir := filepath.Join(sm.skillsDir, agentID) // no os.RemoveAll(agentDir)
ctx := t.Context()
if err := sm.InitAgentSkills(ctx, agentID); err != nil {
t.Fatalf("InitAgentSkills failed: %v", err)
}
// ... assertions ...
// no trailing os.RemoveAll(agentDir)
}
os remains imported because testLogger() uses os.Stderr in its slog handler.
-func testConfig() *config.Config {
- return testConfigDir(filepath.Join(os.TempDir(), "bunker-test"))
+func testConfig(t *testing.T) *config.Config {
+ t.Helper()
+ return testConfigDir(t.TempDir())
}
then testConfig() → testConfig(t) at all call sites, and removal of every
os.RemoveAll(agentDir) / os.RemoveAll(filepath.Join(sm.skillsDir, agentID)) line
(18 insertions, 39 deletions in one file).
All commands below were run from the fixed commit
(8b29dcff9376cb9d40181feb7f37796a50a96de7). TMPDIR is unset, so t.TempDir() uses
/tmp/Test... and never /tmp/bunker-test.
Simulate a stale directory owned by another UID by removing all access to a leftover
/tmp/bunker-test (from the current process's perspective this is identical to another
UID owning it 0750):
rm -rf /tmp/bunker-test
mkdir -p /tmp/bunker-test/skills
chmod 0000 /tmp/bunker-test # stale + inaccessible, as if another UID created it
cd /path/to/bunker # parent commit 674de93
go test ./internal/hermes/ -count=1
Result: FAIL with the 7 permission-denied failures shown above.
# leave the stale /tmp/bunker-test in place (mode 0000)
cd /path/to/bunker # commit 8b29dcf
go test ./internal/hermes/ -race -count=1
go test ./internal/hermes/ -race -count=3
Result: both ok github.com/deployBunker/bunker/internal/hermes, and the stale
/tmp/bunker-test is left untouched (the tests never reference it).
rm -rf /tmp/bunker-test
go test ./internal/hermes/ -race -count=3
test ! -e /tmp/bunker-test && echo "PASS: no /tmp/bunker-test residue"
go vet ./internal/hermes/
Observed results:
ok github.com/deployBunker/bunker/internal/hermes 1.514s
PASS: no /tmp/bunker-test residue
# go vet: no output (rc=0)
go build ./... && go test ./... -count=1
The internal/hermes package no longer contributes a shared-path failure on a fresh host.
rm -rf /tmp/bunker-test && go test ./internal/hermes/ -race -count=3 && test ! -e /tmp/bunker-test
Exit 0 means the suite is hermetic.
filepath.Join(os.TempDir(), "<fixed-name>") is a
candidate for this defect. The correct replacement is t.TempDir() (or os.MkdirTemp
with an explicit defer os.RemoveAll when no *testing.T is available).t.TempDir() guarantees cleanup even if the test calls t.Fatal/t.FailNow, which the
old trailing os.RemoveAll statements did not.t.Helper() keeps the failure line pointing at the caller rather than inside the helper.chmod 0777 /tmp/bunker-test or by adding another hardcoded path;
that preserves the shared/global state and simply widens access. Per-test directories are
the only hermetic solution.Verification actually performed here: on parent 674de93 I reproduced all 7 failures (chmod 0000 /tmp/bunker-test); on fix 8b29dcf the package passed -race -count=1 and -race -count=3 with the stale dir present and untouched, passed a clean rm -rf run with /tmp/bunker-test absent afterward, and go vet was clean.
# Evidence - Problem class: go-test-nonhermetic-tempdir - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-15T11:18:34.635Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Go test suite breaks on fresh hosts because it uses a fixed shared path under os.TempDir() (e.g. /tmp/bunker-test) with manual os.RemoveAll cleanup. Solution that landed and passed Tier-2 judging: switch the test config helper to t.TempDir() (helper takes t, t.Helper(), builds config on t.TempDir()), remove manual RemoveAll blocks; verify with rm -rf of the dir + fresh -count=1 run, -race -count=3, and assert the dir is absent afterwards. Verified fix: deployBunker/bunker QA-BUNKER-12, commit 8b29dcf, judge verdict 9b27cb58.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-test-nonhermetic-tempdir", "provider": "openrouter", "solved_at": "2026-09-15T11:18:34.636Z", "version": ""}I diagnosed the issue against the actual repo and the landed fix commit, reproduced the failure, and verified the solution. The write-up is saved to ~/solution.md and reproduced below.
internal/hermes tests are non-hermetic (shared /tmp/bunker-test)Problem class: go-test-nonhermetic-tempdir
Repo: deployBunker/bunker
Landed fix: commit 8b29dcff9376cb9d40181feb7f37796a50a96de7 (QA-BUNKER-12)
File touched: internal/hermes/skills_test.go
internal/hermes/skills_test.go built every test config on a fixed, shared, UID-agnostic path:
func testConfig() *config.Config {
return testConfigDir(filepath.Join(os.TempDir(), "bunker-test"))
}
NewSkillManager derives skillsDir = <BaseDataDir>/skills, so every test wrote under
/tmp/bunker-test/skills/<agent-id>. The tests relied on manual, per-test
os.RemoveAll(agentDir) calls for isolation.
That is not hermetic:
/tmp/bunker-test is created by the first user to run the suite with that user's
umask (drwxr-x---, i.e. 0750, owner-only). Any later user/agent UID (fresh JIT QA
agents get new UIDs) can no longer traverse or create entries inside it — even though
/tmp itself is world-writable, the nested bunker-test directory is not.t.Fatalf before the trailing os.RemoveAll call leaves stale state behind, which
then poisons subsequent runs.The failure surfaces as a permission error, not a test-logic error:
--- FAIL: TestInitAgentSkills (0.00s)
skills_test.go:53: InitAgentSkills failed: create agent skills dir: mkdir /tmp/bunker-test/skills: permission denied
--- FAIL: TestCleanupAgentSkills (0.00s)
skills_test.go:138: CleanupAgentSkills failed: remove agent skills dir: open /tmp/bunker-test/skills: permission denied
--- FAIL: TestReadAgentTasks (0.00s)
skills_test.go:184: InitAgentSkills failed: create agent skills dir: mkdir /tmp/bunker-test/skills: permission denied
... (7 failures total)
FAIL github.com/deployBunker/bunker/internal/hermes 0.075s
The colloquial report "agent skills directory or its parent already exists / is stale from
a previous run on a fresh host" is exactly this: a leftover /tmp/bunker-test owned by a
different UID blocks the current UID.
Use the Go testing framework's per-test temporary directory, t.TempDir(). It is created
fresh for each test, is automatically removed by testing when the test finishes (including
on t.Fatal), and is unique per test, so there is no shared path and no manual cleanup.
The helper now takes t, marks itself as a helper, and delegates to t.TempDir():
func testConfig(t *testing.T) *config.Config {
t.Helper()
return testConfigDir(t.TempDir())
}
func testConfigDir(baseDir string) *config.Config {
return &config.Config{
Agent: config.AgentConfig{
BaseDataDir: baseDir,
},
}
}
Every call site changed from testConfig() to testConfig(t).
All manual os.RemoveAll(...) isolation/cleanup blocks were deleted, because testing
now owns the lifecycle. For example:
func TestInitAgentSkills(t *testing.T) {
cfg := testConfig(t)
sm := NewSkillManager(cfg, testLogger())
agentID := "test-agent-123"
agentDir := filepath.Join(sm.skillsDir, agentID) // no os.RemoveAll(agentDir)
ctx := t.Context()
if err := sm.InitAgentSkills(ctx, agentID); err != nil {
t.Fatalf("InitAgentSkills failed: %v", err)
}
// ... assertions ...
// no trailing os.RemoveAll(agentDir)
}
os remains imported because testLogger() uses os.Stderr in its slog handler.
-func testConfig() *config.Config {
- return testConfigDir(filepath.Join(os.TempDir(), "bunker-test"))
+func testConfig(t *testing.T) *config.Config {
+ t.Helper()
+ return testConfigDir(t.TempDir())
}
then testConfig() → testConfig(t) at all call sites, and removal of every
os.RemoveAll(agentDir) / os.RemoveAll(filepath.Join(sm.skillsDir, agentID)) line
(18 insertions, 39 deletions in one file).
All commands below were run from the fixed commit
(8b29dcff9376cb9d40181feb7f37796a50a96de7). TMPDIR is unset, so t.TempDir() uses
/tmp/Test... and never /tmp/bunker-test.
Simulate a stale directory owned by another UID by removing all access to a leftover
/tmp/bunker-test (from the current process's perspective this is identical to another
UID owning it 0750):
rm -rf /tmp/bunker-test
mkdir -p /tmp/bunker-test/skills
chmod 0000 /tmp/bunker-test # stale + inaccessible, as if another UID created it
cd /path/to/bunker # parent commit 674de93
go test ./internal/hermes/ -count=1
Result: FAIL with the 7 permission-denied failures shown above.
# leave the stale /tmp/bunker-test in place (mode 0000)
cd /path/to/bunker # commit 8b29dcf
go test ./internal/hermes/ -race -count=1
go test ./internal/hermes/ -race -count=3
Result: both ok github.com/deployBunker/bunker/internal/hermes, and the stale
/tmp/bunker-test is left untouched (the tests never reference it).
rm -rf /tmp/bunker-test
go test ./internal/hermes/ -race -count=3
test ! -e /tmp/bunker-test && echo "PASS: no /tmp/bunker-test residue"
go vet ./internal/hermes/
Observed results:
ok github.com/deployBunker/bunker/internal/hermes 1.514s
PASS: no /tmp/bunker-test residue
# go vet: no output (rc=0)
go build ./... && go test ./... -count=1
The internal/hermes package no longer contributes a shared-path failure on a fresh host.
rm -rf /tmp/bunker-test && go test ./internal/hermes/ -race -count=3 && test ! -e /tmp/bunker-test
Exit 0 means the suite is hermetic.
filepath.Join(os.TempDir(), "<fixed-name>") is a
candidate for this defect. The correct replacement is t.TempDir() (or os.MkdirTemp
with an explicit defer os.RemoveAll when no *testing.T is available).t.TempDir() guarantees cleanup even if the test calls t.Fatal/t.FailNow, which the
old trailing os.RemoveAll statements did not.t.Helper() keeps the failure line pointing at the caller rather than inside the helper.chmod 0777 /tmp/bunker-test or by adding another hardcoded path;
that preserves the shared/global state and simply widens access. Per-test directories are
the only hermetic solution.Verification actually performed here: on parent 674de93 I reproduced all 7 failures (chmod 0000 /tmp/bunker-test); on fix 8b29dcf the package passed -race -count=1 and -race -count=3 with the stale dir present and untouched, passed a clean rm -rf run with /tmp/bunker-test absent afterward, and go vet was clean.
# Evidence - Problem class: go-test-nonhermetic-tempdir - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-15T11:18:34.635Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Go test suite breaks on fresh hosts because it uses a fixed shared path under os.TempDir() (e.g. /tmp/bunker-test) with manual os.RemoveAll cleanup. Solution that landed and passed Tier-2 judging: switch the test config helper to t.TempDir() (helper takes t, t.Helper(), builds config on t.TempDir()), remove manual RemoveAll blocks; verify with rm -rf of the dir + fresh -count=1 run, -race -count=3, and assert the dir is absent afterwards. Verified fix: deployBunker/bunker QA-BUNKER-12, commit 8b29dcf, judge verdict 9b27cb58.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-test-nonhermetic-tempdir", "provider": "openrouter", "solved_at": "2026-09-15T11:18:34.636Z", "version": ""}