cmdtest

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package cmdtest is the test harness for the triage plugin's CLI.

It provides three layers:

  • Run / RunWithStdin — invoke a *cli.Command in-process with captured stdin/stdout/stderr. No subprocess, no built binary required.
  • Assert* — vocabulary for asserting on the captured bytes. Helpers fail with t.Fatal on mismatch and include the captured streams in their failure messages, so debugging a red test does not require re-running with extra logging.
  • Isolate — file-system + env isolation for tests that touch the data directory, slash-command target directory, or git context.

Phase 6 (pivot-to-ai-as-code) scope decision

Pre-Phase-6 the harness drove the in-process triage cmd tree that the `tai` binary embedded directly. Post-Phase-6 the same cmd tree is the entry point for the standalone `triage` plugin binary that the host invokes via subprocess.

The harness still drives the cmd tree IN-PROCESS, not via subprocess exec. Two reasons:

  1. The host-side subprocess wiring (env-var injection, stdio passthrough, exit-code propagation) is verified by core's plugin-host tests (TC-PLG-002, TC-PLG-005). Those tests run a POSIX shell-stub plugin and assert the host fulfils the contract; the plugin-side consumption (taiplugin.Load → storage path → verb dispatch) is exercised by this harness's in-process tests. Re-testing both ends in the same suite would duplicate coverage AND add a build-binary-then-exec dependency to every TC-IMP/TRG/INST test.
  2. The verb-dispatch logic exercised by these tests is the same regardless of whether the cmd tree is reached in-process or via subprocess. The harness pins what only the triage cmd tree owns; the subprocess transport is the host's responsibility.

A future revision MAY introduce an `ExecRoot` variant that builds the triage binary and exec's it for end-to-end coverage of both transports. Today's TC-IDs do not require it.

Conventions:

  • Every test that maps to a BDD case in plugins/triage/test-cases.md (or core/test-cases.md for CMD-001/002/008 and the ERR cases) names the TC-ID in its function name AND its t.Run subtest descriptions, so a failure surfaces the trace back to the spec. Example:

    func TestVersion_TCCMD001_prints_version_string(t *testing.T) { ... }

    The pattern is Test<Cmd>_<TCID>_<short_description>, where <TCID> is the TC-ID with hyphens removed (TC-CMD-001 → TCCMD001).

  • Helpers t.Helper() themselves so failure line numbers point at the caller, not at this file.

  • The assert vocabulary is intentionally narrow. If you reach for a helper that does not exist, prefer adding it here over inlining the check — the harness is the contract every test obeys.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertError

func AssertError(t *testing.T, r Result)

AssertError fails the test when r.Err is nil. Use AssertErrorFooter when the test is specifically about the foundation's error contract; this helper only verifies that *some* error occurred.

func AssertErrorFooter

func AssertErrorFooter(t *testing.T, r Result, wantCode string, wantExitCode int)

AssertErrorFooter asserts that the LAST line of stderr is the foundation's error-contract footer and that:

  • the embedded code equals wantCode, and
  • (when wantExitCode >= 0) the embedded numeric exit code equals wantExitCode. Pass -1 to skip the numeric check.

The spec requires the footer to be the literal last line written to stderr (trailing newline allowed); this helper enforces that — content after the matched footer fails the assertion.

This is the primary error-path assertion every TC-ERR test will use once add-tai-foundation lands.

func AssertExitCode

func AssertExitCode(t *testing.T, r Result, want int)

AssertExitCode fails the test when r.ExitCode != want.

func AssertNoError

func AssertNoError(t *testing.T, r Result)

AssertNoError fails the test when r.Err is non-nil.

func AssertStderrContains

func AssertStderrContains(t *testing.T, r Result, substr string)

AssertStderrContains fails the test when substr does not appear in stderr.

func AssertStderrEmpty

func AssertStderrEmpty(t *testing.T, r Result)

AssertStderrEmpty fails the test when stderr contains any bytes.

func AssertStdoutContains

func AssertStdoutContains(t *testing.T, r Result, substr string)

AssertStdoutContains fails the test when substr does not appear in stdout.

