# Configuration reference

The fields of testkit.config.ts in Testkit 0.4.1. The file lives at the repo root and holds the repo's test policy.

## The file

```ts title=testkit.config.ts
import { app, defineConfig } from "@gfdelarue/testkit/config";

export default defineConfig({
	services: {
		api: app.node({
			build: null,
			start: "npm start",
			port: 4392,
		}),
	},
});
```

Import the builders from `@gfdelarue/testkit/config`.

| Export | Returns |
| --- | --- |
| `defineConfig(config)` | The config, typed |
| `app.node(options)` | A Node service |
| `app.next(options)` | A Next.js service |
| `database.postgres(options)` | A Postgres database for a service |
| `database.fixture(options)` | A service that is only a database |
| `toolchain.node(options)` | A rule for the Node and npm versions |

> Testkit 0.4.1 ignores a top-level key that it does not know, and prints no warning. A [`skip`](#skip) block placed beside `services` does nothing. To confirm that a rule took effect, run `testkit discover` and read the `skipped` count.

## Top-level fields

### `execution`

| Field | Default | Meaning |
| --- | --- | --- |
| `taskConcurrency` | `1` | The most test tasks that run at once. `--task-concurrency` overrides it. |
| `fileTimeoutSeconds` | `60` | The wall-clock limit for each file. `waitFor()` spends from the same limit. |

```ts
execution: {
	taskConcurrency: 4,
	fileTimeoutSeconds: 120,
},
```

### `services`

A map from a service name to a service. The name appears in run output, and `--service` takes it. Build each service with [`app.node(...)`](#appnode), [`app.next(...)`](#appnext), or `database.fixture(...)`.

### `profiles.http`

Named k6 options. An [HTTP test](https://turnstone.cloud/docs/http-tests.md#use-k6-directly) selects one with `defineHttpSuite({ profile: "raw" }, ...)`.

```ts
profiles: {
	http: {
		raw: {
			options: { vus: 1, iterations: 1 },
		},
	},
},
```

When a run selects a k6 file, every profile must be plain JSON. A profile that holds a function fails config validation. Put auth and header functions in the test file or in a helper beside it.

### `ui`

Targets for [browser tests](https://turnstone.cloud/docs/browser-tests.md#point-the-test-at-another-target).

| Field | Default | Meaning |
| --- | --- | --- |
| `frontendBaseUrl` | The run's base URL | The browser target |
| `backendBaseUrl` | None | The API target for `createAuthedApiClient` |
| `allowRemote` | `false` | Permit a target that is not on loopback |

Options that a test passes to `createUiSandbox` win over this block.

### `lint`

Settings for `testkit lint`. Omit the block to run the default rules.

| Rule | Default |
| --- | --- |
| `engineImports` | On |
| `configImports` | On |
| `dataSuiteContract` | On |
| `dalParallelSafety` | On |
| `uiSpecShape` | On |
| `unitTestBoundaries` | Off |
| `testkitBoundary` | Off |

Set a rule to `true`, `false`, or an options object. The options are `enabled`, `include`, and `exclude`. `testkitBoundary` also takes `allowed` and `trackedOnly`.

`lint.ui` sets the limits for browser test files.

| Field | Default | Meaning |
| --- | --- | --- |
| `ui.maxLines` | `220` | The most lines in one `ui` file |
| `ui.maxTests` | `8` | The most tests in one `ui` file |

```ts
lint: {
	rules: {
		unitTestBoundaries: true,
	},
	ui: { maxLines: 300 },
},
```

A finding from an enabled rule fails the command. There are no severity levels.

### `areas`

A map from an area id to a named part of the product. `testkit bug file` uses it to name the area that holds a fault, and puts an `area:<id>` label on the issue.

| Field | Meaning |
| --- | --- |
| The key | The area id. It uses lowercase letters, digits, and single hyphens, and has at most 40 characters. |
| `name` | One line of 1 to 60 characters. It becomes the label's description. |
| `paths` | At least one glob, relative to the repo root and written with `/`. Testkit rejects a leading `!`, a leading `/`, and a `..` segment. |

```ts
areas: {
	checkout: { name: "Checkout", paths: ["src/checkout/**"] },
	billing: { name: "Billing", paths: ["src/billing/**", "src/invoices/**"] },
},
```

Each source file belongs to one area at most. `testkit campaign start` lists the source files that no area claims and the files that two areas claim. `testkit bug file` stops when no area claims the fault's file, or when two areas do, and names the file. Without `areas`, Testkit files bugs with no area.

An area's share is its part of the repo's source bytes at the commit that holds the bug. Tests, generated files, vendored code, and migrations do not count as source.

### `bugs`

Settings for bug filing, and for runs that meet a filed bug.

| Field | Default | Meaning |
| --- | --- | --- |
| `knownFailures` | `"fail"` | `"fail"` or `"allow"`. With `"fail"`, every failing test fails the run. With `"allow"`, a run passes when an open issue with the `testkit` label covers each failure. |
| `repository` | The `origin` remote | The repo that holds the bug issues. Write `owner/name` for a repo on the same host as the `origin` remote, or a URL such as `https://host/owner/name`. |

```ts
bugs: {
	knownFailures: "allow",
	repository: "acme/shop-bugs",
},
```

[Known failures](https://turnstone.cloud/docs/bug-reports.md#known-failures) gives the rule for `knownFailures`. With `"allow"`, the run reads the open issues, so the `testkit run` step in CI [needs a token](https://turnstone.cloud/docs/ci.md#pass-a-run-whose-failures-are-all-filed).

`testkit bug file` files issues in `repository`, and `testkit run` and `testkit ci report` read the open issues from it. Commit statuses and pull request comments always go to the `origin` repo. If a Turnstone project reads this repo's bugs, [enter the same repo in the project](https://turnstone.cloud/docs/sync.md#issues-repository).

### `artifacts`

| Field | Meaning |
| --- | --- |
| `retention.maxRuns` | The most runs to keep |
| `retention.maxAgeDays` | The oldest run to keep, in days |
| `retention.preserveSuites` | Suites whose runs retention does not remove, such as `ui:performance` |
| `publication.directory` | A directory that Testkit copies each finished run into |
| `publication.required` | When `true`, a failed copy fails the command. The local run stays in place. |

With no `retention` block, Testkit keeps every run. It records retention decisions under `.testkit/results/retention` and each publication under `.testkit/results/publications`.

### `fingerprints`

Testkit hashes the repo's files to decide when a cache is stale. `exclude` removes generated files from the hash, and `include` puts a path back below an excluded directory.

```ts
fingerprints: {
	exclude: ["next-env.d.ts"],
},
```

Patterns use `/` as the only separator. Testkit always excludes `.testkit/` and `.next-testkit/`.

### `toolchains`

Named rules for the Node and npm versions that run a service's commands. A service selects one with `runtime.toolchain`.

```ts
toolchains: {
	frontendNode: toolchain.node({
		cwd: "frontend",
		detect: "auto",
		install: "download",
	}),
},
```

| Option | Values | Meaning |
| --- | --- | --- |
| `cwd` | A path, relative to the repo root | The directory that holds the version files |
| `detect` | `"auto"`, `"off"` | `"auto"` reads the versions from the repo |
| `install` | `"require-host"`, `"download"` | `"download"` fetches Node into `.testkit/_toolchains/node` and checks it against a trusted release manifest. It supports glibc hosts only. |
| `node`, `npm` | A version or a range | The version to use |

With `detect: "auto"`, Testkit reads the Node version from `package.json#volta.node`, `.nvmrc`, `.node-version`, `.tool-versions`, and `package.json#engines.node`. It reads the npm version from `package.json#volta.npm`, `package.json#packageManager`, and `package.json#engines.npm`.

## Services

### `app.node(...)`

| Option | Meaning |
| --- | --- |
| `port` | Required. The port the service listens on. Testkit passes it to the process as `PORT`. |
| `start` | The command that starts the service |
| `entry` | The service's entry file, such as `src/index.ts` |
| `build` | How Testkit builds the service. Set `null` when the service needs no build. |
| `cwd` | The service's directory, relative to the repo root |
| `env` | `{ values, databases }`. See [`env`](#env). |
| `toolchain` | A name from [`toolchains`](#toolchains), or an inline `toolchain.node(...)` |
| `shutdownTimeoutMs` | How long the service gets to exit before Testkit kills it. The default is 1000 ms. |

`app.node` also accepts `baseUrl`, `buildInputs`, `outDir`, `readyPath`, `readyTimeoutMs`, `readyUrl`, `runtime`, and `tsconfig`, and every field under [Service fields](#service-fields). The types in `@gfdelarue/testkit/config` define each one.

### `app.next(...)`

Takes the same options as `app.node`, except `entry`, `outDir`, `readyPath`, and `tsconfig`. It adds `mode`, which is `"dev"` or `"start"`, and `api`.

```ts
frontend: app.next({
	cwd: "frontend",
	mode: "start",
	port: 3000,
	dependsOn: ["api"],
	env: {
		values: { NEXT_PUBLIC_API_URL: "{baseUrl:api}" },
	},
}),
```

## Service fields

### `env`

Environment values for the service process and for every test that targets the service. `app.node` and `app.next` take `{ values, databases }`.

Values can use [template tokens](#template-tokens).

`databases` expands one service's database into prefixed entries.

```ts
env: {
	databases: { api: { prefix: "API", service: "api" } },
},
```

That block sets `API_DATABASE_HOST`, `API_DATABASE_PORT`, `API_DATABASE_NAME`, `API_DATABASE_USER`, `API_DATABASE_PASSWORD`, and `API_DATABASE_SSL`. An entry in `values` with the same name wins.

A test reads the values from its engine's context.

| Engine | Where the values are |
| --- | --- |
| `k6` | `__ENV` |
| `node-vitest` | `context.env`, read-only |
| `playwright` | `readPlaywrightContext()?.env` |

### `envFile` and `envFiles`

Env files to load into the same environment as `env`: `envFile` takes one path, and `envFiles` takes a list.

### `database`

`database.postgres(...)` gives the service a local Postgres in Docker. The service process receives it as `DATABASE_URL`. [Test the database](https://turnstone.cloud/docs/database-tests.md) has a working example.

| Option | Meaning |
| --- | --- |
| `template.inputs` | Files that rebuild the template when they change |
| `template.migrate` | Steps that build the schema |
| `template.seed` | Steps that add data |
| `template.verify` | Steps that must pass before Testkit caches the template |
| `binding` | `"shared"` reuses one database. `"per-runtime"` clones the template for each runtime instance. |
| `runtimeConnections` | The connection budget of one runtime instance |
| `maxConnections`, `reservedConnections` | The capacity of the local Postgres, which the scheduler uses to admit tasks |
| `roles` | Roles to create, each as `{ name, login: false }` |
| `sourceSchema` | A source database to replay the schema from, such as `database.schema.fromEnv("PRODUCTION_DATABASE_URL")` |
| `image` | The Postgres image |

A template stage is an ordered list of steps.

| Step | Example |
| --- | --- |
| A command | `{ kind: "command", run: "npm run db:seed" }` |
| An SQL file | `{ kind: "sql-file", path: "db/schema.sql" }` |
| A module export | `{ kind: "module", target: "src/testkit/verify-seed.ts#verifySeed" }` |

Docker also runs `psql` and `pg_dump`. To use another client image, set `TESTKIT_POSTGRES_CLIENT_IMAGE`.

### `databaseFrom`

The name of another service. This service receives that service's `DATABASE_URL`.

### `dependsOn`

The services that must start first.

### `runtime`

| Field | Meaning |
| --- | --- |
| `instances` | How many copies of the service Testkit keeps running |
| `maxConcurrentTasks` | The most test files that one instance serves at once. Omit it to leave `taskConcurrency` as the only limit. |
| `toolchain` | A name from [`toolchains`](#toolchains), or an inline `toolchain.node(...)` |
| `prepare.steps` | Steps that run once per instance before the service starts. Each step takes `cwd` and `inputs`. |
| `prepare.inputs` | Files that rerun the steps when they change |

```ts
runtime: {
	instances: 1,
	maxConcurrentTasks: 4,
	prepare: {
		inputs: ["frontend/src", "frontend/package.json"],
		steps: [{ kind: "command", run: "npm run build", cwd: "frontend" }],
	},
},
```

### `skip`

Skip rules for the service. Every rule needs a `reason`.

```ts
services: {
	api: app.node({
		port: 4392,
		skip: {
			files: [
				{
					path: "src/checkout/__testkit__/payout.contract.testkit.ts",
					reason: "The payout sandbox is down until Friday",
				},
			],
			suites: [
				{ selector: "ui:billing", reason: "Billing UI is a placeholder" },
			],
		},
	}),
},
```

| Field | Meaning |
| --- | --- |
| `files[].path` | An exact path, relative to the repo root |
| `suites[].selector` | A suite selector, such as `ui:billing` |
| `reason` | Required. The run prints it and the run record keeps it. |

To skip one file, prefer [`defineFile({ skip })`](https://turnstone.cloud/docs/test-files.md#skip-a-file-with-a-reason) in the file itself. `testkit run --ignore-skip-rules` runs skipped files anyway.

### `requirements`

Lock rules for the service. Files that need the same lock do not run at the same time.

```ts
requirements: {
	suites: [{ selector: "ui:billing", locks: ["billing-ledger"] }],
	files: [
		{
			path: "src/jobs/__testkit__/worker.int.testkit.ts",
			locks: ["global-worker-loop"],
		},
	],
},
```

To lock one file, prefer [`defineFile({ locks })`](https://turnstone.cloud/docs/test-files.md#keep-two-files-apart-with-a-lock).

### `discovery`

`discovery.roots` assigns directories to the service. Testkit normally gives a test file to the service with the deepest matching `cwd`, so set `roots` only when two services share a directory.

## Template tokens

Strings in a service can hold these tokens.

| Token | Value |
| --- | --- |
| `{service}` | The service's name |
| `{runtime}` | The runtime instance |
| `{port}`, `{port:name}` | The port of this service, or of the service `name` |
| `{baseUrl}`, `{baseUrl:name}` | The base URL |
| `{readyUrl}`, `{readyUrl:name}` | The readiness URL |
| `{stateDir}`, `{prepareDir}`, `{lease}`, `{leaseDir}` | Testkit's directories and lease for the instance |

These tokens work only in `env` values.

| Token | Value |
| --- | --- |
| `{dbUrl}` | The connection URL |
| `{dbHost}`, `{dbPort}` | The host and the host port |
| `{dbName}`, `{dbUser}`, `{dbPassword}` | The database name, user, and password |
| `{dbSsl}` | `1` when the connection uses SSL, and `0` when it does not |

A bare database token reads the service's own database. `{dbUrl:api}` reads the database of the service `api`, which must be in `dependsOn`.

Testkit rejects a database token in `start`, `cwd`, `runtime.prepare`, and `database.template` strings. In those places, read `DATABASE_URL` when the command runs.

## Environment limits

Each engine reserves some names and limits how much environment a test can receive.

| Engine | Reserved prefixes | Reserved names | Limit |
| --- | --- | --- | --- |
| `k6` | `K6_`, `TESTKIT_` | `BASE_URL`, `GODEBUG` | 128 entries and 16,384 environment-block units |
| `node-vitest` | `TESTKIT_`, `VITEST_` | `BASE_URL`, `FORCE_COLOR`, `NODE_OPTIONS`, `NO_COLOR`, `UPDATE_SNAPSHOT` | 512 entries and 4 MiB |
| `playwright` | `TESTKIT_`, `PLAYWRIGHT_`, `PWTEST_`, `PW_TEST_` | `BASE_URL`, `FORCE_COLOR`, `NODE_OPTIONS`, `NO_COLOR` | 512 entries and 2 MiB |

Every engine also reserves `APPDATA`, `ComSpec`, `HOME`, `LANG`, `LC_ALL`, `LOCALAPPDATA`, `PATH`, `PATHEXT`, `SystemRoot`, `TEMP`, `TMP`, `TMPDIR`, `USERPROFILE`, `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, and `XDG_DATA_HOME`.

Testkit compares names without regard to case. It rejects an empty name, a name that contains `=`, and two names that differ only in case.
