# lpm test (/docs/dev/test)



```bash
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 [#examples]

```bash
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 [#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 [#argument-forwarding]

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

```bash
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](#watch-mode) below.

## Watch mode [#watch-mode]

`lpm test --watch` (or `-w`) enters watch mode. For vitest specifically, LPM CLI 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.

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

See [Test & bench runners](/docs/dev/test-bench-runners#watch-mode) for the full rewrite rules.

## Workspaces [#workspaces]

```bash
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 [#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 CLI. To pass any of them through to the underlying runner (e.g. bun's `--filter`, jest's `--all`), put them after `--`:

```bash
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 [#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:

```bash
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 [#flags]

| Flag                                    | Effect                                                                                                  |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `--all`                                 | Run in every workspace member                                                                           |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                       |
| `--affected`                            | Run 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-match`                    | Exit 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](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

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

## See also [#see-also]

* [`lpm bench`](/docs/dev/bench) — same auto-detection, for benchmarks
* [`lpm run test`](/docs/dev/run) — run the `test` script from `package.json` instead
* [Test & bench runners](/docs/dev/test-bench-runners) — how detection works
