Writing tests
Unit-test conventions, the zero-skip policy and the conditional registration that replaces it, premise tests, and the opt-in suites.
Tests live beside the code they cover, in each package's tests/ directory. Every suite in the default gate reports zero skipped, and a guard keeps it that way.
The contract
A test earns its place by catching a real regression and surviving unrelated edits.
| Rule | Do | Never |
|---|---|---|
| Behavior only | Drive a public seam: a function, rendered UI, an HTTP response, a file the CLI writes | Read product source as text, or regex over a component |
| Conventions are lint | Put import boundaries, banned casts and config-gate shapes in ESLint | Write a test that scans src/ for a pattern |
| No inventories | Derive a list from the one source both sides read | Pin a file count, or "every file under X must" |
| Copy is not a contract | Locate by role, test id, or a translation key resolved through the real messages | Match an English sentence |
| No wall clocks | Fake timers for delays, in-memory transports, default timeouts | Assert elapsed milliseconds, bind a real socket where a fake exists |
| Config-driven | Read @repo/config for every expectation | Hardcode a value a project may change |
Two artifacts that cannot import each other - Maestro flow files and their inventory, strip-marker regions and the CLI registry - may be compared by a test. List it in CROSS_ARTIFACT_CHECKS (packages/e2e-support/src/test-contract-debt.ts) with a one-line reason.
Conventions
- Build fixtures with typed factory helpers using
Partial<T>from a package's exported types; never re-declare types by hand. - Put temp paths under
os.tmpdir(); never hard-code/tmp. - Build a case's premise instead of assuming it: a file that must exist, an env var, a configured host. Prefer a synthetic fixture over your own tree, and register conditionally when the premise genuinely cannot exist.
No skips - register conditionally
it.skip, it.skipIf, describe.skipIf, runIf, fixme, todo and testInfo.skip are banned in both runners. A test that does not apply to your build is not registered.
// Wrong - the summary reports a skip, and every reader has to chase what it meant.
describe.skipIf(config.payment.provider !== "stripe")("stripe webhooks", () => { /* … */ });
// Right - the block exists only in a build that has the feature.
if (config.payment.provider === "stripe") {
describe("stripe webhooks", () => { /* … */ });
}Keep the inverse case wherever one exists. Nest both arms inside one describe - the shared ESLint preset's test/no-identical-title rejects two top-level describes with the same name:
describe("organizations", () => {
if (config.tenancy.multiTenant) {
it("invites a member", () => { /* … */ });
} else {
it("ships no organization surface", () => { /* … */ });
}
});apps/web and apps/desktop are not on that preset, so two describes with inverse names are fine there.
Premise tests
A file whose every block is conditional registers one test in the other branch, asserting the config fact the file keys on. Both runners fail a file that registers nothing.
} else {
it("this build ships no stripe provider", () => {
expect(config.payment.provider).not.toBe("stripe");
});
}The guard
Two suites in packages/e2e-support/tests/ parse every test file with the TypeScript compiler API and run inside pnpm test, so they cover your tests as well as the shipped ones.
| Suite | Fails on |
|---|---|
no-skips.test.ts | Any skip form: a call, a bare describe.skip passed as a value, a skip(...) off the test context |
test-contract.test.ts | A read or directory walk of the product tree from a test, an assertion on Date.now() or performance.now() differences, a pinned file count |
The CLI strip tests under packages/cli/tests/ are exempt from the source-read rules: files are the output they test.
Opt-in suites
| Suite | How to run it | What gates it |
|---|---|---|
| Live vendors | pnpm test:live | OPENROUTER_API_KEY, FIRECRAWL_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY |
| Live end-to-end | pnpm e2e:live | the keys above, and/or a coding CLI that can complete a turn |
| Adversarial agent suite | GENERATESAAS_ADVERSARIAL=1 pnpm --filter @repo/agent-core test | the flag, plus an installed CLI per case |
| Dev-server suites | GENERATESAAS_DEV_SERVER_URL=http://127.0.0.1:3000 pnpm --filter web test:dev-server | a dev server you started |
Each prints one not run: <item> — <why> line instead of a skip, and exits 0 when it finds nothing to run. An opt-in gate that finds nothing is a report, not a failure.
The adversarial suite drives real coding CLIs under your signed-in subscription and bills it. Print the line from process.stdout.write when you add one: Vitest's default reporter swallows console.info from a worker file, and only a globalSetup runs in the main process.