This package has two Vitest configs. Run the right one for the change.
| Command | Config | Scope | Needs build? |
|---|---|---|---|
pnpm --filter stash test |
vitest.config.ts |
Unit tests under src/__tests__/** and src/**/__tests__/** |
Partly — needs @cipherstash/stack built (see below). Turbo's ^build supplies it in CI. |
pnpm --filter stash test:e2e |
vitest.integration.config.ts |
E2E tests under tests/e2e/**.e2e.test.ts driving the built dist/bin/stash.js through a real pty (node-pty) |
Yes — run pnpm --filter stash build first, or use the turbo test:e2e task which depends on build. |
The unit config explicitly excludes tests/e2e/** so the default pnpm test
stays fast.
It is not fully self-contained, despite running standalone in CI. Some src
modules import workspace packages that publish ./dist only, so an unbuilt
workspace fails at collection with Failed to resolve entry for package …
rather than at an assertion. vitest.config.ts aliases @cipherstash/migrate
to its source to remove one such coupling; @cipherstash/stack remains, reached
via packages/migrate/src/backfill.ts and a direct import in
init/lib/__tests__/introspect.test.ts. Deleting packages/stack/dist fails 10
files. Closing it needs vitest.shared.ts's stackSourceAlias, which cannot be
spread into this config: its '@/' points at packages/stack/src while this
package's points at packages/cli/src, and a flat alias map admits only one —
spread it after and stack's entry clobbers the CLI's, spread it before and the
CLI's breaks stack's own source imports (#787 review).
Update tests/e2e/** whenever you:
- Add or rename a top-level command, subcommand, or flag (smoke tests assert on help text, command names, and unknown-command behavior).
- Change the user-facing string for an exit message that an existing E2E
asserts on (e.g. cancellation text, "Unknown auth command", the
db migratestub warning). Strings that tests assert on live insrc/messages.ts— update the constant there and both prod and tests pick it up. Don't hard-code the new wording in a test. - Touch
src/bin/stash.tsargv parsing, exit codes, or top-level error handling. - Add a new clack prompt that changes the first prompt rendered for a command currently covered by E2E (the cancel test waits for a specific prompt label).
You do not need to add an E2E test for every new flag or branch — keep E2E coverage to the highest-value flows. Unit tests still own the bulk of behaviour coverage.
tests/helpers/pty.ts exports render(args, opts?) which spawns
dist/bin/stash.js inside a real pseudo-terminal and returns:
output— cumulative ANSI-stripped stdout.raw— same, with ANSI escapes preserved (handy when debugging).waitFor(text|regex, timeoutMs?)— polls until the match appears.key(name)— sends keystrokes (Enter,Up,Down,CtrlC, etc.).write(string)— raw stdin write.exit— promise resolving to{ exitCode, signal? }.kill(signal?)— terminate the pty.
A real pty is required because @clack/prompts switches stdin to raw mode
and renders differently when stdout isn't a TTY; piped-stdin mocks don't
exercise the same code paths.
- Build before E2E.
dist/bin/stash.jsis the artifact under test. The turbotest:e2etask already depends onbuild, but if you invoke the script directly you must build first. - macOS spawn-helper exec bit. pnpm strips the executable bit when
unpacking node-pty's prebuilds. The helper auto-fixes this at module load
via
ensureSpawnHelperExecutable. If you seeposix_spawnp failedafter reinstallingnode_modules, the chmod logic should handle it on next test run; if not, manuallychmod +xthe helper undernode_modules/.pnpm/node-pty@*/node_modules/node-pty/prebuilds/<plat>/spawn-helper. - Don't broaden the cancel test target.
auth loginwas chosen because the region picker runs before any network I/O. Don't move the cancel assertion to a command that hits the auth server or DB before the first prompt — flaky. - Region resolution is CI-aware.
resolveRegion(insrc/commands/auth/region.ts) mirrors theDATABASE_URLresolver: it only renders the interactive picker whenstdin.isTTYandCIis unset, and otherwise honours--region/STASH_REGIONor exits with an actionable error (JSON in--jsonmode). Because the pty harness defaultsCI=true, the interactive-cancel test passesenv: { CI: '' }to force the picker to render. The pure helpers (normalizeRegion,regionSlugs) and the resolver policy have unit coverage insrc/commands/auth/__tests__/region.test.ts— the module imports no native code, so it runs under the fast unit config. - Use
src/messages.tsfor assertion-stable strings. The module is a single typedas constobject grouping copy by area (cli,auth,db). Prod call sites import the same constants the tests do, so a copy tweak only needs to land in one place. Add tomessages.tsonly when a test actually asserts on the string — premature extraction is worse than copy-paste here. For literals tests don't touch (e.g. command names likeinit,eql install), keep them inline. - Telemetry. The CLI has anonymous, opt-out usage analytics in
src/telemetry/(posthog-node, loaded lazily only when an event is actually sent). It ships dormant — a real project key is embedded only by the release build (STASH_POSTHOG_KEYrepo variable → tsup define) — and is force-disabled in every pty e2e child viaSTASH_TELEMETRY_DISABLED=1intests/helpers/pty.ts. Two contracts to preserve when touching it: (1) event VALUES are closed vocabularies —classifyCommand/classifyErrorTypeinsrc/telemetry/classify-command.tsmust wrap anything argv- or error-derived before emit; (2) never interceptprocess.exitwith a thrown signal — @clack/core exits from keypress handlers and several commands have broad catches, so interception breaks cancel flows (tried and reverted; seesrc/cli/exit.ts). Commands that terminate via deepprocess.exit()are simply not tracked; cooperative exits usethrow new CliExit(code)from verified-unwindable sites only.
Background, alternative approaches considered, and the phase-2 messages
module are in docs/plans/cli-pty-integration-tests.md.