Documentation
¶
Overview ¶
Package testutil holds the helpers the Brigade test tree shares. It is imported only from tests and from the test-only entrypoints of the cmd/ packages; nothing under it is linked into the shipped binary.
P1-1 scope. The plan's full list (7.1: fakesock, fakeregistry, fakeadapter, dotenv, …) belongs to the phases that need it. What is here is what P1-1 needs and what P1-1 exercises: Env and Dirs to build a child environment explicitly, Build and BuildStamped to compile a repository binary the way the release does, RepoRoot, and RunID. Anything added later should keep the same rule: a helper with no caller and no test is not a helper, it is a claim.
Everything here is safe to call from a test that has already called t.Parallel: the package holds no mutable state, and every path it hands out is derived from the caller's own t.TempDir.
Index ¶
- Constants
- func Build(tb testing.TB, pkg string) string
- func BuildStamped(tb testing.TB, pkg, version string) string
- func Env(tb testing.TB, extra ...string) []string
- func Eventually(tb testing.TB, timeout, interval time.Duration, cond func() bool)
- func NewSleeper(tb testing.TB) int
- func RepoRoot(tb testing.TB) string
- func RequireSupabaseDB(tb testing.TB) string
- func RunID() string
- type Dirs
- type SupabaseEnv
Constants ¶
const EnvTestFile = ".env.test"
EnvTestFile is the file `make supabase-env` writes from the running stack, gitignored, at the repository root.
const LiveTestVar = "BRIGADE_TEST_LIVE"
LiveTestVar is the explicit opt-in every live test in the tree waits on. A stack that answers is NOT consent: `make supabase-start supabase-env` leaves a listening stack and a .env.test behind for good, so before this gate existed every live test ran inside `make test` on any machine that had run those two recipes once — measured 2026-09-04 on this repository's dev machine, `go test -run TestIntegration ./internal/adapters/supabase/` was 32 PASS / 5 SKIP / 0 FAIL in 55.883 s with no BRIGADE_* variable set, and under `make test`'s `-race -shuffle=on` whole-tree load one of them (TestIntegrationAdversarialBroadcastPayloadIsIdsOnly) missed its 10 s broadcast window and then sat on an untimed websocket read until the Realtime server closed the un-heartbeated socket 60 s later, failing the commit gate with `read: failed to get reader: failed to read frame header: EOF`. The reads are bounded now, and this variable is why a developer's `make test` no longer reaches them at all.
`make test-integration` sets it (and BRIGADE_TEST_DOCKER beside it); CI's `supabase` job gets it from that same recipe. Nothing else sets it, which is what makes CLAUDE.md's "`make test` is Docker-free and stack-free" true on a developer's machine as well as on a bare runner.
const ModulePath = "github.com/appshapes/brigade"
ModulePath is this repository's Go module path. RepoRoot uses it to tell this module's go.mod from any other one it walks past.
const SupabaseDBURLVar = "SUPABASE_DB_URL"
SupabaseDBURLVar is the name `make supabase-env` writes the DSN under.
const VersionLDFlagTarget = ModulePath + "/internal/buildinfo.Version"
VersionLDFlagTarget is the linker symbol the release stamps the version into. It is declared here so that a test asserting the stamped version and the Makefile's -ldflags cannot drift apart silently: if the symbol moves, TestBuiltBinaryReportsTheStampedVersion in cmd/brigade goes red.
Variables ¶
This section is empty.
Functions ¶
func Build ¶
Build compiles a package of this repository into a directory belonging to the test and returns the absolute path of the binary.
pkg is a package pattern relative to the repository root, for example "./cmd/brigade". The build uses the release flags — -trimpath and CGO_ENABLED=0 — so that the artefact under test is the artefact that ships (plan 9.4).
-race is never passed: the race detector needs cgo and CGO_ENABLED=0 turns it off, which is exactly the combination plan 7.3 forbids. The test process itself is race-instrumented by `go test -race`; the child it builds is not.
The result is not cached across calls. A cache would need a directory outliving any single test, and testutil has no process-wide teardown hook to remove one; `go build` caches the link step itself, so a repeat build of an unchanged package is a copy.
func BuildStamped ¶
BuildStamped is Build with the version stamped the way the release ldflags stamp it. An empty version stamps nothing, which is the `go install` shape: buildinfo then falls back to the module version recorded in the binary's build info.
func Env ¶
Env builds the environment for a child process from scratch and returns it in os.Environ form, ready for exec.Cmd.Env.
It does NOT copy the test process's environment. That is the point: the developer running the suite has a real CLAUDE_CONFIG_DIR (never ~/.claude on this project's machines) and real BRIGADE_* values, and a child that inherited them would read and write the developer's own state. Exactly one variable is carried over — PATH, without which nothing can be executed by name — and it is copied explicitly rather than by inheritance so that the exception is visible here and nowhere else.
TMPDIR is deliberately not redirected: unix sockets have to stay under /tmp, because macOS caps sun_path at 103 bytes and a t.TempDir path is already about 91 of them (plan 7.3).
extra is appended last, so a caller can override any variable above or add its own. Env creates the directories and fails the test if it cannot.
func Eventually ¶
Eventually polls cond every interval until it returns true, and fails the test through tb.Fatalf when timeout elapses first (plan 7.3: no sleeps in assertions; a poll with a deadline is the replacement).
The timeout is a HANG CATCHER, not a performance bound. Choose it so a loaded `-race` run on a slow CI runner still passes with room to spare — a 5 s bound on a 300 ms operation tripped at 5.56 s on 2026-09-02 — and keep the interval short so a passing test does not wait out a whole interval it did not need.
The clock is the standard one, so under testing/synctest the polling loop advances the bubble's fake time and a timeout costs no wall time. cond is called at least once, before any wait.
func NewSleeper ¶
NewSleeper starts `sleep 300` as a stand-in for a live Claude Code process and returns its pid (plan 9.5; the fake CLAUDE_PID of the watcher lifecycle tests).
The child is started with an explicit environment from Env — nothing of the test process's environment reaches it — and it is REAPED: a goroutine calls Wait the moment it starts, so that when the test (or the code under test) SIGTERMs the pid, kill(pid, 0) turns to ESRCH promptly instead of succeeding on a zombie for as long as nobody waits (E0-5 item 1; the same trap the procutil package exists for). A test that wants an UNREAPED corpse to prove the zombie path must start its own child and deliberately not wait — this helper is the reaped kind.
Cleanup sends SIGTERM and waits for the reap, escalating to SIGKILL if the child ignores it, so nothing survives the test that started it. A sleeper the test has already terminated is fine: the signal to a finished process is a no-op.
func RepoRoot ¶
RepoRoot returns the absolute path of the repository root: the directory holding the go.mod that declares ModulePath.
It walks up from the working directory rather than from the compiled-in path of this source file, so it keeps working when the tree is moved and fails loudly rather than silently pointing at a stale location.
func RequireSupabaseDB ¶
RequireSupabaseDB returns the local stack's Postgres DSN — the `postgres` superuser connection `make supabase-env` writes as SUPABASE_DB_URL. Without LiveTestVar it skips like RequireSupabase; with it, a missing DSN fails for the same reason a missing stack does.
It exists for the fixtures of plan 9.4 that nothing on the wire can build: backdating `last_seen_at` or a message's `created_at`, running brigade.gc_expired(), and reading auth.users to confirm what a principal is. That is the ONLY reason the test tree talks to Postgres, and only from a _test.go file; everything else goes through the adapter. The DSN carries the local stack's postgres password, so it is returned, never logged: no caller may put it in a t.Log, a failure message or a committed file.
The opt-in gate and the stack health check both run first (inside RequireSupabase), so a run without LiveTestVar costs nothing and a stale .env.test naming a stack that is not up fails on the two-second health probe rather than hanging on a dial.
func RunID ¶
func RunID() string
RunID returns an identifier unique to one test run, in the form 20260831T142530Z-1f4b9c02 (plan 9.4).
Integration tests put it in every name they create so that two runs — or two developers sharing one stack — never collide and no reset is needed between them. It is sortable by time on purpose: a leftover row says when it was made.
Types ¶
type Dirs ¶
type Dirs struct {
// Root is the directory the others live under.
Root string
// Home backs $HOME.
Home string
// ClaudeConfig backs $CLAUDE_CONFIG_DIR. Never hardcode ~/.claude
// anywhere: this is the only config dir a test may touch.
ClaudeConfig string
// PluginRoot backs $CLAUDE_PLUGIN_ROOT.
PluginRoot string
// XDGConfig, XDGState and XDGCache back the XDG base directories the
// adapter kit resolves its files from.
XDGConfig string
XDGState string
XDGCache string
// BrigadeConfig and BrigadeState back $BRIGADE_CONFIG_DIR and
// $BRIGADE_STATE_DIR.
BrigadeConfig string
BrigadeState string
// FSRoot backs $BRIGADE_FS_ROOT, the filesystem adapter's store.
FSRoot string
}
Dirs are the directories a child process started by a test is confined to. Every path Brigade or Claude Code would otherwise take from the developer's own account has one here instead, so a test — or a detached watcher a test leaked — cannot reach the real ~/.claude* tree, the real XDG directories or the real home.
The zero Dirs is not usable; build one with NewDirs.
func NewDirs ¶
NewDirs lays out a Dirs under root. It creates nothing; call Dirs.Mkdir for that.
func (Dirs) Mkdir ¶
Mkdir creates every directory in the layout, 0700 as the plan's file modes require (T7, T14).
func (Dirs) Vars ¶
Vars returns the directory half of a child environment, in os.Environ form.
$HOME is deliberately absent. Env sets it to Dirs.Home; testscript sets its own HOME=/no-home so that any ~ resolution inside a script fails loudly, and a caller building script variables must not overwrite that.
type SupabaseEnv ¶
SupabaseEnv is what an integration test needs of the local stack (plan 9.4): the API url and the publishable key. Never the secret key, the service-role key or the JWT secret — those stay out of the adapter and out of every test that ships (7.7).
func RequireSupabase ¶
func RequireSupabase(tb testing.TB) SupabaseEnv
RequireSupabase returns the local stack's url and publishable key, or skips the test. Every live test is opt-in behind LiveTestVar: without it this skips before it looks at anything — no .env.test read, no health probe — so `make test` is Docker-free AND stack-free even on a machine whose local stack is up.
With the opt-in set, the values come from SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY in the process environment (`make test-integration` sources .env.test first), else from .env.test at the repository root. From here on a missing pair or a stack whose auth service does not answer /auth/v1/health within two seconds is a FAILURE, not a skip: the opt-in says a stack is expected, and a skip here would let `make test-integration` and CI's `supabase` job exit 0 having executed nothing (measured 2026-09-04 before this rule: with the variable set and no stack, 4/4 SKIP, exit 0).
The keys are read here and nowhere else in the test tree. Nothing under testutil is linked into the shipped binary.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package fakeadapter is a scripted BAP/1 adapter used as a test fixture (plan 9.5; brief section 2.10).
|
Package fakeadapter is a scripted BAP/1 adapter used as a test fixture (plan 9.5; brief section 2.10). |
|
Package fakeregistry is the test double for Claude Code's session registry (plan 9.5, A.3): an fstest.MapFS holding <pid>.json entries in the observed shape, served through the fs.FS the registry reader takes, wrapped in a recorder that FAILS THE TEST when any name ending in .key is opened and records every name that was.
|
Package fakeregistry is the test double for Claude Code's session registry (plan 9.5, A.3): an fstest.MapFS holding <pid>.json entries in the observed shape, served through the fs.FS the registry reader takes, wrapped in a recorder that FAILS THE TEST when any name ending in .key is opened and records every name that was. |
|
Package fakesock is the fake inbox socket of plan 9.5: a unix-domain server standing in for a Claude Code session's `CLAUDE_CODE_MESSAGING_SOCKET`, so the socket poster (6.7, U-17, U-19, U-20) and later the watcher can be tested against a real socket with no Claude process anywhere near.
|
Package fakesock is the fake inbox socket of plan 9.5: a unix-domain server standing in for a Claude Code session's `CLAUDE_CODE_MESSAGING_SOCKET`, so the socket poster (6.7, U-17, U-19, U-20) and later the watcher can be tested against a real socket with no Claude process anywhere near. |
|
Package tscmd holds the custom testscript commands the Brigade scripts use.
|
Package tscmd holds the custom testscript commands the Brigade scripts use. |