@luckystack/test-runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@luckystack/test-runner` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0]
11
+
12
+ ### Added
13
+
14
+ - Initial public release as part of the LuckyStack package split.
package/CLAUDE.md ADDED
@@ -0,0 +1,98 @@
1
+ # @luckystack/test-runner
2
+
3
+ > AI summary + function INDEX. For deep specs see `docs/` next to this file.
4
+
5
+ ## What this package does
6
+
7
+ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit de generated `apiMethodMap` en draait vijf progressive layers: contract smoke (Zod-valid input), auth enforcement, rate-limit, crash-resistance fuzz, en per-route custom tests (`<name>_v<N>.tests.ts` naast `_api/`/`_sync/` sources). Elke layer is independent runnable; `runAllTests` orchestreert alles in volgorde en levert een geaggregeerde summary op. Reporter/fixture/layer-extension registries laten consumers eigen checks plug-in zonder de framework te forken.
8
+
9
+ ## When to USE this package
10
+
11
+ - Je wil elke `_api/*` route automatisch valideren tegen de generated contract zonder per endpoint een testfile te schrijven.
12
+ - Je wil in CI verifiëren dat endpoints met `auth.login: true` daadwerkelijk 401 returneren voor unauthenticated requests.
13
+ - Je wil rate-limit gedrag (`rateLimit: N`) confirmeren zonder handmatig de `N+1`th request te bouwen.
14
+ - Je wil nightly crash-resistance fuzz draaien op alle endpoints om 500-stacks te vangen.
15
+ - Je wil custom test-layers, fixtures of een webhook reporter toevoegen aan de standard sweep.
16
+
17
+ ## When to NOT suggest this (yet)
18
+
19
+ - Voor unit tests van pure functions binnen `_api/*` — gebruik vitest/jest direct, niet deze runner.
20
+ - Voor frontend-UI tests — gebruik Playwright/`webapp-testing` skill, deze runner is server-only.
21
+ - Wanneer er geen generated `apiMethodMap` beschikbaar is (project gebruikt geen `@luckystack/devkit`).
22
+ - Wanneer de target server productie is — fuzz/rate-limit layers maken honderden requests en `/_test/reset` is daar uit.
23
+ - Voor sync-event testing via de auto-sweep — die walkt momenteel alleen API endpoints, niet de sync map (sync support voor de auto-layers staat op de roadmap maar is nog niet geleverd). Layer 5 (`runCustomTests` / `<name>_v<N>.tests.ts`) ondersteunt al wél sync routes via per-route testfiles naast `_sync/` sources.
24
+
25
+ ## Function Index
26
+
27
+ | Function / Export | 1-regel | Deep doc |
28
+ |---|---|---|
29
+ | `runAllTests({ apiMethodMap, apiMetaMap, apiInputSchemas, baseUrl, ... })` | Orchestrator: draait contract + auth + rate-limit + fuzz + custom (Layer 5) in volgorde, levert een `RunAllTestsSummary`. | → docs/contract-tests.md |
30
+ | `runCustomTests(input)` | Layer 5: draait per-route `<name>_v<N>.tests.ts` files naast de `_api/` / `_sync/` sources, levert een `RunCustomTestsSummary`. | → docs/contract-tests.md |
31
+ | `discoverCustomTestFiles(srcDir?)` | Walker die elke `<name>_v<N>.tests.ts` onder de configured src dir yields. | → docs/contract-tests.md |
32
+ | `logRunAllSummary(summary)` | Pretty-print één `RunAllTestsSummary`. | → docs/extension-hooks.md |
33
+ | `walkEndpoints(apiMethodMap)` | Genereert `EndpointDescriptor[]` uit de generated map | → docs/contract-tests.md |
34
+ | `runContractCheck({ endpoint, baseUrl, inputFor?, headers? })` | Single-endpoint contract smoke | → docs/contract-tests.md |
35
+ | `runContractTests({ apiMethodMap, baseUrl, skip?, inputFor?, headers?, onResult? })` | Full sweep contract layer | → docs/contract-tests.md |
36
+ | `runAuthEnforcementCheck({ endpoint, baseUrl, inputFor? })` | Single-endpoint auth check (verwacht 401 zonder token) | → docs/auth-tests.md |
37
+ | `runAuthEnforcementTests({ apiMethodMap, apiMetaMap, baseUrl, skip?, inputFor?, onResult? })` | Sweep alle `auth.login: true` endpoints | → docs/auth-tests.md |
38
+ | `runRateLimitCheck({ endpoint, baseUrl, rateLimit, inputFor?, headers? })` | Single-endpoint rate-limit assertie | → docs/rate-limit-tests.md |
39
+ | `runRateLimitTests({ apiMethodMap, apiMetaMap, baseUrl, skip?, inputFor?, headers?, maxRateLimitToTest?, resetBetweenEndpoints?, resetToken?, onResult? })` | Sweep alle `rateLimit: N` endpoints | → docs/rate-limit-tests.md |
40
+ | `runFuzzCheck({ endpoint, baseUrl, headers? })` | Single-endpoint crash-resistance fuzz | → docs/fuzz-tests.md |
41
+ | `runFuzzTests({ apiMethodMap, baseUrl, skip?, headers?, onResult? })` | Sweep fuzz layer (nightly CI) | → docs/fuzz-tests.md |
42
+ | `resetServerState({ baseUrl, token? })` | POST naar `/_test/reset` om DB+Redis schoon te maken | → docs/rate-limit-tests.md |
43
+ | `sampleSchemaInput(schema)` | Genereert Zod-valid sample payload uit een schema | → docs/contract-tests.md |
44
+ | `logContractResult(result)` | Pretty-print één `ContractCheckResult` | → docs/extension-hooks.md |
45
+ | `logContractSummary(summary)` | Pretty-print aggregate `RunContractSummary` | → docs/extension-hooks.md |
46
+ | `registerTestLayer(layer)` | Plug-in custom test-layer (CORS, business rules, multi-tenant) | → docs/extension-hooks.md |
47
+ | `listTestLayers()` | Lees alle geregistreerde custom layers | → docs/extension-hooks.md |
48
+ | `registerTestFixture(typeKey, fixture)` | Realistic-payload fixtures per typeKey (valid + invalid) | → docs/fuzz-tests.md |
49
+ | `getTestFixture(typeKey)` | Lees fixture voor een typeKey | → docs/fuzz-tests.md |
50
+ | `registerTestReporter(reporter)` | Plug-in onResult/onSummary/webhookUrl callbacks | → docs/extension-hooks.md |
51
+ | `getTestReporter()` | Lees actieve reporter callbacks | → docs/extension-hooks.md |
52
+ | `resetTestExtensionsForTests()` | Clear alle registries (alleen voor interne tests) | → docs/extension-hooks.md |
53
+ | Type: `EndpointDescriptor` | `{ page, name, version, method, fullPath }` | → docs/contract-tests.md |
54
+ | Type: `HttpMethod` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE'` | → docs/contract-tests.md |
55
+ | Type: `ContractCheckResult` | Single-result shape (status, httpStatus, responseStatus, errorCode, reason, durationMs) | → docs/contract-tests.md |
56
+ | Type: `RunContractSummary` | Aggregate shape (total, passed, failed, skipped, results[]) | → docs/contract-tests.md |
57
+ | Type: `TestLayer` / `TestLayerInput` / `TestLayerResult` | Custom-layer interface | → docs/extension-hooks.md |
58
+ | Type: `TestFixture` | `{ valid: T[], invalid: T[] }` per typeKey | → docs/fuzz-tests.md |
59
+ | Type: `TestResult` / `TestSummary` | Aggregate shapes voor extension registry | → docs/extension-hooks.md |
60
+ | Type: `TestReporter` | `{ onResult?, onSummary?, webhookUrl?, webhookAuth? }` | → docs/extension-hooks.md |
61
+ | Type: `RunContractTestsInput` / `ContractCheckInput` | Inputs voor contract layer | → docs/contract-tests.md |
62
+ | Type: `RunAuthEnforcementTestsInput` / `AuthEnforcementCheckInput` | Inputs voor auth layer | → docs/auth-tests.md |
63
+ | Type: `RunRateLimitTestsInput` / `RateLimitCheckInput` | Inputs voor rate-limit layer | → docs/rate-limit-tests.md |
64
+ | Type: `RunFuzzTestsInput` / `FuzzCheckInput` | Inputs voor fuzz layer | → docs/fuzz-tests.md |
65
+ | Type: `ResetServerStateInput` | Input voor `resetServerState` | → docs/rate-limit-tests.md |
66
+ | Type: `TestContext` | `{ callApi, callSync, watchStream, session, prisma, expect }` — handed naar elke Layer-5 custom-tests case. | → docs/contract-tests.md |
67
+ | `ctx.watchStream(roomCode)` | Open een second-socket (socket B) joined aan `roomCode` om de chunk stream van `broadcastStream`/`streamTo`/`_client_v{N}` te observeren. Returnt `StreamWatcher<TChunk>` met `.chunks[]`, `.stopAt(predicate, timeoutMs?)`, `.waitForCount(n, timeoutMs?)`, `.close()`. Watchers worden auto-closed na elke case. | → docs/contract-tests.md |
68
+ | `openStreamWatcher(input)` | Lower-level factory voor `watchStream` — direct bruikbaar buiten een `TestContext` (custom layers, ad-hoc harness scripts). | → docs/contract-tests.md |
69
+ | Type: `StreamWatcher<TChunk>` | `{ readonly chunks, stopAt, waitForCount, close }` — observable chunk stream voor één route. | → docs/contract-tests.md |
70
+ | Type: `StreamChunkFrame` | Wire-shape van een sync chunk frame (`{ cb, fullName, status: 'stream', ...payload }`). Default chunk type voor `StreamWatcher`. | → docs/contract-tests.md |
71
+ | Type: `OpenStreamWatcherInput` | `{ baseUrl, roomCode, token, routeFullName, defaultTimeoutMs? }` voor `openStreamWatcher`. | → docs/contract-tests.md |
72
+ | Type: `CustomTestCase` | `{ name, run(ctx) }` — shape die consumers exporteren uit `<name>_v<N>.tests.ts`. | → docs/contract-tests.md |
73
+ | Type: `RunAllTestsSummary` | Aggregate result shape voor `runAllTests` (per-layer summaries + totals). | → docs/contract-tests.md |
74
+ | Type: `RunCustomTestsSummary` | Aggregate result shape voor `runCustomTests` (per-file resultaten + totals). | → docs/contract-tests.md |
75
+
76
+ ## Config keys (env vars + register* slots)
77
+
78
+ - `NODE_ENV` (env, server-side) — `/_test/reset` is automatisch beschikbaar wanneer NIET `production`.
79
+ - `TEST_RESET_TOKEN` (env, optional, server-side) — wanneer gezet moet `resetServerState({ token })` deze meesturen als `X-Test-Reset-Token`. Verplicht voor staging/preview deploys die het endpoint over het netwerk exposen.
80
+ - `RunRateLimitTestsInput.maxRateLimitToTest` (default `50`) — endpoints met hogere `rateLimit` worden geskipt om duizenden requests in CI te vermijden.
81
+ - `RunRateLimitTestsInput.resetBetweenEndpoints` (default `false`) — hit `/_test/reset` voor elke endpoint zodat het shared IP-bucket schoon is.
82
+ - `registerTestLayer({ name, run })` — custom check per endpoint (CORS, business rules, custom auth-schemes, multi-tenant isolation).
83
+ - `registerTestFixture(typeKey, { valid, invalid })` — realistic payloads die de fuzz layer prefereert boven schema-random.
84
+ - `registerTestReporter({ onResult?, onSummary?, webhookUrl?, webhookAuth? })` — wire per-result/per-summary callbacks of POST de summary naar een webhook.
85
+
86
+ ## Peer dependencies
87
+
88
+ - **Required**: `zod@^4.0.0` (peer), `socket.io-client@^4.8.0` (peer — `streamWatcher.ts` opent een tweede socket voor `ctx.watchStream`), `@luckystack/core@^0.1.0` (dependency — leest `getApiMethodMapPath()` voor de default map-locatie).
89
+ - **Required server-side**: `@luckystack/server` met `/_test/reset` endpoint gemount (default ingebouwd, gated op `NODE_ENV !== 'production'`).
90
+ - **Required generated artefact**: `apiMethodMap.generated.ts` (en `apiMetaMap.generated.ts` voor de auth/rate-limit layers), geproduceerd door `@luckystack/devkit`.
91
+ - **Optional**: geen — fixtures, reporters en custom layers zijn pure-JS opt-in.
92
+
93
+ ## Related
94
+
95
+ - Architecture deep-dives: `/docs/ARCHITECTURE_API.md` (het contract dat deze runner asserteert), `/docs/ARCHITECTURE_PACKAGING.md` (hoe generated maps per preset worden geëmit).
96
+ - Consumer quickstart: `./README.md`.
97
+ - Devkit (genereert de maps die deze runner consumeert): `packages/devkit/CLAUDE.md`.
98
+ - Server (`/_test/reset` endpoint): `packages/server/CLAUDE.md`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mathijs van Melick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @luckystack/test-runner
2
+
3
+ > Generated-type-driven test layers for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Walks every endpoint in your `apiMethodMap.generated.ts` and runs four progressive test layers: contract smoke (Zod-valid input), auth enforcement, rate-limit, and crash-resistance fuzz.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev @luckystack/test-runner zod
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ Run all four layers against a running server:
14
+
15
+ ```ts
16
+ import {
17
+ runContractTests,
18
+ runAuthEnforcementTests,
19
+ runRateLimitTests,
20
+ runFuzzTests,
21
+ resetServerState,
22
+ logContractSummary,
23
+ } from '@luckystack/test-runner';
24
+ import { apiMethodMap } from './src/_sockets/apiMethodMap.generated';
25
+ import { apiMetaMap } from './src/_sockets/apiMetaMap.generated';
26
+
27
+ const baseUrl = process.env.TEST_BASE_URL ?? 'http://127.0.0.1:80';
28
+ const contract = await runContractTests({ apiMethodMap, baseUrl });
29
+ logContractSummary(contract);
30
+
31
+ const auth = await runAuthEnforcementTests({ apiMethodMap, apiMetaMap, baseUrl });
32
+ const rate = await runRateLimitTests({ apiMethodMap, apiMetaMap, baseUrl });
33
+ const fuzz = await runFuzzTests({ apiMethodMap, baseUrl });
34
+
35
+ await resetServerState({ baseUrl });
36
+ ```
37
+
38
+ Each layer is independently runnable — you can skip the ones that don't apply or rearrange them.
39
+
40
+ ## Layers
41
+
42
+ | Layer | What it asserts |
43
+ | --- | --- |
44
+ | **Contract** | A Zod-valid input payload reaches `main` and returns `{ status: 'success' }` (or a typed failure status). Catches schema/route mismatches. |
45
+ | **Auth enforcement** | Endpoints flagged `auth.login: true` reject unauthenticated requests with the framework's standard 401 shape. |
46
+ | **Rate limit** | Endpoints with `rateLimit: <N>` reject the `(N + 1)`th request inside the window. |
47
+ | **Fuzz** | Bad/random inputs never crash the server (still returns a typed error response, never 500 with a stack). |
48
+
49
+ The fuzz layer is intentionally exhaustive and slow — wire it into nightly CI rather than every PR.
50
+
51
+ ## Server hooks it depends on
52
+
53
+ - `/_test/reset` endpoint — served by `@luckystack/server` outside production (gated on `NODE_ENV !== 'production'` and an optional `TEST_RESET_TOKEN`). Used by `resetServerState` to clear DB + Redis between layers. Make sure `TEST_RESET_TOKEN` is set in any non-prod environment that is reachable over the network.
54
+ - `apiMethodMap.generated.ts` — produced by `@luckystack/devkit` from your `_api/*` files. Defaults are read via `getApiMethodMapPath()` from `@luckystack/core`.
55
+
56
+ ## Public API
57
+
58
+ | Export | Purpose |
59
+ | --- | --- |
60
+ | `walkEndpoints(apiMethodMap)` | Returns `EndpointDescriptor[]` from the generated map. |
61
+ | `runContractCheck(input)` / `runContractTests(input)` | Single endpoint / full sweep. |
62
+ | `runAuthEnforcementCheck(input)` / `runAuthEnforcementTests(input)` | Auth layer. |
63
+ | `runRateLimitCheck(input)` / `runRateLimitTests(input)` | Rate-limit layer. |
64
+ | `runFuzzCheck(input)` / `runFuzzTests(input)` | Fuzz layer. |
65
+ | `resetServerState(input)` | POST to `/_test/reset`. |
66
+ | `sampleSchemaInput(schema)` | Generate a Zod-valid sample payload. |
67
+ | `logContractResult` / `logContractSummary` | Pretty-printers. |
68
+
69
+ Types: `EndpointDescriptor`, `HttpMethod`, `ContractCheckResult`, `RunContractSummary`, plus per-layer input types.
70
+
71
+ ## Related architecture docs
72
+
73
+ - [`docs/ARCHITECTURE_API.md`](../../docs/ARCHITECTURE_API.md) — the contract this runner asserts against.
74
+ - [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — how generated maps are emitted per preset.
75
+
76
+ ## Dependencies
77
+
78
+ - Peer (canonical ranges, standardized 2026-05-07):
79
+ - `zod@^3.25.0`
80
+
81
+ ## License
82
+
83
+ MIT — see [LICENSE](../../LICENSE).
@@ -0,0 +1,433 @@
1
+ import { z, ZodType } from 'zod';
2
+ import { getPrismaClient } from '@luckystack/core';
3
+
4
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
5
+ interface EndpointDescriptor {
6
+ page: string;
7
+ name: string;
8
+ version: string;
9
+ method: HttpMethod;
10
+ /** `api/<page>/<name>/<version>` */
11
+ fullPath: string;
12
+ }
13
+ interface ContractCheckResult {
14
+ endpoint: EndpointDescriptor;
15
+ status: 'pass' | 'fail' | 'skipped';
16
+ httpStatus?: number;
17
+ responseStatus?: 'success' | 'error' | 'unknown';
18
+ errorCode?: string;
19
+ reason?: string;
20
+ durationMs: number;
21
+ }
22
+ interface RunContractSummary {
23
+ total: number;
24
+ passed: number;
25
+ failed: number;
26
+ skipped: number;
27
+ results: ContractCheckResult[];
28
+ }
29
+
30
+ type ApiMethodMap$6 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
31
+ declare const walkEndpoints: (apiMethodMap: ApiMethodMap$6) => EndpointDescriptor[];
32
+
33
+ interface ContractCheckInput {
34
+ endpoint: EndpointDescriptor;
35
+ baseUrl: string;
36
+ /**
37
+ * Optional input factory. Defaults to `{}`. When an endpoint requires a
38
+ * specific shape to not crash, callers can override per-endpoint here.
39
+ */
40
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
41
+ /**
42
+ * Optional headers (auth tokens, cookies). Applied to every request.
43
+ */
44
+ headers?: Record<string, string>;
45
+ /**
46
+ * Per-call request timeout in ms. Defaults to 5000. Bump for slow endpoints
47
+ * (AI calls, large reports) where the framework default would false-fail
48
+ * the contract check.
49
+ */
50
+ requestTimeoutMs?: number;
51
+ }
52
+ declare const runContractCheck: (input: ContractCheckInput) => Promise<ContractCheckResult>;
53
+
54
+ type ApiMethodMap$5 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
55
+ interface RunContractTestsInput {
56
+ apiMethodMap: ApiMethodMap$5;
57
+ baseUrl: string;
58
+ /**
59
+ * Endpoints to skip. Matched against `<page>/<name>` (version-agnostic) and
60
+ * `<page>/<name>/<version>` (version-specific). Useful for endpoints that
61
+ * are known to need real input (e.g. a file upload).
62
+ */
63
+ skip?: string[];
64
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
65
+ headers?: Record<string, string>;
66
+ onResult?: (result: ContractCheckResult) => void;
67
+ }
68
+ declare const runContractTests: (input: RunContractTestsInput) => Promise<RunContractSummary>;
69
+
70
+ interface AuthEnforcementCheckInput {
71
+ endpoint: EndpointDescriptor;
72
+ baseUrl: string;
73
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
74
+ /** Per-call request timeout in ms. Defaults to 5000. */
75
+ requestTimeoutMs?: number;
76
+ }
77
+ declare const runAuthEnforcementCheck: (input: AuthEnforcementCheckInput) => Promise<ContractCheckResult>;
78
+
79
+ type ApiMethodMap$4 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
80
+ type ApiMetaMap$2 = Partial<Record<string, Partial<Record<string, Partial<Record<string, {
81
+ method: string;
82
+ auth: {
83
+ login: boolean;
84
+ additional?: Record<string, unknown>[];
85
+ };
86
+ rateLimit?: number | false;
87
+ }>>>>>>;
88
+ interface RunAuthEnforcementTestsInput {
89
+ apiMethodMap: ApiMethodMap$4;
90
+ apiMetaMap: ApiMetaMap$2;
91
+ baseUrl: string;
92
+ skip?: string[];
93
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
94
+ onResult?: (result: ContractCheckResult) => void;
95
+ }
96
+ declare const runAuthEnforcementTests: (input: RunAuthEnforcementTestsInput) => Promise<RunContractSummary>;
97
+
98
+ interface RateLimitCheckInput {
99
+ endpoint: EndpointDescriptor;
100
+ baseUrl: string;
101
+ rateLimit: number;
102
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
103
+ headers?: Record<string, string>;
104
+ /** Per-call request timeout in ms. Defaults to 10000. */
105
+ requestTimeoutMs?: number;
106
+ }
107
+ declare const runRateLimitCheck: (input: RateLimitCheckInput) => Promise<ContractCheckResult>;
108
+
109
+ type ApiMethodMap$3 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
110
+ type ApiMetaMap$1 = Partial<Record<string, Partial<Record<string, Partial<Record<string, {
111
+ method: string;
112
+ auth: {
113
+ login: boolean;
114
+ additional?: Record<string, unknown>[];
115
+ };
116
+ rateLimit?: number | false;
117
+ }>>>>>>;
118
+ interface RunRateLimitTestsInput {
119
+ apiMethodMap: ApiMethodMap$3;
120
+ apiMetaMap: ApiMetaMap$1;
121
+ baseUrl: string;
122
+ skip?: string[];
123
+ inputFor?: (endpoint: EndpointDescriptor) => unknown;
124
+ headers?: Record<string, string>;
125
+ /**
126
+ * Max rateLimit value to test. Endpoints with a higher limit are skipped
127
+ * to avoid firing thousands of requests in CI.
128
+ */
129
+ maxRateLimitToTest?: number;
130
+ /**
131
+ * When true, hit `/_test/reset` before each endpoint so limiter state is
132
+ * clean. Requires the server to allow the endpoint (NODE_ENV !== production
133
+ * or `TEST_RESET_TOKEN` configured). Without resetting, the shared IP bucket
134
+ * leaks state between endpoints.
135
+ */
136
+ resetBetweenEndpoints?: boolean;
137
+ /**
138
+ * Token forwarded as `X-Test-Reset-Token` when resetting. Only relevant in
139
+ * staging/preview deploys that enable the endpoint behind a token.
140
+ */
141
+ resetToken?: string;
142
+ onResult?: (result: ContractCheckResult) => void;
143
+ }
144
+ declare const runRateLimitTests: (input: RunRateLimitTestsInput) => Promise<RunContractSummary>;
145
+
146
+ interface FuzzCheckInput {
147
+ endpoint: EndpointDescriptor;
148
+ baseUrl: string;
149
+ headers?: Record<string, string>;
150
+ /** Per-call request timeout in ms. Defaults to 5000. */
151
+ requestTimeoutMs?: number;
152
+ }
153
+ declare const runFuzzCheck: (input: FuzzCheckInput) => Promise<ContractCheckResult>;
154
+
155
+ type ApiMethodMap$2 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
156
+ interface RunFuzzTestsInput {
157
+ apiMethodMap: ApiMethodMap$2;
158
+ baseUrl: string;
159
+ skip?: string[];
160
+ headers?: Record<string, string>;
161
+ onResult?: (result: ContractCheckResult) => void;
162
+ }
163
+ declare const runFuzzTests: (input: RunFuzzTestsInput) => Promise<RunContractSummary>;
164
+
165
+ interface ResetServerStateInput {
166
+ baseUrl: string;
167
+ token?: string;
168
+ }
169
+ declare const resetServerState: ({ baseUrl, token }: ResetServerStateInput) => Promise<boolean>;
170
+
171
+ declare const sampleSchemaInput: (schema: z.ZodType) => unknown;
172
+
173
+ declare const logContractResult: (result: ContractCheckResult) => void;
174
+ declare const logContractSummary: (summary: RunContractSummary) => void;
175
+
176
+ interface StreamChunkFrame {
177
+ /** Per-request correlation id stamped by the server. */
178
+ cb?: string;
179
+ /** Full route name (`sync/<page>/<name>/<version>`). */
180
+ fullName?: string;
181
+ /** Always `'stream'` for chunk frames. */
182
+ status?: 'stream';
183
+ /** Payload fields spread on top of the envelope. */
184
+ [key: string]: unknown;
185
+ }
186
+ interface StreamWatcher<TChunk extends StreamChunkFrame = StreamChunkFrame> {
187
+ /** Chunks received so far. Array reference is stable; entries are pushed in arrival order. */
188
+ readonly chunks: TChunk[];
189
+ /** Wait until `predicate(chunks)` returns true, or until timeout (default 5000ms). */
190
+ stopAt(predicate: (chunks: TChunk[]) => boolean, timeoutMs?: number): Promise<void>;
191
+ /** Wait until N total chunks received, or until timeout. */
192
+ waitForCount(n: number, timeoutMs?: number): Promise<void>;
193
+ /** Close socket B. Tests SHOULD call this in cleanup; ctx auto-closes all watchers when the test ends. */
194
+ close(): Promise<void>;
195
+ }
196
+ interface OpenStreamWatcherInput {
197
+ /** Server URL (same as `ctx`'s `baseUrl`). */
198
+ baseUrl: string;
199
+ /** Room to join as a pure subscriber. */
200
+ roomCode: string;
201
+ /** Active session token (used for `joinRoom` auth). May be null for routes that don't require auth. */
202
+ token: string | null;
203
+ /**
204
+ * Resolved route name `sync/<page>/<name>/<version>` — chunk frames are
205
+ * filtered by their `fullName` property so a watcher only surfaces chunks
206
+ * for the route its test is bound to.
207
+ */
208
+ routeFullName: string;
209
+ /** Default 5000ms — overridable per `stopAt` / `waitForCount` call. */
210
+ defaultTimeoutMs?: number;
211
+ }
212
+ declare const openStreamWatcher: <TChunk extends StreamChunkFrame = StreamChunkFrame>(input: OpenStreamWatcherInput) => Promise<StreamWatcher<TChunk>>;
213
+
214
+ type ApiMethodMap$1 = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
215
+ interface CustomTestCase {
216
+ name: string;
217
+ run: (ctx: TestContext) => Promise<void>;
218
+ /**
219
+ * Mark this case as a KNOWN, accepted failure (e.g. a documented bug not
220
+ * yet fixed, or a scenario the current implementation cannot satisfy). The
221
+ * string is the reason shown in the report. A case marked `expectedToFail`
222
+ * that throws is reported as `xfail` — counted separately, NOT a red
223
+ * failure. If it unexpectedly PASSES it is reported as `xpass` so the stale
224
+ * marker can be removed. Leave undefined for normal cases.
225
+ */
226
+ expectedToFail?: string;
227
+ }
228
+ interface CustomTestResult {
229
+ routePath: string;
230
+ caseName: string;
231
+ status: 'pass' | 'fail' | 'xfail' | 'xpass';
232
+ durationMs: number;
233
+ reason?: string;
234
+ /** Documented reason when the case carried an `expectedToFail` marker. */
235
+ expectedToFail?: string;
236
+ }
237
+ interface RunCustomTestsSummary {
238
+ total: number;
239
+ passed: number;
240
+ failed: number;
241
+ /** Cases marked `expectedToFail` that failed as expected (known issues). */
242
+ xfailed: number;
243
+ /** Cases marked `expectedToFail` that unexpectedly passed (remove the marker). */
244
+ xpassed: number;
245
+ results: CustomTestResult[];
246
+ }
247
+ interface RunCustomTestsInput {
248
+ baseUrl: string;
249
+ /** Optional cookie name for the session token. Defaults to the project's configured cookie. */
250
+ sessionCookieName?: string;
251
+ /** Filter to a subset of routes — substring match against `<page>/<name>/<version>`. */
252
+ filter?: string;
253
+ /**
254
+ * Generated `apiMethodMap` so the harness sends each API route its declared
255
+ * HTTP method (e.g. logout is DELETE). Falls back to POST when absent.
256
+ */
257
+ apiMethodMap?: ApiMethodMap$1;
258
+ onResult?: (result: CustomTestResult) => void;
259
+ }
260
+ interface DiscoveredTestFile {
261
+ /** Absolute path to the test file. */
262
+ filePath: string;
263
+ /** `api` or `sync` — which transport this route uses. */
264
+ kind: 'api' | 'sync';
265
+ /** Route descriptor parsed from the filename + directory location. */
266
+ route: {
267
+ page: string;
268
+ name: string;
269
+ version: string;
270
+ };
271
+ }
272
+ declare const discoverCustomTestFiles: (srcDir?: string) => DiscoveredTestFile[];
273
+ interface TestContext {
274
+ /** Invoke the route under test. Page / name / version are baked in. */
275
+ callApi: <TInput = unknown, TOutput = unknown>(input: TInput) => Promise<TOutput>;
276
+ callSync: <TInput = unknown, TOutput = unknown>(input: TInput, opts?: {
277
+ receiver?: string;
278
+ }) => Promise<TOutput>;
279
+ /**
280
+ * Open a second socket connection that joins `roomCode` as a pure
281
+ * subscriber and exposes the incoming chunk stream for assertions.
282
+ *
283
+ * The route is auto-derived from the test file location, so a watcher
284
+ * only surfaces chunks for the route this test belongs to. Multiple
285
+ * watchers per test are allowed; all are auto-closed when the test
286
+ * case finishes (whether it passes or throws).
287
+ */
288
+ watchStream: <TChunk extends StreamChunkFrame = StreamChunkFrame>(roomCode: string) => Promise<StreamWatcher<TChunk>>;
289
+ session: {
290
+ login: (user?: {
291
+ email?: string;
292
+ id?: string;
293
+ name?: string;
294
+ }) => Promise<{
295
+ token: string;
296
+ userId: string;
297
+ }>;
298
+ logout: () => Promise<void>;
299
+ current: () => {
300
+ token: string | null;
301
+ userId: string | null;
302
+ };
303
+ };
304
+ prisma: ReturnType<typeof getPrismaClient>;
305
+ expect: TestExpect;
306
+ }
307
+ interface TestExpect {
308
+ eq: <T>(actual: T, expected: T, message?: string) => void;
309
+ ok: (value: unknown, message?: string) => void;
310
+ throws: (fn: () => Promise<unknown>, message?: string) => Promise<Error>;
311
+ matches: (value: string, pattern: RegExp, message?: string) => void;
312
+ }
313
+ declare const runCustomTests: (input: RunCustomTestsInput) => Promise<RunCustomTestsSummary>;
314
+
315
+ type ApiMethodMap = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;
316
+ type ApiMetaMap = Partial<Record<string, Partial<Record<string, Partial<Record<string, {
317
+ method: string;
318
+ auth: {
319
+ login: boolean;
320
+ additional?: Record<string, unknown>[];
321
+ };
322
+ rateLimit?: number | false;
323
+ }>>>>>>;
324
+ type ApiInputSchemas = Partial<Record<string, Partial<Record<string, Partial<Record<string, ZodType | undefined>>>>>>;
325
+ interface RunAllTestsInput {
326
+ apiMethodMap: ApiMethodMap;
327
+ apiMetaMap: ApiMetaMap;
328
+ apiInputSchemas: ApiInputSchemas;
329
+ baseUrl: string;
330
+ /** Cookie name for the session token. Defaults to `luckystack_token`. */
331
+ sessionCookieName?: string;
332
+ /** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */
333
+ authToken?: string;
334
+ /** Endpoints to skip from the sweep layers, e.g. routes that need a file upload. */
335
+ skip?: string[];
336
+ /** Substring filter applied to `<page>/<name>/<version>`. */
337
+ filter?: string;
338
+ /** Disable individual layers (faster local iteration). */
339
+ noSweep?: boolean;
340
+ noFuzz?: boolean;
341
+ noRateLimit?: boolean;
342
+ noCustom?: boolean;
343
+ }
344
+ interface RunAllTestsSummary {
345
+ contract?: RunContractSummary;
346
+ auth?: RunContractSummary;
347
+ rateLimit?: RunContractSummary;
348
+ fuzz?: RunContractSummary;
349
+ custom?: RunCustomTestsSummary;
350
+ totalPassed: number;
351
+ totalFailed: number;
352
+ }
353
+ declare const runAllTests: (input: RunAllTestsInput) => Promise<RunAllTestsSummary>;
354
+ declare const logRunAllSummary: (summary: RunAllTestsSummary) => void;
355
+
356
+ interface TestLayerInput {
357
+ /** Resolved route name (`api/billing/getInvoice/v1`). */
358
+ endpoint: string;
359
+ /** Method when applicable, undefined for sync endpoints. */
360
+ method?: string;
361
+ /** Bearer token / session token to use for auth-aware checks. */
362
+ authToken?: string;
363
+ }
364
+ interface TestLayerResult {
365
+ passed: boolean;
366
+ /** Short reason key — i18n-style errorCode when failed. */
367
+ message?: string;
368
+ /** Optional extras for the reporter (latency, response sample, ...). */
369
+ metadata?: Record<string, unknown>;
370
+ }
371
+ interface TestLayer {
372
+ name: string;
373
+ run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;
374
+ }
375
+ interface TestFixture<TPayload = unknown> {
376
+ valid: TPayload[];
377
+ invalid: TPayload[];
378
+ }
379
+ interface TestResult {
380
+ layer: string;
381
+ endpoint: string;
382
+ passed: boolean;
383
+ message?: string;
384
+ durationMs?: number;
385
+ metadata?: Record<string, unknown>;
386
+ }
387
+ interface TestSummary {
388
+ totalLayers: number;
389
+ totalEndpoints: number;
390
+ passed: number;
391
+ failed: number;
392
+ results: TestResult[];
393
+ }
394
+ interface TestReporter {
395
+ /** Called once per individual TestResult as the runner progresses. */
396
+ onResult?: (result: TestResult) => void | Promise<void>;
397
+ /** Called once at the end with the aggregated summary. */
398
+ onSummary?: (summary: TestSummary) => void | Promise<void>;
399
+ /** Optional webhook the runner POSTs the summary to. */
400
+ webhookUrl?: string;
401
+ webhookAuth?: {
402
+ type: 'bearer';
403
+ token: string;
404
+ };
405
+ }
406
+ /**
407
+ * Register a custom test layer. Re-registering the same name replaces
408
+ * the previous entry. Use for CORS enforcement, business-rule checks,
409
+ * custom auth schemes, multi-tenant isolation, GDPR data-flow checks.
410
+ */
411
+ declare const registerTestLayer: (layer: TestLayer) => void;
412
+ declare const listTestLayers: () => TestLayer[];
413
+ /**
414
+ * Register a realistic payload fixture for a specific type key. The fuzz
415
+ * layer prefers these over schema-random generated inputs when a fixture
416
+ * exists for the endpoint's input type.
417
+ *
418
+ * `valid` payloads are used to verify happy-path behavior; `invalid`
419
+ * payloads probe the validator's error responses.
420
+ */
421
+ declare const registerTestFixture: <TPayload = unknown>(typeKey: string, fixture: TestFixture<TPayload>) => void;
422
+ declare const getTestFixture: (typeKey: string) => TestFixture | undefined;
423
+ /**
424
+ * Register a test reporter. Set `null` to unregister. The runner calls
425
+ * `onResult` per individual result as the run progresses and `onSummary`
426
+ * once at the end. When `webhookUrl` is set, the runner additionally
427
+ * POSTs the JSON-serialised summary to that URL.
428
+ */
429
+ declare const registerTestReporter: (newReporter: TestReporter | null) => void;
430
+ declare const getTestReporter: () => TestReporter | null;
431
+ declare const resetTestExtensionsForTests: () => void;
432
+
433
+ export { type AuthEnforcementCheckInput, type ContractCheckInput, type ContractCheckResult, type CustomTestCase, type CustomTestResult, type EndpointDescriptor, type FuzzCheckInput, type HttpMethod, type OpenStreamWatcherInput, type RateLimitCheckInput, type ResetServerStateInput, type RunAllTestsInput, type RunAllTestsSummary, type RunAuthEnforcementTestsInput, type RunContractSummary, type RunContractTestsInput, type RunCustomTestsInput, type RunCustomTestsSummary, type RunFuzzTestsInput, type RunRateLimitTestsInput, type StreamChunkFrame, type StreamWatcher, type TestContext, type TestExpect, type TestFixture, type TestLayer, type TestLayerInput, type TestLayerResult, type TestReporter, type TestResult, type TestSummary, discoverCustomTestFiles, getTestFixture, getTestReporter, listTestLayers, logContractResult, logContractSummary, logRunAllSummary, openStreamWatcher, registerTestFixture, registerTestLayer, registerTestReporter, resetServerState, resetTestExtensionsForTests, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, sampleSchemaInput, walkEndpoints };