test-discovery

Explains the prescribed __test__/ directory layout, test file naming conventions, and how source coverage is derived from the adjacent src/ folder.

SKILL.md

GitHub

Test Layout — __test__/ Conventions

Projects using this layout place tests in a __test__/ directory that sits adjacent to src/. Coverage is calculated from that src/ directory. Auto-discovery tools (like AgentPlugin.discover()) may generate Vitest projects from this structure automatically, but custom project configurations are also possible.

<project-root>/
├── src/            source code — coverage is calculated from here
└── __test__/       tests and test support files

Discovery recognises these two locations and no others. A __test__/ directory anywhere else in the package tree — lib/scripts/__test__/, for example — is not a valid test location: Vitest will not collect it, and the tests inside it will never run. The walker also stops at a nested package.json boundary, so a sibling package's tests are never double-counted in a monorepo scan.

__test__/ Directory Structure

Test files sit flat at the top level of __test__/; helper files go in reserved subdirectories organised by test kind.

__test__/
├── *.test.ts | *.unit.test.ts  unit test files
├── fixtures/                   static files needed by unit tests
├── snapshots/                  Vitest snapshot files for unit tests
├── utils/                      shared helpers for unit tests
├── integration/
│   ├── *.int.test.ts           integration test files
│   ├── fixtures/               static files needed by integration tests
│   ├── snapshots/              Vitest snapshot files for integration tests
│   └── utils/                  shared helpers for integration tests
└── e2e/
    ├── *.e2e.test.ts           e2e test files
    ├── fixtures/               static files needed by e2e tests
    ├── snapshots/              Vitest snapshot files for e2e tests
    └── utils/                  shared helpers for e2e tests

Subdirectory Rules

Subdirectory Coverage Lint Typecheck Purpose
fixtures/ excluded excluded excluded Static files consumed by tests (JSON, TOML, binary, etc.)
snapshots/ excluded excluded excluded Vitest snapshot output (managed automatically)
utils/ excluded required required Shared helpers — mocks, extended expect, shared types

Fixtures and snapshots are fully excluded; place arbitrary static content there without concern for linting or types. Utils files must be valid TypeScript and pass the linter — treat them as first-class support code, just not counted toward coverage.

These three columns describe coverage, linting, and typechecking — not Vitest discovery, which is a separate mechanism. Discovery only ever collects *.test.ts files, so a helper in any of these directories is never collected wherever it sits. What discovery does exclude is a test file under a helper directory named directly beneath __test__/ — __test__/utils/a.test.ts is not collected, while __test__/integration/utils/a.test.ts is an ordinary suite. That boundary is anchored at the test root deliberately: matching the helper name at any depth silently swallowed real suites (issue #251).

Escape Hatch — __fixtures__ and __snapshots__ Anywhere

Directories named __fixtures__ or __snapshots__ (double-underscore wrapping) are recognised and excluded from coverage, linting, and typechecking wherever they appear inside a project — not just under __test__/. This can be useful when a fixture needs to live close to the source file it supports.

Prefer the prescribed locations (__test__/fixtures/, __test__/integration/fixtures/, etc.) over this escape hatch. Scattering __fixtures__ directories through src/ makes test support files harder to find and the project harder to navigate.

Test File Classification

The scanner classifies files by suffix; first match wins:

Suffix Kind Lives in
*.e2e.test.ts e2e __test__/e2e/
*.int.test.ts int __test__/integration/
*.test.ts or *.unit.test.ts unit __test__/ (top level)

Limitation — the PreToolUse Hook Only Knows the Default Layout

The test-location.sh PreToolUse hook and its backing vitest-agent agent check-test-path CLI subcommand classify a test path against the default layout described above — classifyTestPath is purely lexical and never loads a project's actual Vitest config. Most workspaces use the default layout AgentPlugin.discover() produces, so this is right almost always. It is not aware of a consumer who changes discovery via:

  • a custom discoverStrategy passed to AgentPlugin({ discoverStrategy }) (including discoverStrategy: false to disable discovery entirely),
  • AgentPlugin.discover().addProject({ name, path }) registering a non-package folder as an extra project, or
  • a class extending DefaultDiscoverStrategy / implementing DiscoverStrategy directly.

For such a workspace, the default-layout rule can be wrong about a path the consumer's actual config collects fine. Because a denial is the strongest action the hook can take, check-test-path fails open (exits 1 with no stdout — the hook then no-ops) whenever it detects one of those markers in the workspace's vitest.config.* / vitest.workspace.* / vite.config.* source text, or whenever that config file cannot be found or read at all. A missing or unreadable config is treated exactly like a detected marker: no verdict is safer than a confidently wrong one. The detector (detectNonDefaultDiscoverStrategy in @vitest-agent/sdk) is a lexical scan with a best-effort comment strip, so a marker string that only appears inside a comment can still register as a false positive — that direction (failing open) is the safe one, so it is accepted rather than built out further.

If you hit a wrong deny anyway — or just want the check off — set VITEST_AGENT_TEST_LOCATION_HOOK=off (0 and false also work) in your environment. The hook checks this before doing anything else and emits a silent noop without ever invoking the CLI.