tests: never touch the user's real config, cache, or keychain · Entire
tests: never touch the user's real config, cache, or keychain
4801b30→main·
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 thetokenstoredefault backend all detectgo test(viainternal/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 sett.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())andtokenstore.UseFileBackendForTesting(...). - Spawned binaries are NOT covered:
testing.Testing()is false in a subprocess. The integration and e2e TestMains setENTIRE_CONFIG_DIR,XDG_CACHE_HOME,ENTIRE_TOKEN_STORE=file,ENTIRE_TOKEN_STORE_PATH, andENTIRE_TEST_AUTH_STORE_FILEprocess-wide so every spawnedentire(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 needkeyring.MockInit()inTestMain(seecmd/entire/cli/global_test.go) — thetestdirsfallback 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.
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")
}