Documentation
¶
Overview ¶
Package testutil holds test-only helpers shared across the CLI test surface. These helpers are intentionally NOT under a `_test.go` file because they need to be imported by tests in other packages — _test.go files are only visible to their own package's tests.
Production code must not import this package. The drift policy `PolicyTestUtilNotImportedFromProduction` under internal/policies/ (M-0118/AC-7) is the chokepoint.
Index ¶
- Constants
- func AiwfBinary(t *testing.T) string
- func BrokenGitIdentity(t *testing.T)
- func BuildBinary(t *testing.T, tmp string, extraArgs ...string) string
- func CaptureRun(t *testing.T, fn func() int) (rc int, stdout, stderr string)
- func CaptureStderr(t *testing.T, fn func()) []byte
- func CaptureStdout(t *testing.T, fn func()) []byte
- func ExitedWithCode(err error, code int) bool
- func ExtractRow(haystack, prefix string) string
- func HoldRepoLock(t *testing.T, root string) (release func())
- func MustExec(t *testing.T, workdir, name string, args ...string)
- func ReadFileT(t *testing.T, path string) string
- func RunBin(t *testing.T, workdir, extraPath string, env []string, args ...string) (string, error)
- func RunBinStdin(t *testing.T, workdir, extraPath string, stdin io.Reader, args ...string) (string, error)
- func RunBinary(bin string, args ...string) (string, error)
- func RunBinaryAt(workdir, bin string, args ...string) (string, error)
- func RunGit(workdir string, args ...string) (string, error)
- func RunGitWithExtraEnv(workdir string, extraEnv []string, args ...string) (string, error)
- func SetupGitRepoWithUpstream(t *testing.T, email string) string
- func SkipIfShortOrUnsupported(t *testing.T)
- func WriteMalformedEntity(t *testing.T, root, relPath string)
Constants ¶
const InvalidFormat = "xml"
InvalidFormat is a --format value no verb recognizes. It exercises only ONE of the two `--format` verb shapes in this codebase:
- Read-style verbs (history, list, schema, show, template, check, contract verify, status, render) validate a hardcoded text/json string themselves, first thing in Run(...), before touching root or tree — passing InvalidFormat there returns cliutil.ExitUsage. See internal/cli/check/check.go's format guard, pinned by check_test.go's TestRun_BadFormat.
- Mutating verbs going through cliutil.OutputFormat do NOT validate --format at all: anything other than "json" silently degrades to text via OutputFormat.JSON(). InvalidFormat does NOT trigger a failure there — there is no bad-format branch to cover on that verb shape.
Variables ¶
This section is empty.
Functions ¶
func AiwfBinary ¶
AiwfBinary returns the absolute path to a built `aiwf` binary, compiling on the first call. The binary lives in a per-process temp dir so concurrent `go test` runs don't fight over it.
macOS Sonoma 14.8.x has a syspolicyd crash on unsigned Mach-O headers (G-0128). Ad-hoc-signing the binary post-build routes around it.
func BrokenGitIdentity ¶ added in v0.27.0
BrokenGitIdentity isolates the current test's process environment so `git config user.email` cannot yield a usable identity, guaranteeing cliutil.ResolveActor("", root) (and any verb's Run that resolves an actor with an empty --actor) fails via the "no actor" error path.
It points HOME and XDG_CONFIG_HOME at a fresh, empty-of-gitconfig temp directory and sets GIT_CONFIG_NOSYSTEM=1 to keep the host's real git identity from leaking in, then writes a `.gitconfig` whose user.email has no "@" separator — the same degenerate-but-legal git state pinned by actor_test.go's TestResolveActor_MalformedGitEmail. All three env vars are set via t.Setenv, so isolation reverts automatically at test cleanup; no manual teardown is needed.
func BuildBinary ¶
BuildBinary compiles ./cmd/aiwf into tmp/aiwf with the given extra `go build` args (typically `-ldflags=…`) and returns the path. Use when AiwfBinary's cached default-build won't do (e.g., the ldflags-stamped-Version test path needs a per-invocation build).
Builds happen from the repo root so the relative package path resolves regardless of which package the test runs in. The same G-0128 codesigning fix applies as AiwfBinary.
func CaptureRun ¶
CaptureRun redirects os.Stdout and os.Stderr around fn, returning the exit code and both captured streams. Used by tests that need both — typically `aiwf upgrade` where success/failure surface across both streams.
func CaptureStderr ¶
CaptureStderr is the os.Stderr sibling of CaptureStdout. Used by tests that assert on verb-level error messages — verbs write usage errors to stderr while findings/data go to stdout, so separate capture is needed.
func CaptureStdout ¶
CaptureStdout replaces os.Stdout with a pipe for the duration of fn and returns whatever was written. Used by tests that drive verbs in-process (the verbs write to os.Stdout directly so the dispatcher tests need this to assert against output).
Tests calling CaptureStdout cannot run under t.Parallel — os.Stdout is a process-level fd shared by every goroutine. The cmd/aiwf and internal/cli/integration test packages' setup_test.go skip-lists document which tests stay serial because they call CaptureStdout.
Why this lives in a shared testutil package (M-0118/AC-7): the pre-M-0118 codebase had two parallel copies of this function — at cmd/aiwf/helpers_test.go and internal/cli/initcmd/helpers_test.go — because _test.go files cannot cross package boundaries. Sharing the implementation here is the only way to keep one canonical definition.
func ExitedWithCode ¶
ExitedWithCode reports whether err is an *exec.ExitError with the given exit code. Used to tolerate non-zero exit codes that are documented contract (e.g., `aiwf doctor` returns 1 ExitFindings when aiwf.yaml is missing — that's expected, not a test failure).
func ExtractRow ¶
ExtractRow returns the first line of haystack whose prefix (after trimming leading whitespace) matches prefix. Empty if not found. Used by tests that parse line-oriented output like `aiwf doctor`.
func HoldRepoLock ¶ added in v0.27.0
HoldRepoLock takes root's repo lock (via repolock.Acquire, the same mechanism cliutil.AcquireRepoLock wraps) and returns a release func the caller must invoke to free it. Call this before invoking a verb that itself calls cliutil.AcquireRepoLock against the same root, to force the busy/contention branch — a second Acquire against a held lock always fails with repolock.ErrBusy.
The "other error" branch of cliutil.AcquireRepoLock (a root whose lockfile can't even be opened) doesn't need a fixture: pass a non-existent temp directory as root, e.g. filepath.Join(t.TempDir(), "does-not-exist") — the one-line pattern already used at internal/cli/renamearea/renamearea_test.go and internal/cli/setarea/setarea_test.go.
func MustExec ¶
MustExec runs name with args in workdir; failure t.Fatals. The caller's location is reported via t.Helper().
func ReadFileT ¶
ReadFileT reads the file at path and returns its contents as a string; t.Fatals on error. Trivial wrapper around os.ReadFile that keeps test bodies free of repeated error-check boilerplate.
func RunBin ¶
RunBin runs the built binary with args in workdir, prepending extraPath onto PATH. Returns combined stdout+stderr and exit error. The binary is built once per test process via AiwfBinary's sync.Once.
func RunBinStdin ¶
func RunBinStdin(t *testing.T, workdir, extraPath string, stdin io.Reader, args ...string) (string, error)
RunBinStdin is the stdin-bearing variant of RunBin: pipes the supplied reader to the binary's stdin so tests can exercise `--body-file -` and similar shorthands. Otherwise identical to RunBin (env, working dir, combined stdout+stderr).
func RunBinary ¶
RunBinary invokes bin with args and returns combined stdout+stderr. Combined output is what a user sees, so the assertions read the same bytes the user would. Sibling of RunBin but doesn't add env vars — the caller controls the environment.
func RunBinaryAt ¶
RunBinaryAt is the workdir-bearing variant of RunBinary. Used by tests where the binary needs to run inside a specific repo (e.g. after init).
func RunGit ¶
RunGit invokes git in workdir and returns combined output. The command runs with a fixed deterministic identity (GIT_AUTHOR_* env vars) so tests don't depend on the developer's git config.
func RunGitWithExtraEnv ¶ added in v0.12.0
RunGitWithExtraEnv is RunGit plus a slice of additional env entries appended AFTER the fixed identity defaults. Since exec processes env entries last-wins for duplicate keys, an extraEnv entry like "GIT_COMMITTER_EMAIL=human@example.com" overrides the default test identity for THIS subprocess only.
Used by integration tests that need to vary committer identity per-call (e.g., M-0159/AC-6 cherry-pick scenarios that need committer != author to exercise the rule's gap-detection suppression contract). The default RunGit's "-c user.email=X" override would NOT achieve this — git evaluates GIT_*_EMAIL env vars with higher precedence than -c config overrides, so the env-var path is the only way to actually flip the committer identity inside a subprocess whose parent sets GIT_COMMITTER_EMAIL.
func SetupGitRepoWithUpstream ¶
SetupGitRepoWithUpstream creates a fresh git repo with a bare origin set up as its upstream, then pushes an empty seed commit so the working repo has a tracked branch. Returns the absolute path of the working repo. Tests that exercise provenance audit scopes (`@{u}..HEAD` ranges) need an upstream configured.
func SkipIfShortOrUnsupported ¶
SkipIfShortOrUnsupported gates binary integration tests: requires `go` on PATH, skipped under `-short`, skipped on Windows (aiwf is unix-only).
func WriteMalformedEntity ¶ added in v0.27.0
WriteMalformedEntity writes a file at root/relPath containing frontmatter with an unclosed YAML string literal — the same malformed-YAML shape internal/tree/tree_test.go's TestLoad_ParseErrorBecomesLoadError pins — guaranteeing tree.Load(ctx, root) reports exactly one LoadError for relPath while leaving the rest of the tree loaded normally (tree.Load treats a per-file parse error as non-fatal).
relPath must land in one of entity.PathKind's recognized shapes (e.g. "work/epics/E-0099-broken/epic.md" or "work/gaps/G-0099-broken.md") so the loader's directory walk classifies it as an entity file in the first place; anywhere else it is treated as a stray and silently skipped, producing no LoadError.
Types ¶
This section is empty.