# @elench/testkit

`@elench/testkit` discovers `*.testkit.ts` files, infers suite ownership from the
filesystem, starts local services, provisions isolated local Postgres databases,
and runs HTTP, DAL, and Playwright suites.

The package is now driven by `testkit.setup.ts`, not `testkit.config.json`.

## Usage

```bash
cd my-product

# Run every testkit-managed suite
npx @elench/testkit

# Filter by type
npx @elench/testkit --type int
npx @elench/testkit --type dal
npx @elench/testkit --type e2e
npx @elench/testkit --type int,e2e,dal
npx @elench/testkit --type pw

# Shared local stack with parallel workers
npx @elench/testkit --workers 8 --stack-mode shared

# One file-level wall clock budget for every suite file
npx @elench/testkit --file-timeout-seconds 60

# Two reusable local stacks for browser-heavy suites
npx @elench/testkit --workers 6 --stack-mode pooled --stack-count 2

# Run a deterministic shard
npx @elench/testkit --shard 1/3

# Specific service / suite
npx @elench/testkit --service frontend --type pw -s navigation
npx @elench/testkit --service api --type int -s health
npx @elench/testkit --type int,e2e,dal -s dal:queries

# Exact file
npx @elench/testkit --type int --file __testkit__/health/health.int.testkit.ts

# Temporarily ignore repo-declared skip rules
npx @elench/testkit --ignore-skip-rules --file __testkit__/billing/billing.int.testkit.ts

# Deterministic git-trackable status snapshot
npx @elench/testkit --type int --write-status

# Lifecycle
npx @elench/testkit status
npx @elench/testkit destroy
```

## Setup

Create `testkit.setup.ts` at repo root:

```ts
import {
  defineTestkitSetup,
  lifecycle,
  localDatabase,
  nextService,
  service,
  tsxService,
} from "@elench/testkit/setup";

export default defineTestkitSetup({
  execution: {
    workers: 8,
    fileTimeoutSeconds: 60,
    stackMode: "shared",
    stackCount: 1,
  },
  services: {
    api: service({
      ...tsxService({
        cwd: ".",
        entry: "src/index.ts",
        port: 3004,
        readyPath: "/health",
      }),
      envFiles: [".env.testkit"],
      database: localDatabase(),
      migrate: lifecycle("npm run db:migrate", {
        testkitCmd: "npm run db:migrate:testkit",
      }),
    }),
    frontend: service({
      ...nextService({
        cwd: "frontend",
        port: 3000,
        env: {
          NEXT_PUBLIC_API_URL: "{baseUrl:api}",
        },
      }),
      dependsOn: ["api"],
      envFiles: ["frontend/.env.testkit"],
    }),
    billing: service({
      skip: {
        files: [
          {
            path: "__testkit__/invoices/invoices.int.testkit.ts",
            reason: "Billing is still stubbed locally",
          },
        ],
        suites: [
          {
            selector: "pw:lifecycle",
            reason: "End-to-end billing lifecycle is not implemented yet",
          },
        ],
      },
    }),
  },
});
```

`testkit.setup.ts` is optional for simple repos, but it is the primary escape hatch
for:

- worker and stack topology
- per-file wall clock timeout budget
- multi-service graphs
- local DB configuration
- migrate / seed commands
- test-local migrate / seed overrides
- named HTTP suite profiles
- repo-declared suite/file skip policies with explicit reasons
- telemetry upload configuration

## Authoring

HTTP suites:

```ts
import { defineHttpSuite } from "@elench/testkit";

const suite = defineHttpSuite(({ rawReq }) => {
  rawReq("GET", "/health");
});

export default suite;
```

`testkit` suite files should default-export the suite object returned by
`defineHttpSuite(...)` or `defineDalSuite(...)`.

Named HTTP profiles live in `testkit.setup.ts` and can be referenced by name:

```ts
import { defineHttpSuite } from "@elench/testkit";

const suite = defineHttpSuite({ profile: "default-auth" }, ({ req, setupData }) => {
  req("GET", "/api/auth/session", setupData);
});
```

DAL suites:

```ts
import { defineDalSuite } from "@elench/testkit";

const suite = defineDalSuite(({ db }) => {
  db.query("select 1");
});

export default suite;
```

Low-level runtime primitives remain available:

```ts
import { check, group, http, waitFor } from "@elench/testkit/runtime";
```

`waitFor()` consumes the file budget configured by `execution.fileTimeoutSeconds`.
Consumers should not set local timeout values in test files.

```ts
import { waitFor } from "@elench/testkit/runtime";

const response = waitFor(
  () => req("GET", "/api/v1/jobs/123", setupData),
  (res) => JSON.parse(res.body).data?.status === "completed",
  { description: "job 123 to complete" }
);
```

## Discovery

`testkit` discovers suites from `__testkit__/` directories.

Example layouts:

- `src/api/routes/__testkit__/auth/me.int.testkit.ts`
- `src/db/__testkit__/sessions/count-type.dal.testkit.ts`
- `frontend/__testkit__/navigation/navigation.pw.testkit.ts`
- `avocado_api/internal/handler/__testkit__/repos/crud.int.testkit.ts`

`testkit` uses these suffixes automatically:

- `*.int.testkit.ts`
- `*.e2e.testkit.ts`
- `*.dal.testkit.ts`
- `*.load.testkit.ts`
- `*.pw.testkit.ts`

Ownership is inferred from:

- the deepest matching service root from `services.<name>.local.cwd`
- optional `services.<name>.discovery.roots` overrides for shared-root edge cases

Suite names are inferred from the colocated path:

- `auth/__testkit__/*.int.testkit.ts` => `auth`
- `routes/__testkit__/auth/*.int.testkit.ts` => `auth`

## Local Databases

`@elench/testkit` provisions Docker-managed local Postgres automatically for
services that define `database: localDatabase(...)`.

- template databases are cached
- stack databases are cloned from templates
- template fingerprints are derived automatically from env files, migrate/seed
  config, and repo contents

Manual `template.inputs` overrides are still available for edge cases.

## Development Tests

```bash
npm test
npm run test:unit
npm run test:integration
npm run test:system
```