func AssertStdoutEquals

func AssertStdoutEquals(t *testing.T, r Result, want string)

AssertStdoutEquals fails when stdout differs from want byte-for-byte. Use sparingly — substring/regex matching is more forgiving and friendlier to revising output formatting.

func AssertStdoutMatches

func AssertStdoutMatches(t *testing.T, r Result, pattern string)

AssertStdoutMatches fails the test when stdout does not match the regex.

func Chdir

func Chdir(t *testing.T, dir string)

Chdir changes the process's working directory to dir for the test's lifetime, restoring the original cwd via t.Cleanup.

The process cwd is global state — this helper is UNSAFE under t.Parallel(). The guard above panics if a second test attempts to Chdir while another test's Chdir scope is still active.

Types

type Isolated

type Isolated struct {
	// Root is the test's private tmp directory. Use it to write fixtures
	// or inspect what tai produced.
	Root string
	// Home is the private HOME directory; everything under Root/home.
	Home string
	// DataDir is the resolved TAI_DATA_DIR; everything under Root/data.
	DataDir string
}

Isolated is a fully-isolated test environment: a tmp directory rooted at t.TempDir() with a private HOME (and USERPROFILE on Windows), a private TAI_DATA_DIR, and any other env vars callers explicitly set.

All environment changes are torn down via t.Cleanup, so tests do not leak state into each other when run with -parallel.

func Isolate

func Isolate(t *testing.T) *Isolated

Isolate creates an Isolated test environment and wires the relevant env vars for the duration of the test:

  • HOME is set on every platform.
  • USERPROFILE is set on Windows (where it is the conventional home directory pointer).
  • TAI_DATA_DIR is set to a per-test data directory.
  • XDG_DATA_HOME is unset (not merely emptied) so the foundation's data-directory resolver does not see a host-machine value.

Tests that want to override additional env vars can call t.Setenv after Isolate returns; those overrides are also torn down on t.Cleanup.

Note: this does NOT change the process's working directory. Tests that need a specific cwd (e.g. to exercise repo detection) should call Chdir.

func (*Isolated) WriteFile

func (i *Isolated) WriteFile(t *testing.T, path, content string)

WriteFile is a convenience for fixture creation: writes content to path (relative to Root or absolute), creating parent directories as needed, and fails the test on error.

type Result

type Result struct {
	// Stdout is the bytes written to the command's Writer.
	Stdout string
	// Stderr is the bytes written to the command's ErrWriter.
	Stderr string
	// ExitCode is the OS exit code the binary would have produced. It is
	// derived from the error returned by cli.Command.Run: nil → 0; any
	// non-nil error that implements cli.ExitCoder → that code; any other
	// non-nil error → 1 (the default core/cmd/tai/main.go produces).
	ExitCode int
	// Err is the raw error returned by cli.Command.Run. Tests that care
	// about the user-observable error contract should prefer
	// AssertErrorFooter and AssertExitCode (which encode the spec); Err
	// is exposed for assertions that need the underlying Go value.
	Err error
}

Result captures everything tests need to assert against after running the tai CLI.

func Run

func Run(t *testing.T, cmd *cli.Command, argv ...string) Result

Run invokes cmd with the given argv (NOT including the executable name — the harness prepends "triage" for you, matching the post-Phase-6 binary name set on NewRoot's Name field). Stdin is empty; stdout and stderr are captured into the returned Result.

The cmd's Writer / ErrWriter / Reader fields are overwritten by this call; callers should pass a freshly-built command (typically cmd.NewRoot()).

func RunWithStdin

func RunWithStdin(t *testing.T, cmd *cli.Command, stdin string, argv ...string) Result

RunWithStdin is like Run but pipes stdin as the command's standard input.

Execution flows through pkg/cliexec so panic recovery matches the production binary; stderr is post-processed by pkg/cliout.WriteError on a non-nil error so tests observe the same foundation-template footer the user does.

urfave/cli does NOT propagate Writer/ErrWriter/Reader to subcommands; the harness walks the tree and sets all three on every descendant so a subcommand's c.Writer.Write reaches the captured buffer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL