// The six first-class test types. `x verify` selects a suite by FILENAME — `*.job.test.ts`, and // so on — so these helpers only prefix the reported name with its type, which is what makes a // failure line say which of the six steps it belongs to. See packages/cli/src/verify-tests.ts. import { test } from 'bun:test'; import { TestEvalThresholdError } from './errors'; import type { FixtureBody } from './fixtures'; import { runWithFixtures } from './fixtures'; import { paced } from './test-pace'; export const TEST_TYPES = ['unit', 'contract', 'live', 'job', 'e2e', 'eval'] as const; export type TestType = (typeof TEST_TYPES)[number]; export const SEPARATOR = ' · '; export const testName = (type: TestType, name: string): string => `${type}${SEPARATOR}${name}`; /** * What a typed test may say about itself beyond its body. One key today: a deliberately heavy * test (48 PDF renders) used to have to drop to `test(testName('unit', …), body, 30_000)` to * outlive Bun's 5 s default, which is the one-way rule broken for a number (#589). */ export interface TestOptions { /** * This test's own deadline in milliseconds, handed to `bun:test` at this machine's pace * (`paced`). Absent is the run's default, which `x verify` paces the same way. */ readonly timeoutMs?: number; } /** * One typed registrar. The body is `test`'s own (`FixtureBody`): it is handed the fixture bag and * only what it destructures is built, so `unitTest('…', async ({ runJobs }) => …)` is a job body * under the `unit` step with no `describe(testName('unit', …))` around a bare `test`. A body that * destructures nothing builds nothing — the wrapper costs it one resolved promise. */ const typed = (type: TestType) => (name: string, body: FixtureBody, options?: TestOptions): void => { test( testName(type, name), () => runWithFixtures(body), options?.timeoutMs === undefined ? undefined : paced(options.timeoutMs), ); }; /** Pure logic: no database, no clock, no network. The cheapest test that can fail for real. */ export const unitTest = typed('unit'); /** The published surface: OpenAPI diff against the committed spec, MCP exposure, error codes. */ export const contractTest = typed('contract'); /** Live queries: assert exactly what each subscriber receives, snapshot then incremental patch. */ export const liveTest = typed('live'); /** Jobs: step sequence, retries, idempotency. Never wall-clock sleeps — advance the frozen clock. */ export const jobTest = typed('job'); /** One element selection, resolved when it is used rather than when it is built. */ export interface LocatorLike { count(): Promise; click(): Promise; first(): LocatorLike; isVisible(): Promise; } /** * The browser surface an e2e test drives. Intended as the OBSERVED contract rather than a wish * list, and the driver that implements it (a browser, at milestone 11) is the only thing that may * add to it. * * This comment claimed every member was one the reference app's e2e suite already calls, and an * audit on 2026-08-24 found that false for three of eleven: `content()` had ZERO call sites * anywhere in the repository, and `title()` / `reload()` are named only by `x g route`'s generated * template — a string the CLI writes, which nothing executes. `content()` is deleted; the other * two keep their line WITH the caveat, because a generated template is a real contract the moment * a driver lands and a scaffolded app runs it. * * The claim was checkable and nobody checked it, which is the class this tree keeps re-shipping. * Nothing enforces this one either: `PageLike` has no driver, so an unwired member cannot fail — * see `hasE2eDriver()`. Do not restore the absolute wording without a rule that reads it. */ export interface PageLike { goto(url: string): Promise; /** The first flush of a streamed response — the shell, before the holes resolve. */ gotoStreamed(url: string): Promise<{ readonly html: string }>; /** Called only by `x g route`'s generated e2e template — no driver runs it yet. */ reload(): Promise; /** * Resolves once the service worker controls the page, so the offline assertions are not racy. A * page the worker cannot claim — the first of a run, committed before it activated — is reloaded * once (that navigation IS answered by the worker). `timeoutMs` raises this call's budget. */ waitForServiceWorker(options?: { readonly timeoutMs?: number | undefined }): Promise; /** Called only by `x g route`'s generated e2e template — no driver runs it yet. */ title(): Promise; url(): string; evaluate(fn: () => T): Promise; locator(selector: string): LocatorLike; getByRole( role: string, options?: { readonly name?: string; readonly level?: number }, ): LocatorLike; getByText(text: string): LocatorLike; } export interface E2eFixtures { readonly page: PageLike; /** Drop the network so the service worker has to answer. */ offline(): Promise; online(): Promise; /** Publish a new build id so the SW update path runs. */ update(): Promise; } export type E2eBody = (fixtures: E2eFixtures) => Promise; /** What registers one e2e test with a browser behind it. `options` is the test's own (`timeoutMs`). */ export type E2eDriver = (name: string, body: E2eBody, options?: TestOptions) => void; let e2eDriver: E2eDriver | undefined; /** * Register the browser-backed driver. Without one, `e2eTest` skips loudly — the skipped test's own * NAME carries the reason and the command that would build what it drives. * * The gate's rule over that skip: a step whose suite reports `ran === 0` is SKIPPED, unless the * repo's `x.verify.json` requires the step — then it is `X_VERIFY_SUITE_VANISHED` * (`packages/cli/src/verify-run.ts`). Never a pass either way. This block used to say the opposite * and argue for it — that an exit code was the only channel, so the gate could not tell — which * `packages/cli/src/test-counts.ts` had already disproved by reading `bun test`'s own counts (#434). * * `As of 2026-08` there are zero registered drivers, so every `e2eTest` in the tree is a skip; the * framework's own `e2e` suites use plain `bun:test`. */ export function useE2eDriver(driver: E2eDriver): void { e2eDriver = driver; } /** Whether a driver is registered. Exported so a harness can say which of the two states it is in * instead of reading an all-skipped run as a green one. */ export const hasE2eDriver = (): boolean => e2eDriver !== undefined; /** * Put the seam back. The counterpart `useE2eDriver` shipped without, and the module scope it * writes to is process-global: `bun test` shares one process across files, so a file that installs * a browser and does not undo it hands every later file an `e2eTest` that opens a page nobody * asked for. Same shape and same reason as `@ultimat3/scraping`'s `resetScrapeDriver()`. */ export const resetE2eDriver = (): void => { e2eDriver = undefined; }; export const e2eTest = (name: string, body: E2eBody, options?: TestOptions): void => { if (e2eDriver === undefined) { test.skip( testName('e2e', `${name} (no browser driver: x build --target static && x e2e)`), () => undefined, ); return; } e2eDriver(testName('e2e', name), body, options); }; export interface EvalCase { readonly name: string; readonly input: TInput; /** 0..1. Below the threshold fails the test. */ score(output: TOutput): number | Promise; } export interface EvalOptions extends TestOptions { readonly threshold: number; readonly cases: readonly EvalCase[]; run(input: TInput): Promise; } /** * LLM output scoring with a threshold. Deterministic by construction: the model call goes through * the sealed network, so an eval either uses a recorded response or declares its allowlist. */ export function evalTest( name: string, options: EvalOptions, ): void { test( testName('eval', name), async () => { const scores: { readonly name: string; readonly score: number }[] = []; for (const testCase of options.cases) { const output = await options.run(testCase.input); scores.push({ name: testCase.name, score: await testCase.score(output) }); } const failures = scores.filter((entry) => entry.score < options.threshold); if (failures.length > 0) { const detail = failures .map((entry) => `${entry.name}=${entry.score.toFixed(2)}`) .join(', '); throw new TestEvalThresholdError({ name, threshold: options.threshold, detail }); } }, options.timeoutMs === undefined ? undefined : paced(options.timeoutMs), ); } export interface OpenApiOperationLike { readonly operationId: string; readonly required?: readonly string[]; } export interface OpenApiLike { readonly operations: readonly OpenApiOperationLike[]; }