Files
msd-core/docs/TESTING-SUITES.md
Tom Boucher 9dd2c87c6c ci: supersede #367 with combined tiered PR pipeline redesign (#369)
* ci: streamline PR pipeline gates

* test: annotate ci-test-scope stderr assertions for lint rule

---------

Co-authored-by: Colin <colin@solvely.net>
2026-05-26 19:11:58 -04:00

4.8 KiB

Testing Suites

This project's tests/ directory uses filename suffix markers to group tests into named suites. The harness scripts/run-tests.cjs filters by suite when given --suite <name>. Without a flag it runs every *.test.cjs file (the historical default — unchanged).

Tracked by issue #3597.

Suites

Suite Filename pattern What goes here
unit *.test.cjs (no other marker) Default fast lane. Pure logic, no network, no external processes beyond gsd-tools. Most tests live here.
integration *.integration.test.cjs Cross-module flows: full installer end-to-end, multi-tool orchestration, anything that crosses two or more bin entry points.
install *.install.test.cjs Tests that perform a real install/uninstall against a sandbox project. Slower; PR CI skips these on PRs and runs them on main push only.
security *.security.test.cjs Adversarial input, prompt-injection guards, fixture-driven hostile-payload sweeps.
slow *.slow.test.cjs Anything that routinely takes >5s wall-clock or holds significant memory.
all (any) Explicit alias for "no filter". Equivalent to running with no --suite flag.

How to place a new test

  1. Pick the most specific bucket above.
  2. Name the file with the matching suffix: tests/<feature>.<suite>.test.cjs.
  3. If unsure, leave the suffix off — the file lands in unit, the default fast lane.

Examples:

  • tests/agent-frontmatter.test.cjs — unit
  • tests/prompt-injection-guards.security.test.cjs — security
  • tests/installer-end-to-end.install.test.cjs — install
  • tests/sdk-mutation-stress.slow.test.cjs — slow

The suite-suffix convention was chosen over a directory layout (tests/security/) so the 545+ existing test files don't need to move. Existing files all classify as unit until someone explicitly retags them.

Running suites locally

npm test                    # everything (backcompat — same as before)
npm run test:unit           # only unit
npm run test:integration    # only integration
npm run test:install        # only install
npm run test:security       # only security
npm run test:slow           # only slow

npm run test:coverage       # backcompat — coverage over EVERY test
npm run test:coverage:unit  # fast coverage signal — only unit suite
npm run test:coverage:all   # alias for test:coverage

Direct harness invocation also works:

node scripts/run-tests.cjs --suite security
node scripts/run-tests.cjs --suite=security
node scripts/run-tests.cjs --files "tests/command-contract.test.cjs tests/core.test.cjs"
node scripts/run-tests.cjs --files-from .ci-selected-tests.txt

Unknown suites exit non-zero with the list of valid suites. Empty suites (e.g. --suite security before any security-tagged file exists) exit 0 with a no tests in suite "..." notice on stderr so CI lanes don't go red while a suite is being populated.

CI matrix

The Tests workflow runs every PR through a scoped gate generated by scripts/ci-test-scope.cjs.

Lane Node 22 Node 24
ubuntu-latest scoped tests unit + integration + security
windows-latest — scoped Windows/path/shell tests
macos-latest full parity when required full parity when required
  • Node 22 is the engines.node floor (>=22.0.0) — must stay green.
  • Node 24 is the default development lane.
  • Scoped tests are selected from the changed paths, plus a small CLI/package smoke set. They are for confidence on the affected surface, not for counting tests.

The default PR gate runs the broad unit, integration, and security suites once on Ubuntu / Node 24, scoped smoke on Ubuntu / Node 22, scoped Windows/path/shell tests on Windows / Node 24, and unit coverage once on Ubuntu / Node 24. PRs touching workflow, package, test-runner, install, release, or Windows-sensitive surfaces also run the full parity matrix on macOS and the older Windows runtime, plus install and slow on the primary Ubuntu lane. Coverage stays single-lane because multiplying coverage across OS/runtime lanes adds cost without improving the threshold signal.

To inspect the scope locally:

npm run ci:test-scope -- --files "commands/gsd/plan-phase.md"
node scripts/ci-test-scope.cjs --base origin/next --head HEAD

Best practices for forward-compat (Node 24/26)

  • Use process.execPath when spawning Node in tests so each matrix lane exercises the lane's Node version.
  • Avoid stack-trace or error-message prose assertions. Assert err.code, structured JSON fields, or enums — Node minor releases routinely tweak error wording.
  • Prefer node:test, node:assert/strict, and node:test mocks. No external test frameworks.
  • Coverage uses c8 and propagates NODE_V8_COVERAGE through the harness's child process.