GenerateSaaS

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.

RuleDoNever
Behavior onlyDrive a public seam: a function, rendered UI, an HTTP response, a file the CLI writesRead product source as text, or regex over a component
Conventions are lintPut import boundaries, banned casts and config-gate shapes in ESLintWrite a test that scans src/ for a pattern
No inventoriesDerive a list from the one source both sides readPin a file count, or "every file under X must"
Copy is not a contractLocate by role, test id, or a translation key resolved through the real messagesMatch an English sentence
No wall clocksFake timers for delays, in-memory transports, default timeoutsAssert elapsed milliseconds, bind a real socket where a fake exists
Config-drivenRead @repo/config for every expectationHardcode 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.

SuiteFails on
no-skips.test.tsAny skip form: a call, a bare describe.skip passed as a value, a skip(...) off the test context
test-contract.test.tsA 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

SuiteHow to run itWhat gates it
Live vendorspnpm test:liveOPENROUTER_API_KEY, FIRECRAWL_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY
Live end-to-endpnpm e2e:livethe keys above, and/or a coding CLI that can complete a turn
Adversarial agent suiteGENERATESAAS_ADVERSARIAL=1 pnpm --filter @repo/agent-core testthe flag, plus an installed CLI per case
Dev-server suitesGENERATESAAS_DEV_SERVER_URL=http://127.0.0.1:3000 pnpm --filter web test:dev-servera 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.

On this page