# tests: never touch the user's real config, cache, or keychain

`4801b30`→[main](/content/gh/entireio/cli/commits/main/index.html)·

toothbrush·1mo ago·11 files·+208 added/-5 removed

A test fixture (bob@core.example.com) leaked into a developer's real
~/.config/entire/contexts.json. Unit tests isolated config access only
by per-test convention (t.Setenv), and the integration/e2e harnesses
spawned the real entire binary with the developer's real environment —
no ENTIRE_CONFIG_DIR, no XDG_CACHE_HOME, real HOME, real OS keychain.

## Defense in depth:

- internal/testdirs (new): under `go test`, resolution of the config
dir, cache dir, and token store falls back to a throwaway per-process
temp directory when the explicit env override is unset, so an
unisolated test can never read or write real user state.
- Wired into contexts.DefaultConfigDir, discovery.DefaultCacheDir,
versioncheck, and the tokenstore default backend (which previously
defaulted to the real OS keyring even under test).
- versioncheck now honors ENTIRE_CONFIG_DIR before falling back to
~/.config/entire, so one env var isolates every config-dir surface.
- Integration and e2e TestMains export ENTIRE_CONFIG_DIR,
XDG_CACHE_HOME, and file-backed token-store env vars process-wide;
spawned binaries (where testing.Testing() is false) inherit them
through every os.Environ()-based env builder.
- CLAUDE.md documents the isolation rules for future harnesses.

## Sessions

83fe3fd21287View transcript

## Changes

11

- MCLAUDE.md+25

- cmd/entire/cli

- integration_test

- Msetup_test.go+21

- versioncheck

- Mversioncheck.go+11/-1
    - Mversioncheck_test.go+4/-4

- e2e/tests

- Mmain_test.go+8

- internal

- entireclient

- contexts

- Mcontexts.go+9
      - Mcontexts_test.go+16

- discovery

- Mcache.go+7

- tokenstore

- Mtokenstore.go+11

- testdirs

- Atestdirs.go+56
    - Atestdirs_test.go+40

171 unmodified lines

**Do NOT** shell out to `git init`/`git commit` directly without setting user config and `--no-gpg-sign`, and **do NOT** run lifecycle/strategy handlers from the real repo CWD in tests.

### Config/Cache/Keyring Isolation in Tests

Tests must never read or write the developer's real `~/.config/entire`
(contexts.json, version_check.json), `~/.cache/entire` (nodes.json,
cluster_cores.json, api_discovery.json), or OS keychain. The developer may be
using `entire` for real while tests run.

- **In-process safety net**: `contexts.DefaultConfigDir()`,
  `discovery.DefaultCacheDir()`, versioncheck's config path, and the
  `tokenstore` default backend all detect `go test` (via `internal/testdirs`)
  and fall back to a throwaway per-process temp directory when their env
  override is unset. The fallback is shared across tests in one process — for
  per-test isolation still set `t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())`
  and `tokenstore.UseFileBackendForTesting(...)`.
- **Spawned binaries are NOT covered**: `testing.Testing()` is false in a
  subprocess. The integration and e2e TestMains set `ENTIRE_CONFIG_DIR`,
  `XDG_CACHE_HOME`, `ENTIRE_TOKEN_STORE=file`, `ENTIRE_TOKEN_STORE_PATH`, and
  `ENTIRE_TEST_AUTH_STORE_FILE` process-wide so every spawned `entire` (and
every agent-invoked hook) inherits isolation. Any new harness that spawns
the real binary must do the same.
- **Legacy auth store**: `auth.NewStore()` talks straight to the zalando
  keyring; packages whose tests can reach it need `keyring.MockInit()` in
  `TestMain` (see `cmd/entire/cli/global_test.go`) — the `testdirs` fallback
does not cover it in-process.

### Spawning subprocesses in tests (TTY detection)

Tests that spawn the real `entire` or `git` binary need the child to be non-interactive so prompts don't hang on a developer terminal.

```go
MCLAUDE.md+25
```

20 unmodified lines

### globalConfigDirPath

// globalConfigDirPath returns the expanded path to the global config directory (~/.config/entire).
// globalConfigDirPath returns the CLI's global config directory:
// $ENTIRE_CONFIG_DIR if set, else ~/.config/entire. Under `go test` an
// unset ENTIRE_CONFIG_DIR resolves to a throwaway per-process directory
// instead of the real home (see internal/testdirs).
func globalConfigDirPath() (string, error) {
  if dir := os.Getenv("ENTIRE_CONFIG_DIR"); dir != "" {
    return dir, nil
  }
  if dir, ok := testdirs.Dir("config"); ok {
    return dir, nil
  }
  home, err := os.UserHomeDir()
  if err != nil {
    return "", fmt.Errorf("getting home directory: %w", err)
  }
  return filepath.Join(home, ".config", "entire"), nil
}

### DefaultCacheDir

// DefaultCacheDir returns ~/.cache/entire, respecting XDG_CACHE_HOME.
// Under `go test` an unset XDG_CACHE_HOME resolves to a throwaway
// per-process directory instead of the real home (see internal/testdirs).
func DefaultCacheDir() string {
  if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
    return filepath.Join(xdg, "entire")
  }
  if dir, ok := testdirs.Dir("cache"); ok {
    return filepath.Join(dir, "entire")
  }
  home, _ := os.UserHomeDir() //nolint:errcheck // best-effort default
  return filepath.Join(home, ".cache", "entire")
}

```
