LPM-cli

lpm test

Auto-detect and run the project's test runner — vitest, jest, or mocha.

lpm test [-- args...]

Detects which test runner is installed in the project (vitest → jest → mocha) and runs it, forwarding any trailing arguments verbatim. No config; no per-runner subcommand.

Examples

lpm test                              # run the whole suite (single-pass)
lpm test src/utils.test.ts            # run one file
lpm test --watch                      # vitest: enter watch mode (see below)
lpm test -- -w                        # short form, same effect
lpm test -- --reporter=verbose

Detection order

  1. vitest (in dependencies or devDependencies)
  2. jest
  3. mocha
  4. scripts.test in package.json (only consulted when no runner above is installed)

Whichever is found first wins. If none of the runners is installed and no test script exists, lpm test reports it.

Argument forwarding

Anything after the command (or after --) is forwarded to the runner:

lpm test -- --coverage --reporter=html
# vitest: vitest run --coverage --reporter=html
# jest:   jest       --coverage --reporter=html

The vitest base command is vitest run (single-pass), except in Watch mode below.

Watch mode

lpm test --watch (or -w) enters watch mode. For vitest specifically, LPM rewrites the base command from vitest run to vitest so the --watch flag is honored — vitest run would silently force single-pass and drop the flag. jest and mocha accept --watch natively against their bare command, so the rewrite is vitest-only.

lpm test --watch                # vitest: → vitest --watch
                                # jest:   → jest --watch
                                # mocha:  → mocha --watch

See Test & bench runners for the full rewrite rules.

Workspaces

lpm test --all                            # every member
lpm test --filter web                     # exact name
lpm test --filter '@scope/*'              # glob
lpm test --filter './apps/*'              # path glob
lpm test --filter web --filter api        # union
lpm test --filter-prod ...shared          # prod graph closure
lpm test --affected                       # only members touched since main
lpm test --affected --base develop
lpm test --filter web --fail-if-no-match  # exit non-zero on typo'd filter
lpm test --filter '@scope/*' --workspace-concurrency 2

Detection runs per member, so a workspace can mix runners (one member uses vitest, another uses jest) and each gets the right command. A member with no installed runner and no scripts.test becomes a per-member detection failure in the JSON envelope rather than aborting the whole run.

--all is mutually exclusive with filters and --affected. --filter and --filter-prod compose with --affected (the affected set is unioned with the filter result). --filter-prod uses the same grammar as --filter, but closure operators ignore devDependencies.

--workspace-concurrency <N> caps how many selected workspace members run at once within each topological level.

Forwarding runner flags with the same names

The workspace flags (--all, --filter, --filter-prod, --affected, --base, --fail-if-no-match, --workspace-concurrency) are claimed by LPM. To pass any of them through to the underlying runner (e.g. bun's --filter, jest's --all), put them after --:

lpm test -- --filter pattern              # forwards --filter to bun/vitest/etc
lpm test -- --all                         # forwards --all to the runner
lpm test --filter web -- --reporter=verbose  # workspace + forwarded args

Anything after -- is forwarded verbatim regardless of name.

Watch with a workspace selector

lpm test --all --watch would start one watcher per workspace member — almost always a mistake. So watch mode is gated by selection size:

lpm test --filter web --watch             # ✓ one member, one watcher (hands off
                                          #   to single-package mode in web/)
lpm test --all --watch                    # ✗ rejected: would start N watchers
lpm test --filter '@scope/*' --watch      # ✓ allowed only if exactly one member matches
                                          # ✗ rejected with the actual count if more
lpm test --filter typo --watch            # ✗ rejected: nothing to watch

When the filter resolves to exactly one member, lpm test runs against that member's directory as if you'd cd-ed in — the existing vitest run → bare vitest rewrite kicks in, so --watch is honored.

Flags

FlagEffect
--allRun in every workspace member
--filter <expr>Select workspace members by the filter grammar (repeatable)
--filter-prod <expr>Select workspace members with production-only dependency closures
--affectedRun only in members affected by changes vs --base
--base <REF>Git base ref for --affected (default: main)
--changed-files-ignore-pattern <glob>Ignore matching git-diff paths for --affected / [git-ref] filters
--test-pattern <glob>Treat matching git-diff paths as test-only for --affected / [git-ref] fan-out decisions
--fail-if-no-matchExit non-zero if no member matches the filter set
--workspace-concurrency <N>Limit concurrent workspace members for --all, --filter, or --affected runs

Anything after -- (or trailing) is forwarded to the runner.

Plus the global flags.

--json in workspace mode

Single-package mode (lpm test) preserves the runner's stdout — LPM does not wrap it. Workspace mode emits a single LPM envelope on stdout; per-member stdout/stderr is captured and surfaced inside the envelope only on failure. See lpm lint for the exact envelope shape. Per-member detection failures (no runner installed) appear as exit_code: null paired with an error string.

See also