carlos / platform Public

Merge branch 'test-config-isolation' into main

claude agent, operated by paul@keymail.dev ae66fb64375af967223287caff73b9c3cba22acb parent 00a3ba3parent 2b715f1
5 files changed, +86 −8
  • AGENTS.md +7 −0
  • accounts_list_test.go +2 −2
  • apimode_test.go +3 −3
  • config.go +31 −0
  • config_test.go +43 −3

CI passed — run details

diff --git a/AGENTS.md b/AGENTS.md
index 3447c3be1..20a55f2b3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -229,6 +229,13 @@ anything.
`.carlos/config`. The fallback is the operator's real login and default
console; a logged-in laptop running `go test ./...` minted 40+ real accounts
on the production console before this was isolated.
+- **A known-failing test is a defect in its own right.** `log.Fatal` inside
+ an in-process test aborts the whole binary rather than failing the one
+ test, so everything after it silently never runs — and what gets read
+ afterward is the failure COUNT, not the names. A red root package sat
+ "known-failing" for a week while `TestStoreVerbDispatchAndRefusals` hid a
+ stale usage assertion behind it, and CI stayed red on every push with
+ nobody the wiser (discussion #17).
- **Absence of `apps/<acct>/<app>/pipeline.json` = the legacy ladder**,
byte-for-byte; new apps get a one-channel `edge` pipeline at create;
per-channel rules are console-enforced, bake windows box-enforced through
diff --git a/accounts_list_test.go b/accounts_list_test.go
index 3bb1a8610..de32f4bc0 100644
--- a/accounts_list_test.go
+++ b/accounts_list_test.go
@@ -31,7 +31,7 @@ func signedInConsole(t *testing.T, handler http.HandlerFunc) string {
// accounts list, signed-in: no registry on this machine, a credential and a
// console that answers — the no-infra path #157 was filed for.
func TestAccountsListSignedIn(t *testing.T) {
- useTempCredentials(t)
+ useIsolatedConfig(t)
signedInConsole(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/cli/apps" || r.Header.Get("Authorization") != "Bearer tok" {
http.NotFound(w, r)
@@ -58,7 +58,7 @@ func TestAccountsListSignedIn(t *testing.T) {
// A console from before the listing endpoint gets a plain named refusal,
// not the membership-404 riddle.
func TestAccountsListOldConsole(t *testing.T) {
- useTempCredentials(t)
+ useIsolatedConfig(t)
url := signedInConsole(t, func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
})
diff --git a/apimode_test.go b/apimode_test.go
index c459e9004..681094f26 100644
--- a/apimode_test.go
+++ b/apimode_test.go
@@ -85,7 +85,7 @@ func TestResolveModeDirWins(t *testing.T) {
}
func TestResolveModeAPI(t *testing.T) {
- useTempCredentials(t)
+ useIsolatedConfig(t)
clearDeploymentEnv(t)
f, _ := loadCredentials()
@@ -113,7 +113,7 @@ func TestResolveModeAPI(t *testing.T) {
}
func TestResolveModeNothing(t *testing.T) {
- useTempCredentials(t)
+ useIsolatedConfig(t)
clearDeploymentEnv(t)
forceTerminal(t, false) // pin: this asserts the NON-interactive refusal
@@ -137,7 +137,7 @@ func TestResolveModeNothing(t *testing.T) {
// all, and deserves a message that says so instead of offering to set up
// direct mode from scratch.
func TestResolveModeExpiredCredentialOnly(t *testing.T) {
- useTempCredentials(t)
+ useIsolatedConfig(t)
clearDeploymentEnv(t)
forceTerminal(t, false) // pin: this asserts the NON-interactive refusal
diff --git a/config.go b/config.go
index efa918da0..b63847ab5 100644
--- a/config.go
+++ b/config.go
@@ -59,7 +59,23 @@ type cliConfig struct {
// that pick() accepts, returning it with its path. The zero config and ""
// mean "no file said anything". Every per-key default walk (account, app,
// update_check) is this walk with a different pick.
+//
+// CARLOS_CONFIG, when set, skips this discovery entirely: the named file is
+// the only one consulted, whether or not pick() accepts it. This is the
+// override configDefault documents in the same terms as CARLOS_CREDENTIALS
+// (auth.go credentialsPath) — for tests, and for anyone keeping their
+// config somewhere other than $HOME's discovery path.
func walkConfig(pick func(cliConfig) bool) (cliConfig, string, error) {
+ if p := os.Getenv("CARLOS_CONFIG"); p != "" {
+ cfg, err := loadConfig(p)
+ if err != nil {
+ return cliConfig{}, "", err
+ }
+ if !pick(cfg) {
+ return cliConfig{}, "", nil
+ }
+ return cfg, p, nil
+ }
if dir, werr := os.Getwd(); werr == nil {
for {
path := filepath.Join(dir, ".carlos", "config")
@@ -164,7 +180,22 @@ func loadConfigFile(path string) (string, error) {
// rule — the global default arriving one step early, same answer. The
// explicit global check after the walk is for working directories outside
// $HOME entirely.
+//
+// CARLOS_CONFIG, when set, names the one file consulted and skips discovery
+// entirely — see walkConfig's identical override, which every other
+// per-key default (account, app, update_check) already goes through. An
+// unset CARLOS_CONFIG leaves this walk exactly as it was.
func configDefault() (console, source string, err error) {
+ if p := os.Getenv("CARLOS_CONFIG"); p != "" {
+ c, err := loadConfigFile(p)
+ if err != nil {
+ return "", "", err
+ }
+ if c == "" {
+ return "", "", nil
+ }
+ return c, p, nil
+ }
if dir, werr := os.Getwd(); werr == nil {
for {
path := filepath.Join(dir, ".carlos", "config")
diff --git a/config_test.go b/config_test.go
index d727a1356..1089631c0 100644
--- a/config_test.go
+++ b/config_test.go
@@ -24,6 +24,33 @@ func writeConfig(t *testing.T, dir, body string) string {
return path
}
+// useIsolatedConfig isolates an in-process test the way carlosTestEnv
+// isolates a subprocess (e2e_test.go): CARLOS_CONFIG is pointed at a path
+// inside a fresh t.TempDir() where no config exists, which makes walkConfig
+// and configDefault skip discovery entirely — no cwd walk, no global
+// fallback — so a developer's real ~/.carlos/config can never answer for a
+// test. HOME is also set to a separate scratch dir (some paths, like
+// credentialsPath's fallback, still derive from it), and CARLOS_CONSOLE and
+// CARLOS_CREDENTIALS are cleared so neither leaks in from the environment
+// running the test binary.
+//
+// This is deliberately an env override rather than a chdir: t.Chdir forbids
+// t.Parallel(), and CARLOS_CONFIG makes the cwd walk irrelevant for any test
+// that isn't exercising discovery itself. TestConfigDefaultWalk is the one
+// test that genuinely needs the walk, so it builds its own tree with
+// t.Chdir instead of calling this helper.
+//
+// Duplicated wherever package main_test needs the same isolation: this file
+// is package main, e2e_test.go is an external test package, and the two
+// cannot share an unexported helper across the package boundary.
+func useIsolatedConfig(t *testing.T) {
+ t.Helper()
+ t.Setenv("HOME", t.TempDir())
+ t.Setenv("CARLOS_CONFIG", filepath.Join(t.TempDir(), "config"))
+ t.Setenv("CARLOS_CONSOLE", "")
+ t.Setenv("CARLOS_CREDENTIALS", "")
+}
+
func TestLoadConfigFile(t *testing.T) {
useTempCredentials(t)
dir := t.TempDir()
@@ -60,9 +87,23 @@ func TestLoadConfigFile(t *testing.T) {
}
}
+// TestConfigDefaultWalk exercises discovery itself — the cwd walk AND the
+// global fallback — so it cannot use useIsolatedConfig's CARLOS_CONFIG
+// override, which disables discovery entirely. Instead it owns one temp
+// tree for the whole test: HOME lives at the tree's root, and the working
+// directory is a descendant of that same root, so both the cwd walk and
+// the global fallback resolve inside the tree this test controls rather
+// than a developer's real ~/.carlos/config.
func TestConfigDefaultWalk(t *testing.T) {
- useTempCredentials(t)
- home := os.Getenv("HOME")
+ home := t.TempDir()
+ t.Setenv("HOME", home)
+ t.Setenv("CARLOS_CONSOLE", "")
+ t.Setenv("CARLOS_CREDENTIALS", filepath.Join(home, "credentials"))
+ root := filepath.Join(home, "project")
+ if err := os.MkdirAll(root, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ t.Chdir(root)
// Nothing anywhere: no default, no error.
if c, src, err := configDefault(); err != nil || c != "" || src != "" {
@@ -78,7 +119,6 @@ func TestConfigDefaultWalk(t *testing.T) {
// A project config anywhere up the cwd walk beats the global, and the
// NEAREST project config beats a farther one.
- root := t.TempDir()
writeConfig(t, root, `{"console": "https://outer.example"}`)
nested := filepath.Join(root, "cmd", "deep")
if err := os.MkdirAll(nested, 0o755); err != nil {