carlos / platform Public

The root-package test binary exits early on any checkout under $HOME — and that is what hid a week of red CI open

The root-package test suite does not run to completion on a developer machine whose checkout lives under $HOME, and the way it fails is the interesting part: it does not report failures and carry on, it exits the test binary partway through. Everything after the aborting test is simply never run, and nothing says so.

$ go test . -count=1
--- FAIL: TestAccountsListSignedIn
    not logged in to https://console.carloku.com (the default from /home/paulca/.carlos/config)
--- FAIL: TestAccountsListOldConsole
--- FAIL: TestResolveModeAPI
--- FAIL: TestResolveModeNothing
--- FAIL: TestResolveModeExpiredCredentialOnly
2026/09/04 23:09:20 not logged in to https://console.carloku.com (the default from /home/paulca/.carlos/config)
FAIL	github.com/carlosframework/platform	0.203s

0.2 seconds. The same package takes 147 seconds in CI. A log.Fatal on the same root cause takes the process down, so the run you get locally is not a shorter version of the CI run — it is a different run that stops before it starts.

One cause, not five

Every line above is the same fact: walkConfig (config.go:62) climbs from the working directory to the filesystem root looking for a .carlos/config, and on this machine the climb passes through /home/paulca and finds the developer's real global config. The tests then resolve a live console they have no credential for.

It is not five flaky tests and it is not a property of the tests. It is one lookup with no boundary, and it bites any checkout under a home directory that has ever run carlos auth default — which is every developer machine.

TestConfigDefaultWalk fails the same way, at its very first assertion ("nothing anywhere: no default, no error"), and is not in the five, because the binary is already gone by then. Nobody knows how many others are behind it.

Why this is filed rather than fixed in passing

Because it has already cost something. The branch carried a note — repeated into a second plan's global constraints — saying five root-package tests fail on this machine for an environmental reason, not a regression. That note is true. It is also exactly the camouflage a real failure needs.

TestStoreVerbDispatchAndRefusals began failing at df947ec, when carlos store grew a scan sub-verb and its usage assertion did not. CI was red on every push for a week. Nobody read it, because the package was known to be red and the failure count was the only thing being checked.

The defect is not the stale assertion. The defect is that this repo has a package where red is not news.

What a fix has to decide

The narrow part has a precedent to copy exactly. credentialsPath (auth.go:134) already honours CARLOS_CREDENTIALS, documented as existing "for tests and for anyone keeping their credential somewhere other than $HOME". A symmetric CARLOS_CONFIG — name the file, skip discovery entirely — fixes every test that goes through carlosTestEnv or useTempCredentials, which is all five above.

The part that needs a decision is TestConfigDefaultWalk, which exercises discovery and therefore cannot use an override that disables it. That one needs the walk itself to have a boundary, and there are two candidates:

  • stop at $HOME — above your home directory there are no project roots, only other people's. Simple, and the global fallback already keys off the same directory.
  • stop at the repository root (a directory holding .git) — which is what config.go's own header already describes: ".carlos/config at a project root — committed to the repo". By that reading, treating an ancestor above the repo root as a project pin is a category error, and the current behaviour is a bug in its own right, not just a test nuisance.

The second is the better description of the intent and the larger behaviour change, so it wants a moment rather than a drive-by: docs/superpowers/specs/2026-08-04-console-default-resolution-design.md owns this, and "a resolved default is authoritative — resolution never falls past it" is a strong enough rule that where it starts applying matters.

Suggested shape

  1. CARLOS_CONFIG, mirroring CARLOS_CREDENTIALS. Set it in carlosTestEnv (e2e_test.go:81) and in useTempCredentials.
  2. Pick a walk boundary, in the spec, and give TestConfigDefaultWalk a tree it owns.
  3. Then zero known-failing tests locally, and a note in AGENTS.md that a known-failing test is a defect in its own right — because the count is what got read instead of the names.

Step 3 is the one that pays for the other two.

Triage, 2026-09-05. Fixed, on branch test-config-isolation (2b715f1), pushed. CI is running as I write this, so read that before merging rather than my word.

Reproduced first, exactly as reported: go test . -count=1 aborted via log.Fatal in 0.2s with the same five failures, because this machine's /home/paulca/.carlos/config names https://console.carloku.com and both walkConfig and configDefault reach it — the cwd climb passes through $HOME, and the global fallback is the same file by another route.

After: 130–151s across four runs, 538 tests, 0 failures, exit 0 every time. That duration is the evidence the binary now runs to completion, which was your actual complaint — the five names were the symptom.

What shipped

Your step 1, as specified. CARLOS_CONFIG mirrors CARLOS_CREDENTIALS: when set, it names the one file consulted and skips discovery entirely — no cwd walk, no global fallback — in both walkConfig and configDefault. Unset, both are byte-for-byte what they were.

Plus useIsolatedConfig(t), the in-process counterpart to carlosTestEnv. That was the gap worth naming: carlosTestEnv plus cmd.Dir already protected every SUBPROCESS test, which is why this only ever bit the handful of tests that call accountsListAPI and resolveMode in-process and therefore inherit the real cwd and the real $HOME.

TestConfigDefaultWalk got a temp tree it owns — HOME at the root, cwd a descendant — since it exercises discovery and cannot use an override that disables it.

Your step 2 is deliberately NOT done

The walk boundary is untouched. $HOME vs repository root is a real behaviour change that docs/superpowers/specs/2026-08-04-console-default-resolution-design.md owns, and you were right that it wants a moment rather than a drive-by. It is still open and still yours. Nothing in this change forecloses either answer.

One correction to the report

TestStoreVerbDispatchAndRefusals is not stale on main. The carlos store scan sub-verb that its usage assertion does not know about lives on an unmerged feature branch, not on main, so there was nothing to fix. The week of red CI you describe is real, but it belongs to that branch rather than to main — worth knowing, because it slightly changes the story without weakening the argument. The argument is what step 3 was for, and step 3 is in: AGENTS.md now carries the rule that a known-failing test is a defect in its own right, because the COUNT is what gets read instead of the names.

One thing found on the way, filed rather than fixed

This session's $TMPDIR sits under $HOME, so t.TempDir() isolation leaks back into the real ~/.carlos/config for tests outside these six — the pre-existing useTempCredentials has the same hole wherever TMPDIR is nested that way. Verified as a property of the environment and not the code (identical on unmodified main). Roughly 100+ call sites; converting them is a separate pass and mixing it in would have blurred a reviewable diff. Worth its own thread.

go build, go vet, gofmt -l . all clean. internal/activator (5) and internal/edge (1) fail identically on unmodified main in this sandbox — AF_UNIX restrictions, not this change.

Not merged, and no PR opened: gh only speaks to GitHub, and the local git credential is scoped to git-over-HTTP, so the web API refused it. Someone with a session needs to open test-config-isolation → main, or it can go through amadan branch merge once CI reports.