Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions cmd/docket/config_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/danielhanold/docket/internal/testsupport"
)

// These tests drive the BUILT binary, so they are the only place the whole
Expand All @@ -23,7 +25,7 @@ import (
// mutates this process's environment, so parallel tests cannot interfere.
func hermeticEnv(t *testing.T) (xdgDir string, env []string) {
t.Helper()
base := t.TempDir()
base := testsupport.TempDir(t)
xdgDir = filepath.Join(base, "xdg")
homeDir := filepath.Join(base, "home")
for _, d := range []string{xdgDir, homeDir} {
Expand Down Expand Up @@ -58,7 +60,7 @@ func runEnv(t *testing.T, env []string, args ...string) (stdout, stderr string,
// sparseRepo is a repository directory with no configuration files at all.
func sparseRepo(t *testing.T) string {
t.Helper()
dir := filepath.Join(t.TempDir(), "repo")
dir := filepath.Join(testsupport.TempDir(t), "repo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("creating %s: %v", dir, err)
}
Expand All @@ -84,7 +86,7 @@ func copyFixtureRepo(t *testing.T, fixture string) string {
if _, err := os.Stat(src); err != nil {
t.Fatalf("fixture %s: %v", fixture, err)
}
dst := filepath.Join(t.TempDir(), "repo")
dst := filepath.Join(testsupport.TempDir(t), "repo")
err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
Expand Down Expand Up @@ -274,7 +276,7 @@ func TestConfigMissingRepoDirFlag(t *testing.T) {
// repository that is not there.
func TestConfigNonexistentRepoDir(t *testing.T) {
_, env := hermeticEnv(t)
missing := filepath.Join(t.TempDir(), "no-such-repo")
missing := filepath.Join(testsupport.TempDir(t), "no-such-repo")
out, errS, code := runEnv(t, env, "diagnostic", "config", "--repo-dir", missing,
"--default-branch", "main", "--for-mutation")
if code != 2 || out != "" {
Expand Down
6 changes: 4 additions & 2 deletions cmd/docket/devinstall_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/danielhanold/docket/internal/testsupport"
)

// witness is the name-independent core prohibition harness.RecursionGuard
Expand Down Expand Up @@ -34,7 +36,7 @@ const witness = "Do not dispatch another"
// on this first invocation.
func TestDevelopmentInstallFreshRenderHandoff(t *testing.T) {
source := moduleRoot(t)
home := t.TempDir()
home := testsupport.TempDir(t)

// The "old installed binary": a stub whose renderer omits the witness. It is
// the absence control that makes the witness a real discriminator — a
Expand Down Expand Up @@ -91,7 +93,7 @@ func moduleRoot(t *testing.T) string {
// line and exits 0, returning its path.
func buildWitnesslessStub(t *testing.T) string {
t.Helper()
dir := t.TempDir()
dir := testsupport.TempDir(t)
src := filepath.Join(dir, "main.go")
const prog = "package main\n\nimport \"fmt\"\n\nfunc main() { fmt.Println(\"old installed binary: nothing to see here\") }\n"
if err := os.WriteFile(src, []byte(prog), 0o644); err != nil {
Expand Down
35 changes: 7 additions & 28 deletions cmd/docket/gate_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,9 @@ import (
"path/filepath"
"testing"
"time"
)

// gateTempDir is a temp dir whose cleanup tolerates the external supervisor's
// brief exit window. Observe reports "passed" the instant the terminal record
// lands, which can precede the supervisor's final same-directory atomic write
// and lock release, so a single-shot RemoveAll (as t.TempDir does) races it and
// fails "directory not empty". The retry loop lets the supervisor finish.
func gateTempDir(t *testing.T) string {
t.Helper()
dir, err := os.MkdirTemp("", "docket-gate-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
for i := 0; i < 40; i++ {
if err := os.RemoveAll(dir); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
_ = os.RemoveAll(dir)
})
return dir
}
"github.com/danielhanold/docket/internal/testsupport"
)

// gateSh runs one git subcommand in dir through the real git binary, failing on
// any error. It is the built-binary package's local git helper (there is no
Expand All @@ -54,9 +33,9 @@ func gateDriveConfiguredRepo(t *testing.T, configBody string) string {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not found on PATH")
}
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", testsupport.TempDir(t))

root := gateTempDir(t)
root := testsupport.TempDir(t)
origin := filepath.Join(root, "origin.git")
writer := filepath.Join(root, "writer")
invocation := filepath.Join(root, "invocation")
Expand Down Expand Up @@ -104,7 +83,7 @@ func gateDriveConfiguredRepo(t *testing.T, configBody string) string {
// returning the shared protocol document with a drive id and the PASSED outcome.
func TestGateDriveEndToEndThroughBuiltBinary(t *testing.T) {
wt := gateDriveConfiguredRepo(t, "metadata_branch: main\nbuild:\n gate: local\n test_command: /bin/echo hi\n")
root := gateTempDir(t)
root := testsupport.TempDir(t)
out, errS, code := run(t, "--json", "gate", "drive", "start", "--repo-dir", wt, "--run-root", root, "--owner", "build")
if code != 0 || errS != "" {
t.Fatalf("start: out=%q err=%q code=%d", out, errS, code)
Expand All @@ -131,8 +110,8 @@ func TestGateDriveEndToEndThroughBuiltBinary(t *testing.T) {
// supervisor. It proves the whole cli -> app -> process path plus the one
// os.Exit site, not the test-binary seam the package tests use.
func TestGateEndToEndThroughBuiltBinary(t *testing.T) {
root := gateTempDir(t)
cwd := gateTempDir(t)
root := testsupport.TempDir(t)
cwd := testsupport.TempDir(t)

out, errS, code := run(t, "--json", "gate", "launch", "--root", root, "--cwd", cwd, "--", "/bin/echo", "hi")
if code != 0 || errS != "" {
Expand Down
9 changes: 5 additions & 4 deletions cmd/docket/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"

"github.com/danielhanold/docket/internal/assets"
"github.com/danielhanold/docket/internal/testsupport"
)

var binPath string
Expand Down Expand Up @@ -138,7 +139,7 @@ func TestDiagnosticRuntimeReflectsHost(t *testing.T) {
}

func TestInjectedBuildIdentity(t *testing.T) {
injected := filepath.Join(t.TempDir(), "docket-injected")
injected := filepath.Join(testsupport.TempDir(t), "docket-injected")
ldflags := "-X github.com/danielhanold/docket/internal/buildinfo.Version=1.2.3" +
" -X github.com/danielhanold/docket/internal/buildinfo.Commit=abc1234" +
" -X github.com/danielhanold/docket/internal/buildinfo.BuildDate=2026-08-13"
Expand Down Expand Up @@ -250,7 +251,7 @@ func TestCrossCompileApprovedTargets(t *testing.T) {
// Buildability gate only: the four tuples must compile with CGO off.
// Foreign binaries are never executed (change 0317 owns on-target runs).
tuples := [][2]string{{"darwin", "amd64"}, {"darwin", "arm64"}, {"linux", "amd64"}, {"linux", "arm64"}}
dir := t.TempDir()
dir := testsupport.TempDir(t)
for _, tp := range tuples {
out := filepath.Join(dir, "docket-"+tp[0]+"-"+tp[1])
cmd := exec.Command("go", "build", "-o", out, ".")
Expand Down Expand Up @@ -289,7 +290,7 @@ func runIn(t *testing.T, home string, args ...string) (stdout, stderr string, co
// installation-required reason. Field order is protocol too, so this is a byte
// comparison rather than a field-by-field one.
func TestInstallCheckJSONGolden(t *testing.T) {
home := t.TempDir()
home := testsupport.TempDir(t)
catalog, err := assets.EmbeddedCatalog()
if err != nil {
t.Fatalf("embedded catalog: %v", err)
Expand Down Expand Up @@ -337,7 +338,7 @@ func TestInstallCheckJSONGolden(t *testing.T) {
// TestInstallCommandsRegistered proves the three operations reached the built
// binary's command tree.
func TestInstallCommandsRegistered(t *testing.T) {
home := t.TempDir()
home := testsupport.TempDir(t)
out, errS, code := runIn(t, home, "--help")
if code != 0 || errS != "" {
t.Fatalf("err=%q code=%d", errS, code)
Expand Down
3 changes: 2 additions & 1 deletion cmd/releasepkg/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/danielhanold/docket/internal/release"
"github.com/danielhanold/docket/internal/testsupport"
)

var binPath string
Expand Down Expand Up @@ -102,7 +103,7 @@ func TestBadEpochIsUsageError(t *testing.T) {
// line, and the six bundle files. Deep artifact assertions live in Task 5's
// internal/release integration test; this is the command-surface check.
func TestHappyRunPackagesBundle(t *testing.T) {
outDir := t.TempDir()
outDir := testsupport.TempDir(t)
const version = "v0.0.1-cmdintegration"
stdout, errS, code := runCmd(t,
"--source", repoRoot(t),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!-- docket:backlink:start (generated — do not hand-edit) -->
> ↩ **[Change 0398 — Extend the testsupport temp-dir fixture and repoguard to cmd/ real-process test packages](https://github.com/danielhanold/docket/blob/docket/docs/changes/active/0398-extend-the-testsupport-temp-dir-fixture-and-repoguard-to-cmd.md)**
<!-- docket:backlink:end -->
# Extend the testsupport temp-dir fixture and repoguard to cmd/ real-process test packages — Results

**Human action:** None required beyond the normal PR review. This change touches only test code, and the widened guard was mutation-tested during the build.

## Outcome

Change 0373 added a shared temp-dir helper for tests that start real processes. That helper, `testsupport.TempDir`, tolerates a slow child process still holding the directory at cleanup. 0373 also added a guard test that rejects bare `t.TempDir()` calls in those packages, but the guard only covered `internal/`. The `cmd/` test packages were left unprotected and kept their own copy of the retry logic.

This change closes that gap:

- **Guard now covers `cmd/`.** The guard (`TestRealProcessPackagesUseFixtureTempDir`) scans an explicit root list of `internal` and `cmd`. A second minimum-coverage check now requires the scan to find `cmd/docket`, so a broken `cmd/` scan fails loudly instead of passing with nothing checked.
- **Bare temp dirs converted.** All 12 bare `t.TempDir()` calls in `cmd/docket` and `cmd/releasepkg` now use `testsupport.TempDir(t)`.
- **Private helper removed.** The `gateTempDir` helper in `cmd/docket/gate_cli_test.go` is deleted, and its four callers now use the shared helper.

## Verification performed

- The guard failed first, as expected, with exactly the 12 `cmd/` violations. It passed after the conversions.
- Mutation probes:
- Reverting one converted site in `cmd/docket` failed the guard, naming that file.
- Reverting the converted site in `cmd/releasepkg` failed the guard, naming that file.
- Removing `cmd` from the root list failed the new `cmd/docket` minimum-coverage check.
- The gate end-to-end tests passed three times in a row after `gateTempDir` was removed. The `cmd/...` and `internal/repoguard` tests also passed.
- The full-suite build gate ran at the final head. Its result is recorded in the build-evidence block of the PR.

## Known issues and follow-ups

### Removing both the scan root and its minimum-coverage check passes silently

If someone deletes `cmd` from the guard's root list and also deletes the `cmd/docket` minimum-coverage entry, the guard still passes and no longer checks `cmd/`. The probe confirmed this. It needs two deliberate edits to the same test, which would show up plainly in review, so it is the accepted remaining gap. No action is needed.

### A second private `gateTempDir` remains in `internal/cli/gate_test.go`

`internal/cli/gate_test.go` defines the same drain-then-retry helper. The guard does not flag it because it creates its directory with `os.MkdirTemp` rather than `t.TempDir()`. This change's scope excludes `internal/`, so the helper was left alone. Suggested next step: capture a small follow-up change to switch it to `testsupport.TempDir`.
Loading