Configuration reference
SmartTest reads smarttest.config.{ts,mts,js,mjs,json} from the project root, or the file given with --config. smarttest init writes one for you. The file is transpiled on load, so import { defineConfig } from "@tkmremi/smarttest" works even before the package is resolvable.
import { defineConfig } from "@tkmremi/smarttest";
export default defineConfig({
mode: "balanced",
adapters: ["vitest"],
vcs: { provider: "auto", base: "main", snapshots: true },
snapshots: { storeContent: false, keep: 20 },
tests: { layout: "mirror", directory: "tests", naming: "{FileName}.test.{Extension}" },
generated: [{ path: "src/generated/**", policy: "exclude" }],
triggers: ["db/schema.sql"],
rules: [
{
name: "auth-critical",
match: { path: "src/auth/**" },
risk: "critical",
execution: { alwaysRunOnAffected: true },
},
],
retries: 0,
timeoutMs: 600000,
reports: { formats: ["html", "json", "junit"], outputDir: ".smarttest/reports" },
});
| Key | Type | Default | Notes |
|---|---|---|---|
include | string[] | all ts/tsx/mts/cts/js/jsx/mjs/cjs | Globs of analysed files, relative to the root |
exclude | string[] | — | Added to built-in excludes (node_modules, .yarn, .pnp.cjs, .next, *.d.ts…; dist, build, out, coverage next to a package.json) |
mode | "fast" | "balanced" | "safe" | "balanced" | Overridable per run (--mode) and per rule |
adapters | ("vitest" | "jest" | "node" | "playwright" | "cypress" | "angular" | "pytest" | "unittest")[] | detected | Restricts and orders runners |
runners | { adapter, dir?, config?, patterns?, project? }[] | detected | Explicit runners. Replaces detection (custom config names, node --test patterns, the Angular project for ng test) |
tests.discovery | "runner" | "static" | "runner" | runner: each runner lists the files it collects (cached). static: filename patterns |
vcs.provider | "auto" | "git" | "svn" | "hg" | "native" | "auto" | auto uses Git, then Subversion, then Mercurial, then native snapshots |
vcs.base | string | — | Default comparison ref for run / impact |
vcs.snapshots | boolean | true | With Git, record a native snapshot after each fully passing run (--since-snapshot) |
snapshots.storeContent | boolean | false | Store compressed, deduplicated source content so snapshot diffs get line ranges |
snapshots.keep | number | 20 | Default for snapshot prune |
tests.layout | "mirror" | "colocated" | "domain" | "custom" | detected | Generator layout |
tests.directory | string | "tests" | Root for mirror/domain layouts |
tests.naming | string | {FileName}.test.{Extension} | Tokens: {ClassName} {FunctionName} {MethodName} {FileName} {ModuleName} {PackageName} {DomainName} {Type} {Extension} |
tests.match | string[] | Jest/Vitest defaults | Globs that identify test files |
tests.templates | string | — | Directory with <template>.tpl files ({{source}}, {{scaffold}}) |
generated | { path, policy, from? }[] | — | exclude removes from analysis. Referenced generated files are still tracked as dependencies. With rebuild-required and from (generator input globs), a change to an input selects the tests of the outputs and warns to regenerate |
triggers | string[] | — | Extra mandatory triggers. Built-in ones (tsconfig, package.json, lockfiles, runner configs, setup files, .env*) cannot be removed |
rules | Rule[] | [] | See Rules |
retries | number | 0 | A pass after a retry is reported as flaky |
timeoutMs | number | 600000 | Per-runner timeout |
workers | number | runner default | Passed as --maxWorkers |
reports.formats | ("html" | "json" | "junit" | "markdown")[] | ["html","json"] | |
reports.outputDir | string | ".smarttest/reports" | |
reports.annotations | boolean | true | CI annotations on GitHub Actions, GitLab CI and Bitbucket Pipelines: the selection with each reason, and a summary with the time saved (CI guide). --no-annotations turns them off for one run |
adaptive | boolean | true | run selects every test without building the graph when the last full run took less time than a typical analysis (small suites) |
python | { enabled?, interpreter? } | { enabled: true } | Python preview: analyse .py files and run pytest/unittest. interpreter runs the tests (default .venv/venv, VIRTUAL_ENV, then python3) |
projects | Record<dir, { tests?, mode?, rules? }> | — | Per-project conventions. tests (layout, directory, naming, path) applies to generated tests in that directory; mode is a minimum mode for its changes; rules are scoped to it (paths relative to the project) |
cache | boolean | true | Reuse passing results whose inputs are all unchanged; reported as cached, never as passed (details) |
parallelGroups | number | "auto" | "auto" | Runner groups run at the same time; auto is min(groups, CPUs / 4, 4); Playwright and Cypress always run alone |
storeDir | string | ".smarttest" | Local state. Git-ignored by init |
hook.checks is an array of { name, run, projects?, files?, stage? }. Commands run in affected project directories; {files} becomes quoted changed paths. stage is pre-commit (default), pre-push or both. hook.budgetMs defaults to 60000; exceeding the estimate warns and still runs the full selection. hook.runners (an allow-list of vitest, jest, node, playwright, cypress, angular, pytest, unittest; default every runner) and hook.exclude (project-relative test-file globs; default none) set the hook scope: selected tests outside it are not run by run --staged --hook / run --push --hook and are reported as "selected, not run in the hook (left to CI)" with the rule that removed them, never as passed. run, ci, impact and every other command ignore the scope. An empty hook.runners or an unknown runner name fails with ST_CONFIG_INVALID. See commit and push hooks.
hook.trailer (boolean, default false) adds a Tests: trailer to each commit message with what the pre-commit run of the committed tree did (Tests: 12 run, 2,340 not run · ~4 min saved). smarttest hook install then also writes a commit-msg step; a commit whose tree has no passing pre-commit record gets no trailer. See the commit trailer.
plugins is an array of plugin modules: paths relative to the root (./tools/sql-edges.mjs) or package names. A plugin adds dependency edges that static analysis cannot see; see Plugins. A change to a plugin file inside the project selects the full suite.
Unknown keys produce warnings instead of being silently accepted.
The configuration is repository content, so its paths must stay inside the project: storeDir (a subdirectory, never the project root), reports.outputDir, tests.directory, tests.templates and runners[].dir/config are refused when they are absolute or leave the project with ../ (ST_CONFIG_INVALID). A command-line --output is your own choice and is not restricted. SmartTest also refuses to write its state or reports through a symbolic link inside the project (ST_UNSAFE_PATH), and refuses a store that is committed to Git (ST_STORE_TRACKED). See the security review.
JSON Schema #
The configuration has a published JSON Schema, frozen at 1.0.0: config.v1.schema.json, shipped in the package as @tkmremi/smarttest/schemas/config.v1.schema.json. Point $schema at it in smarttest.config.json to get completion and validation in editors; SmartTest ignores the key:
{
"$schema": "./node_modules/@tkmremi/smarttest/schemas/config.v1.schema.json",
"mode": "balanced",
"plugins": ["./tools/sql-edges.mjs"]
}
A test checks the schema, the SmartTestConfig type and the validator SmartTest runs on load against each other, so they cannot drift. The schema rejects unknown keys so editors catch typos; SmartTest itself only warns about them. Public contracts lists what is frozen and how it may change.
Per-project conventions #
import { defineConfig } from "@tkmremi/smarttest";
export default defineConfig({
tests: { layout: "mirror", directory: "tests" },
projects: {
"FrontLeadsIt/app": {
tests: { layout: "colocated", naming: "{FileName}.server.test.{Extension}" },
},
backend: {
tests: { layout: "custom", path: "backend/src/__tests__/{FileName}.test.{Extension}" },
mode: "safe",
},
},
});
{ProjectDir} is available in path and naming templates.