@luckystack/test-runner 0.2.4 → 0.2.6
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/dist/index.js +36 -16
- package/dist/index.js.map +1 -1
- package/docs/fuzz-tests.md +2 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -347,16 +347,14 @@ var runRateLimitCheck = async (input) => {
|
|
|
347
347
|
for (let i = 0; i < rateLimit; i += 1) {
|
|
348
348
|
const drained = await send();
|
|
349
349
|
if (!drained) continue;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
};
|
|
359
|
-
}
|
|
350
|
+
const errorCode = drained.parsed?.errorCode;
|
|
351
|
+
if (errorCode && PRE_LIMITER_ERROR_CODES.has(errorCode)) {
|
|
352
|
+
return {
|
|
353
|
+
endpoint,
|
|
354
|
+
status: "skipped",
|
|
355
|
+
durationMs: Date.now() - started,
|
|
356
|
+
reason: `drain request ${i + 1} rejected pre-limiter (${errorCode}); bucket was never incremented \u2014 check CSRF/auth headers`
|
|
357
|
+
};
|
|
360
358
|
}
|
|
361
359
|
}
|
|
362
360
|
const final = await send();
|
|
@@ -843,6 +841,7 @@ var logContractSummary = (summary) => {
|
|
|
843
841
|
};
|
|
844
842
|
|
|
845
843
|
// src/customTests.ts
|
|
844
|
+
import crypto from "crypto";
|
|
846
845
|
import fs from "fs";
|
|
847
846
|
import path from "path";
|
|
848
847
|
import { pathToFileURL } from "url";
|
|
@@ -1084,10 +1083,10 @@ var buildSessionHelpers = (state) => {
|
|
|
1084
1083
|
return {
|
|
1085
1084
|
login: async (user) => {
|
|
1086
1085
|
const { saveSession, getSession } = await import("@luckystack/login");
|
|
1087
|
-
const userId = user?.id ?? `test-user-${Date.now()}-${
|
|
1088
|
-
const email = user?.email ?? `test-${Date.now()}-${
|
|
1086
|
+
const userId = user?.id ?? `test-user-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
|
|
1087
|
+
const email = user?.email ?? `test-${Date.now()}-${crypto.randomBytes(8).toString("hex")}@example.com`;
|
|
1089
1088
|
const name = user?.name ?? "Test User";
|
|
1090
|
-
const token = `test-${Date.now()}-${
|
|
1089
|
+
const token = `test-${Date.now()}-${crypto.randomBytes(16).toString("hex")}`;
|
|
1091
1090
|
const sessionData = {
|
|
1092
1091
|
id: userId,
|
|
1093
1092
|
email,
|
|
@@ -1212,13 +1211,29 @@ var buildContext = (discovery, input) => {
|
|
|
1212
1211
|
return { ctx, closeAllWatchers, state };
|
|
1213
1212
|
};
|
|
1214
1213
|
var runCustomTests = async (input) => {
|
|
1215
|
-
const
|
|
1214
|
+
const srcDir = getSrcDir();
|
|
1215
|
+
const files = discoverCustomTestFiles(srcDir);
|
|
1216
1216
|
const results = [];
|
|
1217
1217
|
for (const discovery of files) {
|
|
1218
1218
|
const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
|
|
1219
1219
|
if (input.filter && !routePath.includes(input.filter)) continue;
|
|
1220
|
+
const [realPathErr, realFilePath] = tryCatchSync(() => fs.realpathSync(discovery.filePath));
|
|
1221
|
+
const resolvedPath = realPathErr ? discovery.filePath : realFilePath ?? discovery.filePath;
|
|
1222
|
+
const relToSrc = path.relative(srcDir, resolvedPath);
|
|
1223
|
+
if (relToSrc.startsWith("..") || path.isAbsolute(relToSrc)) {
|
|
1224
|
+
const r = {
|
|
1225
|
+
routePath,
|
|
1226
|
+
caseName: "(import)",
|
|
1227
|
+
status: "fail",
|
|
1228
|
+
durationMs: 0,
|
|
1229
|
+
reason: `test file path escapes srcDir and was rejected: ${discovery.filePath}`
|
|
1230
|
+
};
|
|
1231
|
+
results.push(r);
|
|
1232
|
+
input.onResult?.(r);
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1220
1235
|
const [importError, mod] = await tryCatch4(
|
|
1221
|
-
() => import(pathToFileURL(
|
|
1236
|
+
() => import(pathToFileURL(resolvedPath).href)
|
|
1222
1237
|
);
|
|
1223
1238
|
if (importError || !mod) {
|
|
1224
1239
|
const r = {
|
|
@@ -1307,7 +1322,12 @@ var buildAuthHeaders = async (input) => {
|
|
|
1307
1322
|
if (session?.csrfToken) {
|
|
1308
1323
|
headers[getCsrfConfig2().headerName] = session.csrfToken;
|
|
1309
1324
|
}
|
|
1310
|
-
} catch {
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
console.warn(
|
|
1327
|
+
"[test-runner] buildAuthHeaders: could not resolve CSRF token \u2014",
|
|
1328
|
+
error instanceof Error ? error.message : String(error),
|
|
1329
|
+
"\u2014 sweep will run without the CSRF header (degraded mode)."
|
|
1330
|
+
);
|
|
1311
1331
|
}
|
|
1312
1332
|
return headers;
|
|
1313
1333
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/walkEndpoints.ts","../src/probeRequest.ts","../src/contractCheck.ts","../src/testLayerHelpers.ts","../src/runContractTests.ts","../src/authEnforcementCheck.ts","../src/runAuthEnforcementTests.ts","../src/rateLimitCheck.ts","../src/resetServerState.ts","../src/runRateLimitTests.ts","../src/csrfEnforcementCheck.ts","../src/runCsrfEnforcementTests.ts","../src/fuzzCheck.ts","../src/runFuzzTests.ts","../src/schemaSampleInput.ts","../src/reporter.ts","../src/customTests.ts","../src/streamWatcher.ts","../src/runAllTests.ts","../src/extensionRegistry.ts"],"sourcesContent":["import type { EndpointDescriptor, HttpMethod, SyncMethodMap } from './types';\n\ntype ApiMethodMap = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;\n\n//? Must stay in sync with the `HttpMethod` union in types.ts. Only values that\n//? appear in both sets are valid probes — anything else is an unrecognised method.\nconst VALID_HTTP_METHODS = new Set<HttpMethod>(['GET', 'POST', 'PUT', 'DELETE']);\n\nconst toHttpMethod = (raw: string): HttpMethod | null => {\n //? Guard against a stale or hand-edited generated map carrying an unrecognised\n //? method string. An unknown value would propagate silently as a typed HttpMethod\n //? and break every probe that uses it. Return null so the caller can skip it.\n const upper = raw.toUpperCase();\n for (const m of VALID_HTTP_METHODS) {\n if (m === upper) return m;\n }\n return null;\n};\n\nexport const walkEndpoints = (apiMethodMap: ApiMethodMap): EndpointDescriptor[] => {\n const endpoints: EndpointDescriptor[] = [];\n for (const [page, nameMap] of Object.entries(apiMethodMap)) {\n if (!nameMap) continue;\n for (const [name, versionMap] of Object.entries(nameMap)) {\n if (!versionMap) continue;\n for (const [version, rawMethod] of Object.entries(versionMap)) {\n if (rawMethod === undefined) continue;\n const method = toHttpMethod(rawMethod);\n if (!method) {\n //? Unrecognised method in the generated map — log and skip rather than\n //? silently coercing, which would produce invalid probes.\n console.warn(`[test-runner] walkEndpoints: skipping api/${page}/${name}/${version} — unrecognised method '${rawMethod}' in apiMethodMap`);\n continue;\n }\n endpoints.push({\n page,\n name,\n version,\n method,\n fullPath: `api/${page}/${name}/${version}`,\n });\n }\n }\n }\n return endpoints;\n};\n\n//? Walk the generated `syncMethodMap` and produce a flat list of sync-route\n//? descriptors. Each descriptor's `fullPath` is `sync/<page>/<name>/<version>`.\n//? Sync routes always use POST over the HTTP-fallback transport regardless of\n//? the declared method, so the method is hardcoded to `POST` here.\nexport const walkSyncEndpoints = (syncMethodMap: SyncMethodMap): EndpointDescriptor[] => {\n const endpoints: EndpointDescriptor[] = [];\n for (const [page, nameMap] of Object.entries(syncMethodMap)) {\n if (!nameMap) continue;\n for (const [name, versionMap] of Object.entries(nameMap)) {\n if (!versionMap) continue;\n for (const [version, method] of Object.entries(versionMap)) {\n if (method === undefined) continue;\n endpoints.push({\n page,\n name,\n version,\n method: 'POST',\n fullPath: `sync/${page}/${name}/${version}`,\n });\n }\n }\n }\n return endpoints;\n};\n","//? Single-source fetch probe used by every check layer (contract, auth,\n//? csrf, rate-limit). Centralises the AbortController + timeout + tryCatch\n//? pattern so it isn't copied into each check file.\n\nimport { tryCatch } from '@luckystack/core';\nimport type { HttpMethod } from './types';\n\nexport interface ProbeRequestInput {\n url: string;\n method: HttpMethod;\n baseUrl: string;\n body?: unknown;\n headers?: Record<string, string>;\n requestTimeoutMs: number;\n}\n\nexport interface ProbeResponse {\n httpStatus: number;\n parsed: { status?: string; errorCode?: string } | null;\n}\n\n/**\n * Send a single timed fetch probe and parse the JSON envelope. Returns\n * `null` when the request itself fails (network error or abort).\n */\nexport const sendProbe = async (input: ProbeRequestInput): Promise<ProbeResponse | null> => {\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, input.requestTimeoutMs);\n\n const [fetchError, response] = await tryCatch(() => fetch(input.url, {\n method: input.method,\n headers: {\n 'Content-Type': 'application/json',\n //? Browsers attach Origin on state-changing requests; the server's origin\n //? policy fail-closes POST/PUT/DELETE without one.\n 'Origin': new URL(input.baseUrl).origin,\n ...input.headers,\n },\n body: input.method === 'GET' ? undefined : JSON.stringify(input.body),\n signal: controller.signal,\n }));\n clearTimeout(timeoutHandle);\n\n if (fetchError || !response) return null;\n\n const [, parsed] = await tryCatch<{ status?: string; errorCode?: string } | null, undefined>(\n async () => await response.json() as { status?: string; errorCode?: string },\n );\n\n return { httpStatus: response.status, parsed: parsed ?? null };\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\nexport interface ContractCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n /**\n * Optional input factory. Defaults to `{}`. When an endpoint requires a\n * specific shape to not crash, callers can override per-endpoint here.\n */\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /**\n * Optional headers (auth tokens, cookies). Applied to every request.\n */\n headers?: Record<string, string>;\n /**\n * Per-call request timeout in ms. Defaults to 5000. Bump for slow endpoints\n * (AI calls, large reports) where the framework default would false-fail\n * the contract check.\n */\n requestTimeoutMs?: number;\n}\n\n//? Contract layer: send any-shape input, accept any response where\n//? `status` is `success` or `error` (with an `errorCode`). Anything else is\n//? a contract violation — 5xx, thrown exceptions, or shapes outside this\n//? envelope.\n//?\n//? The goal here is NOT to prove every endpoint works end-to-end; it's to\n//? catch endpoints that crash the server or return garbage on well-formed\n//? empty input. Real behavior tests go in the per-endpoint test file.\nexport const runContractCheck = async (input: ContractCheckInput): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: input.headers,\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return {\n endpoint,\n status: 'fail',\n reason: 'fetch returned no response',\n durationMs: Date.now() - started,\n };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (!parsed) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'JSON parse failed',\n durationMs,\n };\n }\n\n if (parsed.status !== 'success' && parsed.status !== 'error') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'Response missing standard `status` envelope',\n durationMs,\n };\n }\n\n if (parsed.status === 'error' && !parsed.errorCode) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'error',\n reason: 'Error response missing `errorCode`',\n durationMs,\n };\n }\n\n //? When `inputFor` produced a non-empty sample body but the route returned an\n //? error, the probe may have failed its own Zod validation rather than proving\n //? the contract. Counting that as a PASS is misleading — the happy-path was\n //? never exercised. Classify as `skipped` so the caller knows the body was\n //? rejected before the handler ran and can supply a better sample.\n const bodyIsNonEmpty = body !== null\n && typeof body === 'object'\n && !Array.isArray(body)\n && Object.keys(body).length > 0;\n if (input.inputFor && bodyIsNonEmpty && parsed.status === 'error') {\n return {\n endpoint,\n status: 'skipped',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n reason: `inputFor produced a non-empty sample but route returned error (${parsed.errorCode ?? 'unknown'}); provide a valid sample via inputFor to test the happy path`,\n durationMs,\n };\n }\n\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: parsed.status,\n errorCode: parsed.errorCode,\n durationMs,\n };\n};\n","//? Shared helpers for the sweep test layers (contract, auth-enforcement,\n//? rate-limit, fuzz). Centralizes the skip-matching, meta-map queries, and\n//? summary-count math that were previously copy-pasted across each layer file.\n\nimport type {\n ApiMetaMap,\n ContractCheckResult,\n EndpointDescriptor,\n HttpMethod,\n RunContractSummary,\n} from './types';\n\n//? Single source of truth for the canonical layer names used in both the\n//? orchestrator (`runAllTests`) and the reporter (`logRunAllSummary`). Keeping\n//? them here prevents silent drift between the reporting labels and the summary\n//? property keys when layers are added or renamed.\nexport const LAYER_KEYS = {\n contract: 'contract',\n auth: 'auth-enforcement',\n rateLimit: 'rate-limit',\n csrf: 'csrf-enforcement',\n fuzz: 'fuzz',\n custom: 'custom',\n} as const;\n\n//? HTTP methods that carry a body and mutate server state. The CSRF middleware\n//? enforces a token only on these. Fuzz / rate-limit layers share this set to\n//? avoid firing junk bodies at mutating authenticated routes in cookie-mode.\nexport const STATE_CHANGING_METHODS = new Set<HttpMethod>(['POST', 'PUT', 'DELETE']);\n\n/**\n * Whether an endpoint is in the explicit skip list. Matched against\n * `<page>/<name>` (version-agnostic) and `<page>/<name>/<version>`\n * (version-specific).\n */\nexport const shouldSkip = (endpoint: EndpointDescriptor, skip: string[]): boolean => {\n if (skip.length === 0) return false;\n const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;\n const versionless = `${endpoint.page}/${endpoint.name}`;\n return skip.includes(versioned) || skip.includes(versionless);\n};\n\n/** Whether the endpoint's meta entry declares `auth.login: true`. */\nexport const requiresLogin = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): boolean => {\n const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n //? `apiMetaMap` is a consumer-generated RUNTIME artifact — the static type\n //? declares `auth` as required, but a stale/partial entry can be missing it.\n //? Read defensively so a malformed entry surfaces as `false` (no login) and\n //? gets handled by the layer's skip path, instead of throwing a TypeError\n //? outside the per-endpoint tryCatch and aborting the whole layer.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime-defensive read of a generated artifact\n return meta?.auth?.login ?? false;\n};\n\n/** Whether the meta map carries an entry for this endpoint at all. */\nexport const hasMetaEntry = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): boolean =>\n apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version] !== undefined;\n\n/**\n * The endpoint's declared numeric rate limit, or `null` when it has none\n * (`false`/`undefined`) or no meta entry exists.\n */\nexport const getRateLimit = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): number | null => {\n const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n if (!meta || meta.rateLimit === false || meta.rateLimit === undefined) return null;\n return meta.rateLimit;\n};\n\n/** Aggregate pass/fail/skip counts over a sweep layer's results. */\nexport const calculateSummary = (results: ContractCheckResult[]): RunContractSummary => ({\n total: results.length,\n passed: results.filter(r => r.status === 'pass').length,\n failed: results.filter(r => r.status === 'fail').length,\n skipped: results.filter(r => r.status === 'skipped').length,\n results,\n});\n","import { walkEndpoints } from './walkEndpoints';\nimport { runContractCheck } from './contractCheck';\nimport { shouldSkip, calculateSummary } from './testLayerHelpers';\nimport type { ApiMethodMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunContractTestsInput {\n apiMethodMap: ApiMethodMap;\n baseUrl: string;\n /**\n * Endpoints to skip. Matched against `<page>/<name>` (version-agnostic) and\n * `<page>/<name>/<version>` (version-specific). Useful for endpoints that\n * are known to need real input (e.g. a file upload).\n */\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runContractTests = async (input: RunContractTestsInput): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n //? Serial rather than parallel so we don't trip rate limiters or mask\n //? endpoint ordering bugs. Swap to a pool later if walk-time matters.\n const result = await runContractCheck({\n endpoint,\n baseUrl: input.baseUrl,\n inputFor: input.inputFor,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Canonical error code the framework emits for unauthenticated requests\n//? to `auth.login: true` endpoints. See packages/api/src/handleHttpApiRequest.ts.\nconst EXPECTED_ERROR_CODE = 'auth.required';\n\nexport interface AuthEnforcementCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n}\n\n//? Auth-enforcement layer: for each `auth.login: true` endpoint, call it with\n//? NO session headers/cookies. Expect the server to reject with `auth.required`.\n//?\n//? A passing endpoint here proves the login guard isn't silently bypassed.\n//? Endpoints that return `success` without a session are a critical finding.\nexport const runAuthEnforcementCheck = async (\n input: AuthEnforcementCheckInput,\n): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n\n //? No session cookie, no auth header — deliberately. Origin IS sent so the\n //? server's origin policy doesn't 403 before the auth check can run.\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return {\n endpoint,\n status: 'fail',\n reason: 'fetch returned no response',\n durationMs: Date.now() - started,\n };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (parsed?.status === 'success') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'success',\n reason: 'auth.login endpoint returned success without a session',\n durationMs,\n };\n }\n\n if (parsed?.status !== 'error') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'Response missing standard `status` envelope',\n durationMs,\n };\n }\n\n if (parsed.errorCode !== EXPECTED_ERROR_CODE) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n reason: `expected errorCode '${EXPECTED_ERROR_CODE}' but got '${parsed.errorCode ?? '(missing)'}'`,\n durationMs,\n };\n }\n\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n durationMs,\n };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runAuthEnforcementCheck } from './authEnforcementCheck';\nimport { shouldSkip, requiresLogin, calculateSummary } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunAuthEnforcementTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runAuthEnforcementTests = async (\n input: RunAuthEnforcementTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n //? Order matters: requiresLogin must run BEFORE shouldSkip so that a\n //? login-required endpoint in the explicit skip list is still recorded as\n //? `skipped` in the results (not silently dropped as a public route).\n //? Reversing the order would hide the skip from the summary.\n if (!requiresLogin(input.apiMetaMap, endpoint)) {\n //? Public endpoints can't be tested by this layer — skip silently, no\n //? noise. They're still covered by the contract layer.\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runAuthEnforcementCheck({\n endpoint,\n baseUrl: input.baseUrl,\n inputFor: input.inputFor,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\nconst EXPECTED_ERROR_CODE = 'api.rateLimitExceeded';\n\n//? Error codes the server emits BEFORE the rate-limit pipeline runs (CSRF gate,\n//? auth guard, schema validation). When a drain response carries one of these, the\n//? bucket was never incremented — the final probe will fail for the wrong reason\n//? and should be classified as `skipped` rather than a real failure.\nconst PRE_LIMITER_ERROR_CODES = new Set([\n 'auth.required',\n 'auth.csrfTokenMissing',\n 'auth.csrfTokenInvalid',\n 'auth.unauthorized',\n 'api.csrfTokenMissing',\n 'api.csrfTokenInvalid',\n 'api.unauthorized',\n 'api.validationError',\n]);\n\nexport interface RateLimitCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n rateLimit: number;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n /** Per-call request timeout in ms. Defaults to 10000. */\n requestTimeoutMs?: number;\n}\n\n//? Rate-limit layer: fire `rateLimit + 1` requests back to back and assert the\n//? last one is rejected with `api.rateLimitExceeded`. All preceding requests\n//? are allowed to return either success or any other envelope — only the N+1\n//? matters for this layer.\n//?\n//? This layer mutates server-side state (limiter counters). Run it after the\n//? contract + auth layers, or against a dedicated test instance. The server's\n//? clearAllRateLimits() utility can reset state between runs.\nexport const runRateLimitCheck = async (input: RateLimitCheckInput): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl, rateLimit } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\n const send = () => sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: input.headers,\n requestTimeoutMs,\n });\n\n //? Fire `rateLimit` allowed requests first (drain the bucket), then a final\n //? request that should be blocked. Serial so the server sees a clean order.\n //? Read the first drain response to detect pre-limiter rejections (CSRF/auth/\n //? validation): if the server rejects before the limiter runs, the bucket is\n //? never incremented, the final probe will fail for the wrong reason, and we\n //? classify the whole check as `skipped` rather than a false failure.\n for (let i = 0; i < rateLimit; i += 1) {\n const drained = await send();\n if (!drained) continue;\n if (i === 0) {\n //? Inspect the first drain to detect pre-limiter rejection.\n const errorCode = drained.parsed?.errorCode;\n if (errorCode && PRE_LIMITER_ERROR_CODES.has(errorCode)) {\n return {\n endpoint,\n status: 'skipped',\n durationMs: Date.now() - started,\n reason: `drain response rejected pre-limiter (${errorCode}); bucket was never incremented — check CSRF/auth headers`,\n };\n }\n }\n }\n\n const final = await send();\n const durationMs = Date.now() - started;\n\n if (!final) {\n return {\n endpoint,\n status: 'fail',\n reason: 'Final rate-limit probe request failed to return a response',\n durationMs,\n };\n }\n\n const { httpStatus, parsed } = final;\n\n if (parsed?.status === 'error' && parsed.errorCode === EXPECTED_ERROR_CODE) {\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n durationMs,\n };\n }\n\n const responseStatus: ContractCheckResult['responseStatus'] =\n parsed?.status === 'success' || parsed?.status === 'error' ? parsed.status : 'unknown';\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus,\n errorCode: parsed?.errorCode,\n reason: `expected '${EXPECTED_ERROR_CODE}' on request ${rateLimit + 1} but got ${parsed?.status ?? 'none'}/${parsed?.errorCode ?? '(none)'}`,\n durationMs,\n };\n};\n","import { tryCatch } from '@luckystack/core';\n\n//? Calls the server's /_test/reset endpoint. Used between rate-limit test\n//? endpoints to drain the shared IP-based limiter bucket so one endpoint's\n//? N+1 assertion doesn't consume the next endpoint's window.\n//?\n//? /_test/reset is gated by `NODE_ENV in { 'development', 'test' }` on the\n//? server side AND requires `TEST_RESET_TOKEN` to be set unconditionally\n//? (no token = 403). For staging/preview deploys that enable it for CI,\n//? set `TEST_RESET_TOKEN` on the server and pass the same value here.\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\nexport interface ResetServerStateInput {\n baseUrl: string;\n token?: string;\n requestTimeoutMs?: number;\n}\n\nexport const resetServerState = async ({ baseUrl, token, requestTimeoutMs }: ResetServerStateInput): Promise<boolean> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/_test/reset`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n if (token) headers['X-Test-Reset-Token'] = token;\n\n //? Bound the call so a server that accepts the connection but never responds\n //? returns `false` (the existing failure path) instead of wedging the sweep.\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);\n const [error, response] = await tryCatch(() => fetch(url, { method: 'POST', headers, signal: controller.signal }));\n clearTimeout(timeoutHandle);\n if (error || !response) return false;\n return response.ok;\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runRateLimitCheck } from './rateLimitCheck';\nimport { resetServerState } from './resetServerState';\nimport { shouldSkip, requiresLogin, getRateLimit, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunRateLimitTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n /**\n * Max rateLimit value to test. Endpoints with a higher limit are skipped\n * to avoid firing thousands of requests in CI.\n */\n maxRateLimitToTest?: number;\n /**\n * When true, hit `/_test/reset` before each endpoint so limiter state is\n * clean. Requires the server to allow the endpoint (NODE_ENV !== production\n * or `TEST_RESET_TOKEN` configured). Without resetting, the shared IP bucket\n * leaks state between endpoints.\n */\n resetBetweenEndpoints?: boolean;\n /**\n * Token forwarded as `X-Test-Reset-Token` when resetting. Only relevant in\n * staging/preview deploys that enable the endpoint behind a token.\n */\n resetToken?: string;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runRateLimitTests = async (\n input: RunRateLimitTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const maxRateLimit = input.maxRateLimitToTest ?? 50;\n //? A login-required route returns `auth.required` BEFORE the rate limiter\n //? runs, so it can only be rate-limit-tested with a session. A Cookie header\n //? means an authenticated sweep. We deliberately do NOT auto-authenticate the\n //? sweep — that would execute real mutations (delete/update) with junk input.\n //? Set TEST_AUTH_TOKEN to opt in and cover login-gated routes.\n const isAuthenticatedSweep = Boolean(input.headers?.Cookie);\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n const rateLimit = getRateLimit(input.apiMetaMap, endpoint);\n if (rateLimit === null) continue;\n\n if (!isAuthenticatedSweep && requiresLogin(input.apiMetaMap, endpoint)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'login-required route — set TEST_AUTH_TOKEN to rate-limit-test it (unauthenticated calls hit auth.required before the limiter)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n //? In cookie-mode the rate-limit check fires the endpoint `rateLimit + 1`\n //? times with junk input. State-changing routes (POST/PUT/DELETE) execute\n //? real mutation handlers on each hit — skip them to avoid side-effects on\n //? the test database. Their rate limits are better covered by a custom test\n //? that supplies valid input.\n if (isAuthenticatedSweep && STATE_CHANGING_METHODS.has(endpoint.method)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'state-changing route skipped in cookie-mode rate-limit sweep (mutation risk with junk bodies)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (rateLimit > maxRateLimit) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: `rateLimit ${rateLimit} exceeds maxRateLimitToTest=${maxRateLimit}`,\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (input.resetBetweenEndpoints) {\n await resetServerState({ baseUrl: input.baseUrl, token: input.resetToken });\n }\n\n const result = await runRateLimitCheck({\n endpoint,\n baseUrl: input.baseUrl,\n rateLimit,\n inputFor: input.inputFor,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Canonical error code the framework's CSRF middleware emits when a\n//? state-changing, cookie-authenticated request arrives without a matching\n//? CSRF token. See packages/server/src/httpRoutes/csrfMiddleware.ts.\nconst DEFAULT_EXPECTED_ERROR_CODE = 'auth.csrfMismatch';\nconst DEFAULT_EXPECTED_HTTP_STATUS = 403;\n\nexport interface CsrfEnforcementCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n /**\n * A valid session Cookie header (`<name>=<token>`). The probe deliberately\n * carries it so the request passes the auth guard and reaches the CSRF check.\n */\n authCookie: string;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n /** Error code expected on CSRF rejection. Defaults to `auth.csrfMismatch`. */\n expectedErrorCode?: string;\n /** HTTP status expected on CSRF rejection. Defaults to 403; pass `false` to skip. */\n expectedHttpStatus?: number | false;\n}\n\n//? CSRF-enforcement layer: for each `auth.login: true` state-changing route,\n//? send an authenticated request (valid session Cookie) WITHOUT the CSRF\n//? header. Expect the framework's CSRF middleware to reject it. This is the\n//? inverse of the custom layer's CSRF token-passing — it proves the protection\n//? actually fires, so a consumer who misconfigures CSRF doesn't get a green run.\nexport const runCsrfEnforcementCheck = async (\n input: CsrfEnforcementCheckInput,\n): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n //? CSRF is only enforced on state-changing methods (POST/PUT/DELETE). A GET\n //? probe would never reach the CSRF middleware regardless of headers, so it\n //? would always appear to pass — a false green. Guard here so the check is\n //? safe to call directly (the runner already filters, but direct callers may not).\n if (endpoint.method === 'GET') {\n return {\n endpoint,\n status: 'skipped',\n reason: `CSRF enforcement does not apply to ${endpoint.method} requests`,\n durationMs: 0,\n };\n }\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n const expectedErrorCode = input.expectedErrorCode ?? DEFAULT_EXPECTED_ERROR_CODE;\n const expectedHttpStatus = input.expectedHttpStatus ?? DEFAULT_EXPECTED_HTTP_STATUS;\n\n //? Valid session Cookie + Origin, but NO CSRF header — that omission is the\n //? whole point of the probe.\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: { Cookie: input.authCookie },\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return { endpoint, status: 'fail', reason: 'fetch returned no response', durationMs: Date.now() - started };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (parsed?.status === 'success') {\n return {\n endpoint, status: 'fail', httpStatus, responseStatus: 'success',\n reason: 'state-changing endpoint accepted an authenticated request with NO CSRF header — CSRF protection is not enforced',\n durationMs,\n };\n }\n\n if (parsed?.status !== 'error' || parsed.errorCode !== expectedErrorCode) {\n const responseStatus: ContractCheckResult['responseStatus'] =\n parsed?.status === 'success' || parsed?.status === 'error' ? parsed.status : 'unknown';\n return {\n endpoint, status: 'fail', httpStatus, responseStatus, errorCode: parsed?.errorCode,\n reason: `expected CSRF rejection '${expectedErrorCode}' but got ${parsed?.status ?? 'none'}/${parsed?.errorCode ?? '(none)'}`,\n durationMs,\n };\n }\n\n if (expectedHttpStatus !== false && httpStatus !== expectedHttpStatus) {\n return {\n endpoint, status: 'fail', httpStatus, responseStatus: 'error', errorCode: parsed.errorCode,\n reason: `expected HTTP ${expectedHttpStatus} on CSRF rejection but got ${httpStatus}`,\n durationMs,\n };\n }\n\n return { endpoint, status: 'pass', httpStatus, responseStatus: 'error', errorCode: parsed.errorCode, durationMs };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runCsrfEnforcementCheck } from './csrfEnforcementCheck';\nimport { shouldSkip, requiresLogin, hasMetaEntry, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunCsrfEnforcementTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n /** Valid session Cookie header (`<name>=<token>`) the probes carry so they reach the CSRF check. */\n authCookie: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms forwarded to each check. */\n requestTimeoutMs?: number;\n /** Error code expected on CSRF rejection. Defaults to `auth.csrfMismatch`. */\n expectedErrorCode?: string;\n /** HTTP status expected on CSRF rejection. Defaults to 403; pass `false` to skip. */\n expectedHttpStatus?: number | false;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runCsrfEnforcementTests = async (\n input: RunCsrfEnforcementTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n //? Only login-required, state-changing routes are CSRF-protected. A missing\n //? meta entry means we can't know the auth requirement — skip silently here\n //? (the auth layer already surfaces the stale-map gap).\n if (!hasMetaEntry(input.apiMetaMap, endpoint)) continue;\n if (!requiresLogin(input.apiMetaMap, endpoint)) continue;\n if (!STATE_CHANGING_METHODS.has(endpoint.method)) continue;\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runCsrfEnforcementCheck({\n endpoint,\n baseUrl: input.baseUrl,\n authCookie: input.authCookie,\n inputFor: input.inputFor,\n requestTimeoutMs: input.requestTimeoutMs,\n expectedErrorCode: input.expectedErrorCode,\n expectedHttpStatus: input.expectedHttpStatus,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Lightweight fuzz layer — no Zod yet. Sends well-known junk shapes and\n//? asserts the server doesn't 5xx or return a non-envelope body. Real\n//? schema-driven fuzzing (TS type -> random valid input) is deferred until\n//? the generator emits Zod schemas alongside the types.\nconst JUNK_PAYLOADS: readonly unknown[] = [\n null,\n [],\n [1, 2, 3],\n 'string-instead-of-object',\n 1_234_567_890,\n true,\n { nested: { deeply: { nested: { value: 'x'.repeat(10_000) } } } },\n //? `{ __proto__: ... }` serializes to `{}` via JSON.stringify (prototype keys\n //? are not own-enumerable), so it would probe nothing. Use `constructor` instead\n //? — a named own-key that some parsers misinterpret but survives JSON round-trip.\n { constructor: { polluted: true } },\n { key: null, other: undefined, third: Number.NaN },\n];\n\nexport interface FuzzCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n headers?: Record<string, string>;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n}\n\nexport const runFuzzCheck = async (input: FuzzCheckInput): Promise<ContractCheckResult> => {\n const started = Date.now();\n const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const url = `${input.baseUrl.replace(/\\/$/, '')}/${input.endpoint.fullPath}`;\n\n for (const payload of JUNK_PAYLOADS) {\n const result = await sendProbe({\n url,\n method: input.endpoint.method,\n baseUrl: input.baseUrl,\n body: payload,\n headers: input.headers,\n requestTimeoutMs,\n });\n const durationMs = Date.now() - started;\n\n if (!result) {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n reason: `fuzz probe crashed (no response) with payload: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n\n //? A 5xx that STILL returns a valid `{ status:'error', errorCode }` envelope\n //? is the framework gracefully catching an error (e.g. a route designed to\n //? throw), not a crash. The fuzz layer's concern is crashes / hangs /\n //? garbage — so we only fail on a non-envelope body below (which catches raw\n //? 5xx, HTML error pages, and truncated responses). A controlled 500\n //? envelope passes.\n if (!result.parsed || (result.parsed.status !== 'success' && result.parsed.status !== 'error')) {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n httpStatus: result.httpStatus,\n reason: `fuzz payload produced non-envelope response: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n\n if (result.parsed.status === 'error' && typeof result.parsed.errorCode !== 'string') {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n httpStatus: result.httpStatus,\n reason: `fuzz error response missing errorCode for payload: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n }\n\n return {\n endpoint: input.endpoint,\n status: 'pass',\n durationMs: Date.now() - started,\n };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runFuzzCheck } from './fuzzCheck';\nimport { shouldSkip, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ContractCheckResult, RunContractSummary } from './types';\n\nexport interface RunFuzzTestsInput {\n apiMethodMap: ApiMethodMap;\n baseUrl: string;\n skip?: string[];\n headers?: Record<string, string>;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runFuzzTests = async (input: RunFuzzTestsInput): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n //? In cookie-mode (authenticated sweep) junk bodies sent to state-changing\n //? routes (POST/PUT/DELETE) bypass the body-ignore short-circuit and reach real\n //? mutation handlers — risking unintended side-effects on the test database.\n //? Skip those endpoints here; the contract layer already validates their shape\n //? with well-formed input, and the CSRF layer probes their auth guard separately.\n const isAuthenticatedSweep = Boolean(input.headers?.Cookie);\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n if (isAuthenticatedSweep && STATE_CHANGING_METHODS.has(endpoint.method)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'state-changing route skipped in cookie-mode fuzz sweep (mutation risk with junk bodies)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runFuzzCheck({\n endpoint,\n baseUrl: input.baseUrl,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { z } from 'zod';\n\n//? Walk a Zod schema and produce a minimal valid value. Used by the\n//? contract/fuzz runners so \"valid input\" tests don't require hand-written\n//? fixtures per endpoint. Handles the shapes the generator emits\n//? (primitives, literals, unions, optionals, objects, records, arrays).\n//?\n//? Not a property-based generator — returns the SAME deterministic value\n//? every call. For randomized fuzz, swap to `fast-check` in a later session.\n\n//? zod 4 renamed the internal def accessor from `._def` to `.def`; recurse\n//? through whichever a child node exposes so the walker handles both versions.\nconst innerDef = (node: { _def?: unknown; def?: unknown } | undefined): unknown =>\n node ? (node.def ?? node._def) : undefined;\n\nconst sampleForDef = (def: unknown): unknown => {\n //? Zod stores its runtime shape on `_def.type` (v3) or `_def.typeName`.\n //? Access is untyped because `def` can be any of dozens of internal shapes.\n const d = def as {\n typeName?: string;\n type?: string;\n innerType?: { _def?: unknown; def?: unknown };\n values?: unknown[] | Record<string, unknown>;\n entries?: Record<string, unknown>;\n value?: unknown;\n options?: { _def?: unknown; def?: unknown }[];\n shape?: (() => Record<string, { _def?: unknown; def?: unknown }>) | Record<string, { _def?: unknown; def?: unknown }>;\n valueType?: { _def: unknown };\n element?: { _def: unknown };\n items?: { _def?: unknown; def?: unknown }[];\n schema?: { _def?: unknown; def?: unknown };\n in?: { _def?: unknown; def?: unknown };\n };\n\n const name = d.typeName ?? d.type;\n\n switch (name) {\n case 'ZodString':\n case 'string': {\n return 'test';\n }\n case 'ZodNumber':\n case 'number': {\n return 0;\n }\n case 'ZodBoolean':\n case 'boolean': {\n return false;\n }\n case 'ZodNull':\n case 'null': {\n return null;\n }\n case 'ZodUndefined':\n case 'undefined': {\n return undefined;\n }\n case 'ZodAny':\n case 'any':\n case 'ZodUnknown':\n case 'unknown': {\n return null;\n }\n case 'ZodLiteral':\n case 'literal': {\n //? zod 3 stores the literal on `_def.value`; zod 4 stores `_def.values` (array).\n return d.value ?? (Array.isArray(d.values) ? d.values[0] : undefined);\n }\n case 'ZodOptional':\n case 'optional':\n case 'ZodNullable':\n case 'nullable': {\n //? Omitting optional fields is the most conservative valid input.\n return undefined;\n }\n case 'ZodEnum':\n case 'enum':\n case 'ZodNativeEnum':\n case 'nativeEnum': {\n //? Return the first declared value so an enum-typed required field gets a\n //? VALID member rather than `null` (which the route's own Zod rejects,\n //? silently turning the contract probe into a validation-error test).\n //? zod 3 stores members on `_def.values` (array); zod 4 on `_def.entries`\n //? (object) or `_def.values` (Set/array).\n if (Array.isArray(d.values)) return d.values[0];\n const entries = d.entries ?? (d.values && !Array.isArray(d.values) ? d.values : undefined);\n if (entries) {\n const first = Object.values(entries)[0];\n if (first !== undefined) return first;\n }\n return undefined;\n }\n case 'ZodDefault':\n case 'default':\n case 'ZodCatch':\n case 'catch': {\n //? Unwrap to the inner schema's sample (the field is still required at the\n //? type level; the default only applies when omitted).\n return sampleForDef(innerDef(d.innerType));\n }\n case 'ZodEffects':\n case 'effects':\n case 'ZodPipeline':\n case 'pipe':\n case 'ZodBranded':\n case 'branded':\n case 'ZodReadonly':\n case 'readonly': {\n //? `.refine`/`.transform`/`.pipe`/`.brand`/`.readonly` wrap an inner\n //? schema — recurse into it. A `.refine` may still reject the sample, but\n //? recursing yields a structurally-valid base far more often than `null`.\n return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType));\n }\n case 'ZodTuple':\n case 'tuple': {\n const items = d.items ?? [];\n return items.map(item => sampleForDef(innerDef(item)));\n }\n case 'ZodUnion':\n case 'union':\n case 'ZodDiscriminatedUnion':\n case 'discriminatedUnion': {\n return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0])) : undefined;\n }\n case 'ZodArray':\n case 'array': {\n return [];\n }\n case 'ZodObject':\n case 'object': {\n const out: Record<string, unknown> = {};\n //? zod 3 exposes `_def.shape` as a function; zod 4 as a plain object.\n const shape = typeof d.shape === 'function' ? d.shape() : (d.shape ?? {});\n for (const [key, valueSchema] of Object.entries(shape)) {\n const sample = sampleForDef(innerDef(valueSchema));\n if (sample !== undefined) out[key] = sample;\n }\n return out;\n }\n case 'ZodRecord':\n case 'record': {\n return {};\n }\n case 'ZodDate':\n case 'date': {\n return new Date().toISOString();\n }\n default: {\n //? Unknown node: OMIT (undefined) rather than emit `null`. A `null` for a\n //? required field of an unhandled type makes the route's own Zod reject\n //? the body, silently turning the \"happy path\" contract probe into a\n //? validation-error test. Omitting lets the object walker drop the key so\n //? the probe only fails if the field was genuinely required (a real,\n //? visible signal) instead of always.\n return undefined;\n }\n }\n};\n\nexport const sampleSchemaInput = (schema: z.ZodType): unknown => {\n //? zod 4 renamed the public def accessor from `._def` to `.def`; fall back to\n //? `._def` (zod 3) so the top-level entry handles both, matching the\n //? recursive `innerDef` helper.\n const node = schema as { def?: unknown; _def?: unknown };\n return sampleForDef(node.def ?? node._def);\n};\n","import type { ContractCheckResult, RunContractSummary } from './types';\n\nconst statusIcon = (status: ContractCheckResult['status']): string => {\n switch (status) {\n case 'pass': { return '[PASS]';\n }\n case 'fail': { return '[FAIL]';\n }\n case 'skipped': { return '[SKIP]';\n }\n }\n};\n\nexport const logContractResult = (result: ContractCheckResult): void => {\n const { endpoint, durationMs, httpStatus, responseStatus, errorCode, reason } = result;\n const icon = statusIcon(result.status);\n const core = `${icon} ${endpoint.method} /${endpoint.fullPath} ${durationMs}ms`;\n\n if (result.status === 'pass') {\n const tail = responseStatus === 'error' ? ` (error: ${errorCode ?? 'unknown'})` : '';\n console.log(`${core} http=${httpStatus ?? '-'}${tail}`);\n return;\n }\n\n if (result.status === 'skipped') {\n console.log(`${core} ${reason ?? ''}`);\n return;\n }\n\n console.log(`${core} http=${httpStatus ?? '-'} reason=${reason ?? '(none)'}`);\n};\n\nexport const logContractSummary = (summary: RunContractSummary): void => {\n const { total, passed, failed, skipped } = summary;\n console.log('');\n console.log(`Contract tests: ${passed}/${total} passed, ${failed} failed, ${skipped} skipped`);\n\n if (failed > 0) {\n console.log('');\n console.log('Failures:');\n for (const result of summary.results) {\n if (result.status !== 'fail') continue;\n console.log(` - /${result.endpoint.fullPath}: ${result.reason ?? '(no reason)'}`);\n }\n }\n};\n","//? Per-route business-logic test layer. Discovers `*.tests.ts` files\n//? alongside `_api/` and `_sync/` route source, dynamic-imports each,\n//? builds a route-bound `TestContext`, runs the exported `customTests`\n//? cases. Spec: docs/ARCHITECTURE_TESTING.md.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { getCsrfConfig, getPrismaClient, getProjectConfig, getSrcDir, tryCatch, tryCatchSync } from '@luckystack/core';\n\nimport {\n openStreamWatcher,\n type StreamChunkFrame,\n type StreamWatcher,\n} from './streamWatcher';\n//? `ApiMethodMap` mirrors the generated `apiMethodMap` shape (page → name →\n//? version → method) and is a type-only import, so there's still no import-time\n//? coupling to the consumer's generated artifact.\nimport type { ApiMethodMap } from './types';\n\nconst API_TEST_FILE_PATTERN = /_v(\\d+)\\.tests\\.ts$/;\nconst SYNC_SERVER_TEST_FILE_PATTERN = /_server_v(\\d+)\\.tests\\.ts$/;\n\n//? Default per-request timeout for the Layer-5 call helpers. Mirrors the sweep\n//? layers so a hung (non-erroring) server can't wedge the whole custom-test run\n//? indefinitely — without it the case's own tryCatch never fires.\nconst DEFAULT_CALL_TIMEOUT_MS = 10_000;\n\nexport interface CustomTestCase {\n name: string;\n run: (ctx: TestContext) => Promise<void>;\n /**\n * Mark this case as a KNOWN, accepted failure (e.g. a documented bug not\n * yet fixed, or a scenario the current implementation cannot satisfy). The\n * string is the reason shown in the report. A case marked `expectedToFail`\n * that throws is reported as `xfail` — counted separately, NOT a red\n * failure. If it unexpectedly PASSES it is reported as `xpass` so the stale\n * marker can be removed. Leave undefined for normal cases.\n */\n expectedToFail?: string;\n}\n\nexport interface CustomTestResult {\n routePath: string;\n caseName: string;\n //? `xfail` = marked expectedToFail and failed as expected (known issue).\n //? `xpass` = marked expectedToFail but passed (remove the marker).\n status: 'pass' | 'fail' | 'xfail' | 'xpass';\n durationMs: number;\n reason?: string;\n /** Documented reason when the case carried an `expectedToFail` marker. */\n expectedToFail?: string;\n}\n\nexport interface RunCustomTestsSummary {\n total: number;\n passed: number;\n failed: number;\n /** Cases marked `expectedToFail` that failed as expected (known issues). */\n xfailed: number;\n /** Cases marked `expectedToFail` that unexpectedly passed (remove the marker). */\n xpassed: number;\n results: CustomTestResult[];\n}\n\nexport interface RunCustomTestsInput {\n baseUrl: string;\n /** Optional cookie name for the session token. Defaults to the project's configured cookie. */\n sessionCookieName?: string;\n /** Filter to a subset of routes — substring match against `<page>/<name>/<version>`. */\n filter?: string;\n /**\n * Generated `apiMethodMap` so the harness sends each API route its declared\n * HTTP method (e.g. logout is DELETE). Falls back to POST when absent.\n */\n apiMethodMap?: ApiMethodMap;\n onResult?: (result: CustomTestResult) => void;\n}\n\ninterface DiscoveredTestFile {\n /** Absolute path to the test file. */\n filePath: string;\n /** `api` or `sync` — which transport this route uses. */\n kind: 'api' | 'sync';\n /** Route descriptor parsed from the filename + directory location. */\n route: { page: string; name: string; version: string };\n}\n\n//? Walk `src/` recursively. A file matches when its directory contains an\n//? `_api/` or `_sync/` segment AND the filename matches the version suffix\n//? pattern. The route's `page` is everything between `src/` and the marker\n//? segment, joined with `/`. Nested page folders are supported (e.g.\n//? `src/admin/users/_api/getUser_v1.tests.ts` → page `admin/users`).\nexport const discoverCustomTestFiles = (srcDir: string = getSrcDir()): DiscoveredTestFile[] => {\n if (!fs.existsSync(srcDir)) return [];\n const found: DiscoveredTestFile[] = [];\n const stack: string[] = [srcDir];\n //? Track visited real-paths so a cyclic symlink under `src/` can't drive an\n //? infinite walk / unbounded stack growth.\n const visited = new Set<string>();\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined) break;\n //? Resolve to the canonical path and skip if already seen (symlink cycle).\n const [realError, realPath] = tryCatchSync(() => fs.realpathSync(current));\n const canonical = realError || realPath === null ? current : realPath;\n if (visited.has(canonical)) continue;\n visited.add(canonical);\n //? Guard the readdir: a permission error or a directory deleted mid-walk\n //? throws synchronously and would otherwise abort the whole custom layer.\n //? Skip the unreadable directory instead.\n const [readError, entries] = tryCatchSync(() => fs.readdirSync(current, { withFileTypes: true }));\n if (readError || entries === null) continue;\n for (const entry of entries) {\n const fullPath = path.join(current, entry.name);\n //? Skip symlinked entries entirely — following them risks a cycle and\n //? a discovered test file should live in the real source tree.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n stack.push(fullPath);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.tests.ts')) continue;\n const parentDir = path.basename(current);\n if (parentDir !== '_api' && parentDir !== '_sync') continue;\n const pattern = parentDir === '_api' ? API_TEST_FILE_PATTERN : SYNC_SERVER_TEST_FILE_PATTERN;\n const versionMatch = pattern.exec(entry.name);\n if (!versionMatch) continue;\n const version = `v${versionMatch[1]}`;\n const namePart = entry.name.replace(parentDir === '_api' ? /_v\\d+\\.tests\\.ts$/ : /_server_v\\d+\\.tests\\.ts$/, '');\n const pageDir = path.dirname(current);\n //? Top-level `src/_api/` (no page folder) routes under the framework's\n //? `system` page — mirror that, otherwise an empty page produces the\n //? malformed URL `/api//logout/v1` and the call 404s.\n const relativePage = path.relative(srcDir, pageDir).replaceAll('\\\\', '/') || 'system';\n found.push({\n filePath: fullPath,\n kind: parentDir === '_api' ? 'api' : 'sync',\n route: { page: relativePage, name: namePart, version },\n });\n }\n }\n return found.toSorted((a, b) => a.filePath.localeCompare(b.filePath));\n};\n\n//? Test fixture shapes. The TestContext is built per route — each per-route\n//? test file's `customTests` cases receive a context already bound to the\n//? route the file lives next to, so authors don't repeat `page/name/version`.\nexport interface TestContext {\n /** Invoke the route under test. Page / name / version are baked in. */\n //? Two type parameters by design — `TInput` lets callers document the\n //? typed shape they're sending even though we don't validate against it\n //? here (the route's own runtime validation does). The ESLint rule\n //? otherwise flags it as \"used once\"; the documentation value is real.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n callApi: <TInput = unknown, TOutput = unknown>(input: TInput) => Promise<TOutput>;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n callSync: <TInput = unknown, TOutput = unknown>(input: TInput, opts?: { receiver?: string }) => Promise<TOutput>;\n /**\n * Open a second socket connection that joins `roomCode` as a pure\n * subscriber and exposes the incoming chunk stream for assertions.\n *\n * The route is auto-derived from the test file location, so a watcher\n * only surfaces chunks for the route this test belongs to. Multiple\n * watchers per test are allowed; all are auto-closed when the test\n * case finishes (whether it passes or throws).\n */\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n watchStream: <TChunk extends StreamChunkFrame = StreamChunkFrame>(roomCode: string) => Promise<StreamWatcher<TChunk>>;\n session: {\n login: (user?: { email?: string; id?: string; name?: string }) => Promise<{ token: string; userId: string }>;\n logout: () => Promise<void>;\n current: () => { token: string | null; userId: string | null };\n };\n prisma: ReturnType<typeof getPrismaClient>;\n expect: TestExpect;\n}\n\nexport interface TestExpect {\n eq: <T>(actual: T, expected: T, message?: string) => void;\n ok: (value: unknown, message?: string) => void;\n throws: (fn: () => Promise<unknown>, message?: string) => Promise<Error>;\n matches: (value: string, pattern: RegExp, message?: string) => void;\n}\n\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\nconst buildExpect = (): TestExpect => ({\n eq: (actual, expected, message) => {\n const ok = Object.is(actual, expected)\n || (typeof actual === 'object' && typeof expected === 'object' && JSON.stringify(actual) === JSON.stringify(expected));\n if (!ok) {\n const prefix = message ? `${message}: ` : '';\n throw new AssertionError(`${prefix}expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);\n }\n },\n ok: (value, message) => {\n if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);\n },\n throws: async (fn, message) => {\n const [err] = await tryCatch(fn);\n if (!err) throw new AssertionError(message ?? 'expected fn to throw, but it did not');\n return err;\n },\n matches: (value, pattern, message) => {\n if (!pattern.test(value)) {\n const prefix = message ? `${message}: ` : '';\n throw new AssertionError(`${prefix}\"${value}\" does not match ${pattern}`);\n }\n },\n});\n\ninterface TestSessionState {\n token: string | null;\n userId: string | null;\n //? CSRF token minted by saveSession on login. Sent on authenticated\n //? state-changing requests so the server's CSRF middleware doesn't 403.\n csrfToken: string | null;\n //? Raw JSON body of the most recent callApi / callSync response. Surfaced in\n //? a failed case's reason so the report shows the server's actual errorCode\n //? (e.g. `api.rateLimitExceeded`) instead of only the assertion text.\n lastResponse: unknown;\n}\n\n//? Pull the server's errorCode (or non-success status) out of the last HTTP\n//? response so a failure's reason shows WHY the server rejected the call, not\n//? just the assertion text. Returns undefined for success / non-envelope bodies.\nconst extractErrorCode = (response: unknown): string | undefined => {\n if (!response || typeof response !== 'object') return undefined;\n const r = response as { errorCode?: unknown; status?: unknown };\n if (typeof r.errorCode === 'string' && r.errorCode.length > 0) return r.errorCode;\n if (typeof r.status === 'string' && r.status !== 'success') return r.status;\n return undefined;\n};\n\n//? Minimal session login: mints a session in Redis via the consumer's\n//? registered SessionAdapter (from `@luckystack/login`). Deliberately does\n//? NOT create the Prisma user record — User-table shape varies per\n//? project. If your test needs a real user row, create it via\n//? `ctx.prisma.user.create(...)` first, then pass `{ id, email }` to\n//? `session.login()`.\nconst buildSessionHelpers = (state: TestSessionState): TestContext['session'] => {\n return {\n login: async (user) => {\n const { saveSession, getSession } = await import('@luckystack/login');\n const userId = user?.id ?? `test-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const email = user?.email ?? `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;\n const name = user?.name ?? 'Test User';\n const token = `test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n const sessionData = {\n id: userId,\n email,\n name,\n token,\n provider: 'credentials' as const,\n };\n await saveSession(token, sessionData, true);\n state.token = token;\n state.userId = userId;\n //? Read back the CSRF token saveSession minted so authenticated\n //? state-changing requests can pass the server's CSRF middleware.\n const persisted = await getSession(token);\n state.csrfToken = persisted?.csrfToken ?? null;\n return { token, userId };\n },\n logout: async () => {\n if (!state.token) return;\n const { deleteSession } = await import('@luckystack/login');\n await deleteSession(state.token);\n state.token = null;\n state.userId = null;\n state.csrfToken = null;\n },\n current: () => ({ token: state.token, userId: state.userId }),\n };\n};\n\n//? Parse a fetch Response as JSON behind tryCatch and store the raw body on the\n//? session state. A non-JSON body (HTML 500, empty 204, truncated) would\n//? otherwise throw a raw SyntaxError with `state.lastResponse` null, hiding the\n//? server's actual output. On parse failure we keep a snippet of the text body\n//? so a failed case's reason can show what the server actually returned.\nconst parseResponse = async (response: Response, state: TestSessionState): Promise<unknown> => {\n const [textError, text] = await tryCatch(() => response.text());\n if (textError || text === null) {\n state.lastResponse = null;\n throw new Error(`failed to read response body (HTTP ${response.status}): ${textError?.message ?? 'unknown'}`);\n }\n const [parseError, parsed] = tryCatchSync(() => JSON.parse(text) as unknown);\n if (parseError) {\n const snippet = text.length > 500 ? `${text.slice(0, 500)}…` : text;\n state.lastResponse = snippet;\n throw new Error(`response was not valid JSON (HTTP ${response.status}): ${snippet}`);\n }\n state.lastResponse = parsed;\n return parsed;\n};\n\nconst buildCallApi = (\n baseUrl: string,\n routePath: string,\n state: TestSessionState,\n cookieName: string,\n //? The route's declared HTTP method (from the generated apiMethodMap). The\n //? server validates the method, so DELETE/PUT routes (e.g. logout) 405 if we\n //? always sent POST.\n method: string,\n): TestContext['callApi'] => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- must match TestContext['callApi'] generic signature\n return async <TInput = unknown, TOutput = unknown>(input: TInput): Promise<TOutput> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/api/${routePath}`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json', Origin: new URL(baseUrl).origin };\n if (state.token) headers.Cookie = `${cookieName}=${state.token}`;\n if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, DEFAULT_CALL_TIMEOUT_MS);\n const init: RequestInit = { method, headers, signal: controller.signal };\n //? GET / HEAD requests cannot carry a body (fetch throws). Every other\n //? method sends the JSON payload.\n if (method !== 'GET' && method !== 'HEAD') init.body = JSON.stringify(input ?? {});\n const [fetchError, response] = await tryCatch(() => fetch(url, init));\n clearTimeout(timeoutHandle);\n if (fetchError || !response) {\n state.lastResponse = null;\n throw new Error(`callApi ${method} ${routePath} failed: ${fetchError?.message ?? 'no response'}`);\n }\n //? `parseResponse` returns `unknown`; TOutput is the caller-supplied expectation.\n //? The runtime shape is unverifiable here — the route's own validator owns that\n //? contract. The cast is required because parseResponse must return `unknown`.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax -- parseResponse returns unknown; TOutput is the caller's type contract, verified at the call site\n return await parseResponse(response, state) as TOutput;\n };\n};\n\nconst buildCallSync = (\n baseUrl: string,\n routePath: string,\n state: TestSessionState,\n cookieName: string,\n): TestContext['callSync'] => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- must match TestContext['callSync'] generic signature\n return async <TInput = unknown, TOutput = unknown>(input: TInput, opts?: { receiver?: string }): Promise<TOutput> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/sync/${routePath}`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json', Origin: new URL(baseUrl).origin };\n if (state.token) headers.Cookie = `${cookieName}=${state.token}`;\n if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;\n const body = { data: input ?? {}, receiver: opts?.receiver };\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, DEFAULT_CALL_TIMEOUT_MS);\n const [fetchError, response] = await tryCatch(() => fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n }));\n clearTimeout(timeoutHandle);\n if (fetchError || !response) {\n state.lastResponse = null;\n throw new Error(`callSync ${routePath} failed: ${fetchError?.message ?? 'no response'}`);\n }\n //? parseResponse returns unknown; TOutput is the caller's type contract, verified at the call site\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax -- parseResponse returns unknown; cast to TOutput is the boundary\n return await parseResponse(response, state) as TOutput;\n };\n};\n\ninterface BuiltContext {\n ctx: TestContext;\n /** Close every watcher opened during the case. Called by `runCustomTests` after each case. */\n closeAllWatchers: () => Promise<void>;\n /** Per-case session state — read after the run to surface the last response's errorCode. */\n state: TestSessionState;\n}\n\nconst buildContext = (\n discovery: DiscoveredTestFile,\n input: RunCustomTestsInput,\n): BuiltContext => {\n //? Default to the PROJECT's configured session cookie name (the server reads\n //? the token from this cookie). Hardcoding a name silently breaks every\n //? authenticated custom test when the project uses a different name.\n const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;\n const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;\n const routeFullName = `sync/${routePath}`;\n //? API routes are invoked with their declared HTTP method; sync routes always\n //? POST. Default to POST when the map lacks the route (e.g. sync, or a map\n //? wasn't threaded through).\n const apiMethod = input.apiMethodMap?.[discovery.route.page]?.[discovery.route.name]?.[discovery.route.version] ?? 'POST';\n const state: TestSessionState = { token: null, userId: null, csrfToken: null, lastResponse: null };\n const prisma = getPrismaClient();\n\n //? Watchers are tracked per case so a missing explicit `close()` can't\n //? leak socket B between tests.\n const watchers: StreamWatcher[] = [];\n const watchStream = async <TChunk extends StreamChunkFrame = StreamChunkFrame>(\n roomCode: string,\n ): Promise<StreamWatcher<TChunk>> => {\n const watcher = await openStreamWatcher<TChunk>({\n baseUrl: input.baseUrl,\n roomCode,\n token: state.token,\n routeFullName,\n });\n //? Track watchers under the base `StreamChunkFrame` shape; the\n //? returned reference keeps the caller-supplied generic.\n watchers.push(watcher);\n return watcher;\n };\n\n const closeAllWatchers = async (): Promise<void> => {\n while (watchers.length > 0) {\n const watcher = watchers.pop();\n if (!watcher) continue;\n const [closeError] = await tryCatch(() => watcher.close());\n if (closeError) {\n //? Swallow — closing in cleanup must never mask the original test\n //? failure. Surface via logger if a project wires one up.\n //? (Intentionally not using getLogger here to avoid an extra import.)\n }\n }\n };\n\n const ctx: TestContext = {\n callApi: buildCallApi(input.baseUrl, routePath, state, cookieName, apiMethod),\n callSync: buildCallSync(input.baseUrl, routePath, state, cookieName),\n watchStream,\n session: buildSessionHelpers(state),\n prisma,\n expect: buildExpect(),\n };\n\n return { ctx, closeAllWatchers, state };\n};\n\nexport const runCustomTests = async (input: RunCustomTestsInput): Promise<RunCustomTestsSummary> => {\n const files = discoverCustomTestFiles();\n const results: CustomTestResult[] = [];\n\n for (const discovery of files) {\n const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;\n if (input.filter && !routePath.includes(input.filter)) continue;\n\n const [importError, mod] = await tryCatch(\n () => import(pathToFileURL(discovery.filePath).href) as Promise<Record<string, unknown>>,\n );\n if (importError || !mod) {\n const r: CustomTestResult = {\n routePath, caseName: '(import)', status: 'fail', durationMs: 0,\n reason: `failed to import ${discovery.filePath}: ${importError?.message ?? 'unknown'}`,\n };\n results.push(r);\n input.onResult?.(r);\n continue;\n }\n\n const exported = (mod as { customTests?: unknown }).customTests;\n //? DD-TR-various — empty-stub contract (documented decision):\n //? A test file that exports an empty array (or no `customTests` export) is\n //? silently skipped, NOT emitted as a `skipped` result. Rationale: AI-generated\n //? stub files start life empty; surfacing them as \"skipped\" every run adds noise\n //? before the author fills them in. The `CustomTestResult` status union therefore\n //? stays `'pass' | 'fail' | 'xfail' | 'xpass'` — no `'skipped'` variant.\n //? If you need to track unfilled stubs, scan `discoverCustomTestFiles()` and\n //? compare against the results array.\n if (!Array.isArray(exported) || exported.length === 0) {\n continue;\n }\n\n for (const rawCase of exported) {\n const c = rawCase as CustomTestCase;\n if (typeof c.name !== 'string' || typeof c.run !== 'function') continue;\n const expectedToFail = typeof c.expectedToFail === 'string' ? c.expectedToFail : undefined;\n const built = buildContext(discovery, input);\n const started = Date.now();\n const [runError] = await tryCatch(async () => c.run(built.ctx));\n //? Always close watchers regardless of pass/fail so the next case\n //? starts clean. tryCatch on the cleanup itself prevents a teardown\n //? throw from masking the original failure.\n await tryCatch(() => built.closeAllWatchers());\n //? Best-effort session cleanup: if `session.login()` minted a Redis\n //? session but the case didn't call `session.logout()`, the token\n //? accumulates until its TTL expires. Delete it now to avoid a slow\n //? session-key leak across a full sweep run.\n const residualToken = built.state.token;\n if (residualToken) {\n await tryCatch(async () => {\n const { deleteSession } = await import('@luckystack/login');\n await deleteSession(residualToken);\n });\n }\n const durationMs = Date.now() - started;\n\n let r: CustomTestResult;\n if (runError) {\n //? Augment the assertion message with the server's actual errorCode so\n //? the report explains the rejection, not just the failed expectation.\n const errorCode = extractErrorCode(built.state.lastResponse);\n const reason = errorCode ? `${runError.message} (server: ${errorCode})` : runError.message;\n r = expectedToFail\n ? { routePath, caseName: c.name, status: 'xfail', durationMs, reason, expectedToFail }\n : { routePath, caseName: c.name, status: 'fail', durationMs, reason };\n } else {\n r = expectedToFail\n ? {\n routePath, caseName: c.name, status: 'xpass', durationMs,\n reason: `marked expectedToFail (\"${expectedToFail}\") but passed — remove the marker`,\n expectedToFail,\n }\n : { routePath, caseName: c.name, status: 'pass', durationMs };\n }\n results.push(r);\n input.onResult?.(r);\n }\n }\n\n return {\n total: results.length,\n passed: results.filter(r => r.status === 'pass').length,\n failed: results.filter(r => r.status === 'fail').length,\n xfailed: results.filter(r => r.status === 'xfail').length,\n xpassed: results.filter(r => r.status === 'xpass').length,\n results,\n };\n};\n","//? Second-socket harness for Layer 5 custom tests. Opens a separate\n//? socket.io-client connection (socket B), joins the receiver room as a\n//? pure subscriber, and exposes the broadcast/streamTo chunk stream for\n//? assertions. Without this, `ctx.callSync` only surfaces the final\n//? envelope — chunks (LLM tokens, progress ticks) emitted via\n//? `broadcastStream` / `streamTo` flow on the `sync` event channel to\n//? room members other than the request originator's socket.\n//?\n//? Spec: see `docs/contract-tests.md` §watchStream and CLAUDE Rule 21\n//? (no `as unknown as X` / `as any` — typed generics throughout).\n//?\n//? Wire shape, sourced from `@luckystack/sync` `_shared/streamEmitters.ts`:\n//? io.to(receiver).emit('sync', { ...payload, cb, fullName, status: 'stream' })\n//? where `fullName` = `sync/<page>/<name>/<version>`. We filter incoming\n//? frames by that `fullName` so a watcher only sees chunks for the route\n//? its test file lives next to.\n\nimport { io as createSocketIoClient, Socket } from 'socket.io-client';\nimport {\n buildJoinRoomResponseEventName,\n socketEventNames,\n tryCatch,\n} from '@luckystack/core';\n\nexport interface StreamChunkFrame {\n /** Per-request correlation id stamped by the server. */\n cb?: string;\n /** Full route name (`sync/<page>/<name>/<version>`). */\n fullName?: string;\n /** Always `'stream'` for chunk frames. */\n status?: 'stream';\n /** Payload fields spread on top of the envelope. */\n [key: string]: unknown;\n}\n\nexport interface StreamWatcher<TChunk extends StreamChunkFrame = StreamChunkFrame> {\n /** Chunks received so far. Array reference is stable; entries are pushed in arrival order. */\n readonly chunks: TChunk[];\n /** Wait until `predicate(chunks)` returns true, or until timeout (default 5000ms). */\n stopAt(predicate: (chunks: TChunk[]) => boolean, timeoutMs?: number): Promise<void>;\n /** Wait until N total chunks received, or until timeout. */\n waitForCount(n: number, timeoutMs?: number): Promise<void>;\n /** Close socket B. Tests SHOULD call this in cleanup; ctx auto-closes all watchers when the test ends. */\n close(): Promise<void>;\n}\n\nexport interface OpenStreamWatcherInput {\n /** Server URL (same as `ctx`'s `baseUrl`). */\n baseUrl: string;\n /** Room to join as a pure subscriber. */\n roomCode: string;\n /** Active session token (used for `joinRoom` auth). May be null for routes that don't require auth. */\n token: string | null;\n /**\n * Resolved route name `sync/<page>/<name>/<version>` — chunk frames are\n * filtered by their `fullName` property so a watcher only surfaces chunks\n * for the route its test is bound to.\n */\n routeFullName: string;\n /** Default 5000ms — overridable per `stopAt` / `waitForCount` call. */\n defaultTimeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst JOIN_RESPONSE_TIMEOUT_MS = 3000;\nconst CONNECT_TIMEOUT_MS = 3000;\n\ninterface JoinRoomResponse {\n status?: string;\n errorCode?: string;\n}\n\nconst isJoinRoomResponse = (value: unknown): value is JoinRoomResponse =>\n typeof value === 'object' && value !== null;\n\nconst isStreamChunkFrame = (value: unknown): value is StreamChunkFrame =>\n typeof value === 'object' && value !== null;\n\n//? Monotonic counter — each watcher uses a fresh responseIndex so the\n//? `buildJoinRoomResponseEventName(...)` listener doesn't collide with a\n//? sibling watcher on the same process.\nlet responseIndexCounter = 0;\nconst nextResponseIndex = (): number => {\n responseIndexCounter += 1;\n return responseIndexCounter;\n};\n\nconst waitForConnect = (socket: Socket): Promise<void> => {\n return new Promise<void>((resolve, reject) => {\n if (socket.connected) {\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`watchStream: socket failed to connect within ${CONNECT_TIMEOUT_MS}ms`));\n }, CONNECT_TIMEOUT_MS);\n const onConnect = (): void => {\n cleanup();\n resolve();\n };\n const onConnectError = (err: Error): void => {\n cleanup();\n reject(new Error(`watchStream: connect_error — ${err.message}`));\n };\n const cleanup = (): void => {\n clearTimeout(timer);\n socket.off('connect', onConnect);\n socket.off('connect_error', onConnectError);\n };\n socket.on('connect', onConnect);\n socket.on('connect_error', onConnectError);\n });\n};\n\nconst waitForJoinAck = (socket: Socket, responseIndex: number): Promise<void> => {\n const eventName = buildJoinRoomResponseEventName(responseIndex);\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n socket.off(eventName, onAck);\n reject(new Error(`watchStream: joinRoom ack timeout after ${JOIN_RESPONSE_TIMEOUT_MS}ms`));\n }, JOIN_RESPONSE_TIMEOUT_MS);\n const onAck = (payload: unknown): void => {\n clearTimeout(timer);\n socket.off(eventName, onAck);\n if (isJoinRoomResponse(payload) && payload.status === 'error') {\n reject(new Error(`watchStream: joinRoom rejected — ${payload.errorCode ?? 'unknown'}`));\n return;\n }\n resolve();\n };\n socket.on(eventName, onAck);\n });\n};\n\nexport const openStreamWatcher = async <TChunk extends StreamChunkFrame = StreamChunkFrame>(\n input: OpenStreamWatcherInput,\n): Promise<StreamWatcher<TChunk>> => {\n const defaultTimeoutMs = input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;\n const chunks: TChunk[] = [];\n const waiters: { predicate: (chunks: TChunk[]) => boolean; resolve: () => void }[] = [];\n\n const socket: Socket = createSocketIoClient(input.baseUrl, {\n transports: ['websocket'],\n forceNew: true,\n reconnection: false,\n auth: input.token ? { token: input.token } : undefined,\n });\n\n const handleChunk = (raw: unknown): void => {\n if (!isStreamChunkFrame(raw)) return;\n if (raw.status !== 'stream') return;\n if (raw.fullName !== input.routeFullName) return;\n //? Server emits chunk frames typed as `StreamChunkFrame` — caller's\n //? `TChunk` narrows the payload shape (e.g. `{ chunk: string }` for\n //? `streamBroadcast`). The runtime guards above confirm the wire shape;\n //? narrowing the static type is the test author's responsibility.\n const chunk = raw as TChunk;\n chunks.push(chunk);\n //? Drain waiters whose predicates are now satisfied. Iterate in reverse\n //? so we can splice without breaking indices.\n for (let i = waiters.length - 1; i >= 0; i--) {\n const waiter = waiters[i];\n if (!waiter) continue;\n if (waiter.predicate(chunks)) {\n waiters.splice(i, 1);\n waiter.resolve();\n }\n }\n };\n\n socket.on(socketEventNames.sync, handleChunk);\n\n //? On a connect timeout / connect_error the socket (and its `sync` listener)\n //? must be torn down — otherwise every failed attempt leaks one socket that\n //? is never auto-closed (the caller only tracks the watcher after this\n //? resolves). Mirror the join-ack cleanup path below.\n const [connectError] = await tryCatch(() => waitForConnect(socket));\n if (connectError) {\n socket.off(socketEventNames.sync, handleChunk);\n socket.disconnect();\n throw connectError;\n }\n\n const responseIndex = nextResponseIndex();\n socket.emit(socketEventNames.joinRoom, {\n group: input.roomCode,\n responseIndex,\n });\n const [joinError] = await tryCatch(() => waitForJoinAck(socket, responseIndex));\n if (joinError) {\n socket.off(socketEventNames.sync, handleChunk);\n socket.disconnect();\n throw joinError;\n }\n\n let closed = false;\n const close = (): Promise<void> => {\n if (closed) return Promise.resolve();\n closed = true;\n socket.off(socketEventNames.sync, handleChunk);\n //? Drain any outstanding waiters so callers don't hang past test end.\n for (const waiter of waiters) waiter.resolve();\n waiters.length = 0;\n socket.disconnect();\n return Promise.resolve();\n };\n\n const stopAt = (\n predicate: (chunks: TChunk[]) => boolean,\n timeoutMs: number = defaultTimeoutMs,\n ): Promise<void> => {\n if (predicate(chunks)) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const waiter = {\n predicate,\n resolve: () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve();\n },\n };\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n const idx = waiters.indexOf(waiter);\n if (idx !== -1) waiters.splice(idx, 1);\n reject(new Error(\n `watchStream.stopAt: predicate not satisfied within ${timeoutMs}ms (received ${chunks.length} chunks)`,\n ));\n }, timeoutMs);\n waiters.push(waiter);\n });\n };\n\n const waitForCount = (n: number, timeoutMs: number = defaultTimeoutMs): Promise<void> => {\n return stopAt((current) => current.length >= n, timeoutMs);\n };\n\n return {\n chunks,\n stopAt,\n waitForCount,\n close,\n };\n};\n","//? Orchestrator — runs all five test layers (contract, auth-enforcement,\n//? rate-limit, fuzz, custom) against a running server and returns a single\n//? combined summary. Consumers call this from their own thin script that\n//? imports the generated `apiMethodMap` + `apiInputSchemas`.\n\nimport { clearAllRateLimits, getCsrfConfig, getProjectConfig } from '@luckystack/core';\n\nimport { sampleSchemaInput } from './schemaSampleInput';\nimport { runContractTests } from './runContractTests';\nimport { runAuthEnforcementTests } from './runAuthEnforcementTests';\nimport { runRateLimitTests } from './runRateLimitTests';\nimport { runCsrfEnforcementTests } from './runCsrfEnforcementTests';\nimport { runFuzzTests } from './runFuzzTests';\nimport { runCustomTests } from './customTests';\nimport { calculateSummary, LAYER_KEYS } from './testLayerHelpers';\nimport type { CustomTestResult, RunCustomTestsSummary } from './customTests';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\nimport type { ZodType } from 'zod';\n\ntype ApiInputSchemas = Partial<Record<string, Partial<Record<string, Partial<Record<string, ZodType | undefined>>>>>>;\n\nexport interface RunAllTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n apiInputSchemas: ApiInputSchemas;\n baseUrl: string;\n /** Cookie name for the session token. Defaults to `luckystack_token`. */\n sessionCookieName?: string;\n /** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */\n authToken?: string;\n /** Endpoints to skip from the sweep layers, e.g. routes that need a file upload. */\n skip?: string[];\n /** Substring filter applied to `<page>/<name>/<version>`. */\n filter?: string;\n /** Disable individual layers (faster local iteration). */\n noSweep?: boolean;\n noFuzz?: boolean;\n noRateLimit?: boolean;\n /**\n * Disable the CSRF-enforcement layer. Defaults to ON (the layer runs) — but it\n * can only probe past the auth guard with a session, so it is a no-op unless an\n * `authToken` is supplied.\n */\n noCsrf?: boolean;\n noCustom?: boolean;\n}\n\nexport interface RunAllTestsSummary {\n contract?: RunContractSummary;\n auth?: RunContractSummary;\n rateLimit?: RunContractSummary;\n csrf?: RunContractSummary;\n fuzz?: RunContractSummary;\n custom?: RunCustomTestsSummary;\n totalPassed: number;\n totalFailed: number;\n}\n\nconst inputForEndpoint = (apiInputSchemas: ApiInputSchemas) =>\n (endpoint: EndpointDescriptor): unknown => {\n const schema = apiInputSchemas[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n return schema ? sampleSchemaInput(schema) : {};\n };\n\nconst matchesFilter = (endpoint: EndpointDescriptor, filter: string | undefined): boolean => {\n if (!filter) return true;\n return `${endpoint.page}/${endpoint.name}/${endpoint.version}`.includes(filter);\n};\n\nconst filterResults = (results: ContractCheckResult[], filter: string | undefined): ContractCheckResult[] =>\n filter ? results.filter(r => matchesFilter(r.endpoint, filter)) : results;\n\nconst cloneSummary = (summary: RunContractSummary, filter: string | undefined): RunContractSummary => {\n if (!filter) return summary;\n return calculateSummary(filterResults(summary.results, filter));\n};\n\n//? Build the Cookie (and CSRF) headers sweep layers send when an `authToken`\n//? is supplied. In cookie-mode the server's CSRF middleware rejects POST/PUT/DELETE\n//? without a matching CSRF token, so we look up the session record and attach the\n//? token the same way customTests.ts does: call getSession(authToken) and read\n//? csrfToken off the persisted record. Token-mode sweeps (no Cookie header) and\n//? runs without authToken are unaffected — the CSRF header is never added.\nconst buildAuthHeaders = async (input: RunAllTestsInput): Promise<Record<string, string>> => {\n const headers: Record<string, string> = {};\n if (!input.authToken) return headers;\n\n const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;\n headers.Cookie = `${cookieName}=${input.authToken}`;\n\n //? Resolve the CSRF token lazily — import @luckystack/login at call time so\n //? projects that run the test-runner without login installed don't crash on\n //? import. Mirror the same dynamic import pattern used in customTests.ts.\n try {\n const { getSession } = await import('@luckystack/login');\n const session = await getSession(input.authToken);\n if (session?.csrfToken) {\n headers[getCsrfConfig().headerName] = session.csrfToken;\n }\n } catch {\n //? @luckystack/login not installed or session lookup failed.\n //? The sweep will proceed without the CSRF header, which is the same\n //? behavior as before this fix — acceptable for token-mode setups.\n }\n\n return headers;\n};\n\n//? Run the four auto-sweep layers (contract → auth → rate-limit → fuzz) in\n//? order, honoring the per-layer disable flags, and write each filtered\n//? summary onto `summary`. Mutates `summary` in place — identical to the\n//? prior inline block. The auth and rate-limit / fuzz nesting + ordering is\n//? preserved exactly (auth always runs in the sweep; rate-limit + fuzz are\n//? individually gated).\nconst runSweepLayers = async (\n input: RunAllTestsInput,\n summary: RunAllTestsSummary,\n headers: Record<string, string>,\n inputFor: (endpoint: EndpointDescriptor) => unknown,\n): Promise<void> => {\n const contract = await runContractTests({\n apiMethodMap: input.apiMethodMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n inputFor,\n });\n summary.contract = cloneSummary(contract, input.filter);\n\n const auth = await runAuthEnforcementTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n inputFor,\n });\n summary.auth = cloneSummary(auth, input.filter);\n\n if (!input.noRateLimit) {\n const rateLimit = await runRateLimitTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n inputFor,\n //? Reset the shared per-IP rate-limit bucket between endpoints so a\n //? neighbour saturating the window doesn't cause false pass/fail on the\n //? next endpoint. Requires the server to expose /_test/reset (allowed in\n //? NODE_ENV !== production or behind TEST_RESET_TOKEN).\n resetBetweenEndpoints: true,\n });\n summary.rateLimit = cloneSummary(rateLimit, input.filter);\n }\n\n //? CSRF-enforcement layer: probes that the framework's CSRF middleware rejects\n //? state-changing authenticated requests carrying NO CSRF header. It can only\n //? reach the CSRF check past the auth guard with a valid session, so it runs\n //? only when an `authToken` is supplied (and the layer isn't disabled). Without\n //? a token there is nothing to probe — the layer is silently skipped, leaving\n //? behavior unchanged for tokenless sweeps.\n if (!input.noCsrf && headers.Cookie) {\n const csrf = await runCsrfEnforcementTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n authCookie: headers.Cookie,\n skip: input.skip,\n inputFor,\n });\n summary.csrf = cloneSummary(csrf, input.filter);\n }\n\n if (!input.noFuzz) {\n const fuzz = await runFuzzTests({\n apiMethodMap: input.apiMethodMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n });\n summary.fuzz = cloneSummary(fuzz, input.filter);\n }\n};\n\n//? Run the custom (Layer 5) sweep, after clearing the per-route rate-limit\n//? buckets the sweep layers drained. Mutates `summary` in place — identical to\n//? the prior inline block.\nconst runCustomLayer = async (input: RunAllTestsInput, summary: RunAllTestsSummary): Promise<void> => {\n //? The rate-limit layer drains per-route buckets; clear them before the\n //? custom layer so business-logic tests on low-limit routes (e.g.\n //? confirmReset, sendReset) aren't spuriously rejected with\n //? api.rateLimitExceeded. Runs in-process against the same Redis the server\n //? uses. Non-fatal if the active strategy has no clear().\n try {\n await clearAllRateLimits();\n } catch {\n //? swallow — a missing clear() is a test-quality degrade, not a crash.\n }\n const custom = await runCustomTests({\n baseUrl: input.baseUrl,\n sessionCookieName: input.sessionCookieName,\n filter: input.filter,\n //? Pass the generated method map so the harness invokes each API route\n //? with its declared HTTP method (e.g. logout is DELETE, not POST).\n apiMethodMap: input.apiMethodMap,\n });\n summary.custom = custom;\n};\n\n//? The ordered list of sweep-layer keys that map to `RunContractSummary`\n//? slots on `RunAllTestsSummary`. Drives `computeTotals` and the reporter so\n//? adding a new sweep layer only requires adding it here + to the summary type.\nconst SWEEP_LAYER_ORDER = [\n LAYER_KEYS.contract,\n LAYER_KEYS.auth,\n LAYER_KEYS.rateLimit,\n LAYER_KEYS.csrf,\n LAYER_KEYS.fuzz,\n] as const;\n\ntype SweepLayerKey = typeof SWEEP_LAYER_ORDER[number];\n\n//? Map from each canonical layer key to the matching summary property name on\n//? `RunAllTestsSummary`. Property names diverged from LAYER_KEYS values early\n//? (LAYER_KEYS uses display strings like `'auth-enforcement'`); keeping this\n//? map here means neither the reporter nor computeTotals need to hand-code the\n//? mapping.\nconst SWEEP_SUMMARY_PROP: Record<SweepLayerKey, keyof Pick<RunAllTestsSummary, 'contract' | 'auth' | 'rateLimit' | 'csrf' | 'fuzz'>> = {\n [LAYER_KEYS.contract]: 'contract',\n [LAYER_KEYS.auth]: 'auth',\n [LAYER_KEYS.rateLimit]: 'rateLimit',\n [LAYER_KEYS.csrf]: 'csrf',\n [LAYER_KEYS.fuzz]: 'fuzz',\n};\n\n//? Iterate every sweep layer + the custom layer to sum passed/failed.\n//? DD-TR-various — xpass exit-code policy (documented decision):\n//? `xpass` (a case marked `expectedToFail` that unexpectedly passes) is NOT\n//? counted in `totalFailed`. Rationale: an xpass is a positive signal — the\n//? known issue is gone and the marker just needs removing. Treating it as a\n//? failure would make CI red when a bug is fixed, which is backwards.\n//? It IS shown prominently in the summary line and the \"Unexpectedly passed\"\n//? section so the stale marker can't rot silently behind a green run.\n//? To opt into strict mode (xpass = CI failure), inspect `summary.custom?.xpassed`\n//? in your own script and `process.exit(1)` when it is > 0.\nconst computeTotals = (summary: RunAllTestsSummary): void => {\n let passed = 0;\n let failed = 0;\n for (const key of SWEEP_LAYER_ORDER) {\n const s = summary[SWEEP_SUMMARY_PROP[key]];\n passed += s?.passed ?? 0;\n failed += s?.failed ?? 0;\n }\n summary.totalPassed = passed + (summary.custom?.passed ?? 0);\n summary.totalFailed = failed + (summary.custom?.failed ?? 0);\n};\n\nexport const runAllTests = async (input: RunAllTestsInput): Promise<RunAllTestsSummary> => {\n const summary: RunAllTestsSummary = { totalPassed: 0, totalFailed: 0 };\n\n //? Warn when a non-empty apiMethodMap is paired with an empty apiMetaMap.\n //? The auth-enforcement and rate-limit layers both use the meta map to decide\n //? which routes require login and which have declared rate limits. An absent\n //? or stale map silently skips every auth/rate-limit check — the run looks\n //? green but provides no real coverage.\n if (!input.noSweep) {\n const hasRoutes = Object.keys(input.apiMethodMap).length > 0;\n const hasMeta = Object.keys(input.apiMetaMap).length > 0;\n if (hasRoutes && !hasMeta) {\n console.warn(\n '[test-runner] runAllTests: apiMetaMap is empty but apiMethodMap has routes. '\n + 'The auth-enforcement and rate-limit layers will skip every route. '\n + 'Pass the generated apiMetaMap to enable those checks.',\n );\n }\n }\n\n const headers = await buildAuthHeaders(input);\n const inputFor = inputForEndpoint(input.apiInputSchemas);\n\n if (!input.noSweep) {\n await runSweepLayers(input, summary, headers, inputFor);\n }\n\n if (!input.noCustom) {\n await runCustomLayer(input, summary);\n }\n\n computeTotals(summary);\n\n return summary;\n};\n\n//? ── Colored, list-based reporting ───────────────────────────────────────\n//? Honors NO_COLOR / FORCE_COLOR (https://no-color.org) and falls back to\n//? plain text when stdout is not a TTY (piped to a file / CI log).\nconst useColor = ((): boolean => {\n if (process.env.NO_COLOR) return false;\n if (process.env.FORCE_COLOR) return true;\n return process.stdout.isTTY;\n})();\nconst paint = (code: string, text: string): string => (useColor ? `\\u001B[${code}m${text}\\u001B[0m` : text);\nconst green = (t: string): string => paint('32', t);\nconst red = (t: string): string => paint('31', t);\nconst yellow = (t: string): string => paint('33', t);\nconst dim = (t: string): string => paint('2', t);\nconst bold = (t: string): string => paint('1', t);\n\ninterface ReportRow {\n label: string;\n reason?: string;\n}\n\n//? DD-TR-various — leaky summary union tightened:\n//? `LayerSummary` is the minimal shape the reporter cares about — the fields\n//? every layer summary has in common. The extra-field helpers discriminate via\n//? `in` to stay type-safe without widening callers to `unknown`.\ninterface LayerSummary {\n total: number;\n passed: number;\n failed: number;\n}\n\nconst xfailOf = (s: LayerSummary | undefined): number => {\n if (s === undefined || !('xfailed' in s)) return 0;\n const v = (s as Record<string, unknown>).xfailed;\n return typeof v === 'number' ? v : 0;\n};\nconst skippedOf = (s: LayerSummary | undefined): number => {\n if (s === undefined || !('skipped' in s)) return 0;\n const v = (s as Record<string, unknown>).skipped;\n return typeof v === 'number' ? v : 0;\n};\n\n//? Per-layer headline: green \"X/Y passed\" when clean, red \"Z failed\" when not,\n//? plus dim xfail / skipped counts. This is the \"14/20 in groen, 6/20 in rood\"\n//? the report leads with.\nconst formatLayerLine = (label: string, s: LayerSummary | undefined): string => {\n if (!s) return ` ${dim('–')} ${label.padEnd(18)} ${dim('(layer skipped)')}`;\n const clean = s.failed === 0;\n const mark = clean ? green('✓') : red('✗');\n const head = clean\n ? green(`${s.passed}/${s.total} passed`)\n : `${green(`${s.passed}/${s.total} passed`)} ${red(`${s.failed}/${s.total} failed`)}`;\n const extras: string[] = [];\n const xf = xfailOf(s);\n const sk = skippedOf(s);\n if (xf) extras.push(yellow(`${xf} expected-fail`));\n if (sk) extras.push(dim(`${sk} skipped`));\n const extra = extras.length > 0 ? ` ${dim('·')} ${extras.join(dim(', '))}` : '';\n return ` ${mark} ${label.padEnd(18)} ${head}${extra}`;\n};\n\nconst sweepFailRows = (s: RunContractSummary | undefined): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === 'fail')\n .map((r) => ({\n label: `${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,\n reason: r.errorCode ? `${r.reason ?? 'failed'} [${r.errorCode}]` : r.reason,\n }));\n\nconst sweepSkipRows = (layer: string, s: RunContractSummary | undefined): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === 'skipped')\n .map((r) => ({\n label: `${layer}: ${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,\n reason: r.reason,\n }));\n\nconst customRows = (s: RunCustomTestsSummary | undefined, status: CustomTestResult['status']): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === status)\n .map((r) => ({ label: `${r.routePath} :: ${r.caseName}`, reason: r.reason }));\n\nconst printSection = (title: (t: string) => string, heading: string, rows: ReportRow[], bullet: string): void => {\n if (rows.length === 0) return;\n console.log('');\n console.log(title(bold(`${heading} (${rows.length})`)));\n for (const row of rows) {\n console.log(` ${title(bullet)} ${row.label}`);\n if (row.reason) console.log(` ${dim(row.reason)}`);\n }\n};\n\nexport const logRunAllSummary = (summary: RunAllTestsSummary): void => {\n console.log('');\n\n //? Print a headline line for every sweep layer (ordered) then the custom layer.\n for (const key of SWEEP_LAYER_ORDER) {\n console.log(formatLayerLine(key, summary[SWEEP_SUMMARY_PROP[key]]));\n }\n console.log(formatLayerLine(LAYER_KEYS.custom, summary.custom));\n\n //? Real failures across every layer — these MUST be fixed (a green test that\n //? went red, or a wrong test). Sweep failures + un-marked custom failures.\n const failed: ReportRow[] = [\n ...SWEEP_LAYER_ORDER.flatMap((key) => sweepFailRows(summary[SWEEP_SUMMARY_PROP[key]])),\n ...customRows(summary.custom, 'fail'),\n ];\n printSection(red, 'Failed — must be fixed (real bugs / wrong tests)', failed, '✗');\n\n //? Known, accepted failures — red is OK here; they are tracked, not regressions.\n printSection(yellow, 'Expected failures — known issues, allowed to fail', customRows(summary.custom, 'xfail'), '⚠');\n\n //? Stale markers — these passed but are still flagged expectedToFail.\n printSection(yellow, 'Unexpectedly passed — remove the expectedToFail marker', customRows(summary.custom, 'xpass'), '?');\n\n //? Skipped with reason — not run in this mode (login-gated, high rate-limit, …).\n const skipped: ReportRow[] = SWEEP_LAYER_ORDER.flatMap((key) =>\n sweepSkipRows(key, summary[SWEEP_SUMMARY_PROP[key]]),\n );\n printSection(dim, 'Skipped — not run (with reason)', skipped, '–');\n\n //? Sum xfail and skipped across all layers (sweep + custom) in one pass.\n let totalXfail = 0;\n let totalSkipped = 0;\n for (const key of SWEEP_LAYER_ORDER) {\n const s = summary[SWEEP_SUMMARY_PROP[key]];\n totalXfail += xfailOf(s);\n totalSkipped += skippedOf(s);\n }\n totalXfail += xfailOf(summary.custom);\n totalSkipped += skippedOf(summary.custom);\n\n //? `xpass` (a case still marked expectedToFail that now passes) is NOT a\n //? failure, so it doesn't move the exit code — but it MUST be visible in the\n //? final line, otherwise a stale marker rots silently behind a green run.\n const totalXpass = summary.custom?.xpassed ?? 0;\n\n console.log('');\n const parts = [\n green(`${summary.totalPassed} passed`),\n summary.totalFailed > 0 ? red(`${summary.totalFailed} failed`) : dim('0 failed'),\n totalXfail > 0 ? yellow(`${totalXfail} expected-fail`) : dim('0 expected-fail'),\n totalXpass > 0 ? yellow(`${totalXpass} unexpectedly-passed`) : dim('0 unexpectedly-passed'),\n totalSkipped > 0 ? yellow(`${totalSkipped} skipped`) : dim('0 skipped'),\n ];\n console.log(`Summary: ${parts.join(dim(' · '))}`);\n console.log(dim(' legend: ✗ red = must fix · ⚠ yellow = known/allowed · – skipped (not run)'));\n};\n","//? Extension registry for the test runner. Consumers plug in custom test\n//? layers, realistic-payload fixtures, and result reporters without\n//? forking the framework.\n//?\n//? Three slots:\n//?\n//? 1. Test layers — additional checks per endpoint (CORS enforcement,\n//? business rules, custom auth schemes, multi-tenant isolation).\n//? 2. Fixtures — realistic input payloads (valid + invalid) per typeKey\n//? so the fuzz layer can use them before falling back to schema-random.\n//? 3. Reporters — per-result and per-summary callbacks + optional\n//? webhook URL the runner POSTs the batch summary to.\n//?\n//? All slots are append-only at registration time; clearing requires\n//? `resetTestExtensionsForTests()` (kept out of the production surface\n//? to prevent accidental wipes from a misbehaving plugin).\n\nexport interface TestLayerInput {\n /** Resolved route name (`api/billing/getInvoice/v1`). */\n endpoint: string;\n /** Method when applicable, undefined for sync endpoints. */\n method?: string;\n /** Bearer token / session token to use for auth-aware checks. */\n authToken?: string;\n}\n\nexport interface TestLayerResult {\n passed: boolean;\n /** Short reason key — i18n-style errorCode when failed. */\n message?: string;\n /** Optional extras for the reporter (latency, response sample, ...). */\n metadata?: Record<string, unknown>;\n}\n\nexport interface TestLayer {\n name: string;\n run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;\n}\n\nexport interface TestFixture<TPayload = unknown> {\n valid: TPayload[];\n invalid: TPayload[];\n}\n\nexport interface TestResult {\n layer: string;\n endpoint: string;\n passed: boolean;\n message?: string;\n durationMs?: number;\n metadata?: Record<string, unknown>;\n}\n\nexport interface TestSummary {\n totalLayers: number;\n totalEndpoints: number;\n passed: number;\n failed: number;\n results: TestResult[];\n}\n\nexport interface TestReporter {\n /** Called once per individual TestResult as the runner progresses. */\n onResult?: (result: TestResult) => void | Promise<void>;\n /** Called once at the end with the aggregated summary. */\n onSummary?: (summary: TestSummary) => void | Promise<void>;\n /** Optional webhook the runner POSTs the summary to. */\n webhookUrl?: string;\n webhookAuth?: { type: 'bearer'; token: string };\n}\n\nconst layers = new Map<string, TestLayer>();\nconst fixtures = new Map<string, TestFixture>();\nlet reporter: TestReporter | null = null;\n\n/**\n * Register a custom test layer. Re-registering the same name replaces\n * the previous entry. Use for CORS enforcement, business-rule checks,\n * custom auth schemes, multi-tenant isolation, GDPR data-flow checks.\n */\nexport const registerTestLayer = (layer: TestLayer): void => {\n layers.set(layer.name, layer);\n};\n\nexport const listTestLayers = (): TestLayer[] => [...layers.values()];\n\n/**\n * Register a realistic payload fixture for a specific type key. The fuzz\n * layer prefers these over schema-random generated inputs when a fixture\n * exists for the endpoint's input type.\n *\n * `valid` payloads are used to verify happy-path behavior; `invalid`\n * payloads probe the validator's error responses.\n */\nexport const registerTestFixture = <TPayload = unknown>(typeKey: string, fixture: TestFixture<TPayload>): void => {\n fixtures.set(typeKey, fixture);\n};\n\nexport const getTestFixture = (typeKey: string): TestFixture | undefined => fixtures.get(typeKey);\n\n/**\n * Register a test reporter. Set `null` to unregister. The runner calls\n * `onResult` per individual result as the run progresses and `onSummary`\n * once at the end. When `webhookUrl` is set, the runner additionally\n * POSTs the JSON-serialised summary to that URL.\n */\nexport const registerTestReporter = (newReporter: TestReporter | null): void => {\n reporter = newReporter;\n};\n\nexport const getTestReporter = (): TestReporter | null => reporter;\n\nexport const resetTestExtensionsForTests = (): void => {\n layers.clear();\n fixtures.clear();\n reporter = null;\n};\n"],"mappings":"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","names":["DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","EXPECTED_ERROR_CODE","tryCatch","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","tryCatch","tryCatch","tryCatch","getCsrfConfig","getProjectConfig","getProjectConfig","getCsrfConfig"]}
|
|
1
|
+
{"version":3,"sources":["../src/walkEndpoints.ts","../src/probeRequest.ts","../src/contractCheck.ts","../src/testLayerHelpers.ts","../src/runContractTests.ts","../src/authEnforcementCheck.ts","../src/runAuthEnforcementTests.ts","../src/rateLimitCheck.ts","../src/resetServerState.ts","../src/runRateLimitTests.ts","../src/csrfEnforcementCheck.ts","../src/runCsrfEnforcementTests.ts","../src/fuzzCheck.ts","../src/runFuzzTests.ts","../src/schemaSampleInput.ts","../src/reporter.ts","../src/customTests.ts","../src/streamWatcher.ts","../src/runAllTests.ts","../src/extensionRegistry.ts"],"sourcesContent":["import type { EndpointDescriptor, HttpMethod, SyncMethodMap } from './types';\n\ntype ApiMethodMap = Partial<Record<string, Partial<Record<string, Partial<Record<string, string>>>>>>;\n\n//? Must stay in sync with the `HttpMethod` union in types.ts. Only values that\n//? appear in both sets are valid probes — anything else is an unrecognised method.\nconst VALID_HTTP_METHODS = new Set<HttpMethod>(['GET', 'POST', 'PUT', 'DELETE']);\n\nconst toHttpMethod = (raw: string): HttpMethod | null => {\n //? Guard against a stale or hand-edited generated map carrying an unrecognised\n //? method string. An unknown value would propagate silently as a typed HttpMethod\n //? and break every probe that uses it. Return null so the caller can skip it.\n const upper = raw.toUpperCase();\n for (const m of VALID_HTTP_METHODS) {\n if (m === upper) return m;\n }\n return null;\n};\n\nexport const walkEndpoints = (apiMethodMap: ApiMethodMap): EndpointDescriptor[] => {\n const endpoints: EndpointDescriptor[] = [];\n for (const [page, nameMap] of Object.entries(apiMethodMap)) {\n if (!nameMap) continue;\n for (const [name, versionMap] of Object.entries(nameMap)) {\n if (!versionMap) continue;\n for (const [version, rawMethod] of Object.entries(versionMap)) {\n if (rawMethod === undefined) continue;\n const method = toHttpMethod(rawMethod);\n if (!method) {\n //? Unrecognised method in the generated map — log and skip rather than\n //? silently coercing, which would produce invalid probes.\n console.warn(`[test-runner] walkEndpoints: skipping api/${page}/${name}/${version} — unrecognised method '${rawMethod}' in apiMethodMap`);\n continue;\n }\n endpoints.push({\n page,\n name,\n version,\n method,\n fullPath: `api/${page}/${name}/${version}`,\n });\n }\n }\n }\n return endpoints;\n};\n\n//? Walk the generated `syncMethodMap` and produce a flat list of sync-route\n//? descriptors. Each descriptor's `fullPath` is `sync/<page>/<name>/<version>`.\n//? Sync routes always use POST over the HTTP-fallback transport regardless of\n//? the declared method, so the method is hardcoded to `POST` here.\nexport const walkSyncEndpoints = (syncMethodMap: SyncMethodMap): EndpointDescriptor[] => {\n const endpoints: EndpointDescriptor[] = [];\n for (const [page, nameMap] of Object.entries(syncMethodMap)) {\n if (!nameMap) continue;\n for (const [name, versionMap] of Object.entries(nameMap)) {\n if (!versionMap) continue;\n for (const [version, method] of Object.entries(versionMap)) {\n if (method === undefined) continue;\n endpoints.push({\n page,\n name,\n version,\n method: 'POST',\n fullPath: `sync/${page}/${name}/${version}`,\n });\n }\n }\n }\n return endpoints;\n};\n","//? Single-source fetch probe used by every check layer (contract, auth,\n//? csrf, rate-limit). Centralises the AbortController + timeout + tryCatch\n//? pattern so it isn't copied into each check file.\n\nimport { tryCatch } from '@luckystack/core';\nimport type { HttpMethod } from './types';\n\nexport interface ProbeRequestInput {\n url: string;\n method: HttpMethod;\n baseUrl: string;\n body?: unknown;\n headers?: Record<string, string>;\n requestTimeoutMs: number;\n}\n\nexport interface ProbeResponse {\n httpStatus: number;\n parsed: { status?: string; errorCode?: string } | null;\n}\n\n/**\n * Send a single timed fetch probe and parse the JSON envelope. Returns\n * `null` when the request itself fails (network error or abort).\n */\nexport const sendProbe = async (input: ProbeRequestInput): Promise<ProbeResponse | null> => {\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, input.requestTimeoutMs);\n\n const [fetchError, response] = await tryCatch(() => fetch(input.url, {\n method: input.method,\n headers: {\n 'Content-Type': 'application/json',\n //? Browsers attach Origin on state-changing requests; the server's origin\n //? policy fail-closes POST/PUT/DELETE without one.\n 'Origin': new URL(input.baseUrl).origin,\n ...input.headers,\n },\n body: input.method === 'GET' ? undefined : JSON.stringify(input.body),\n signal: controller.signal,\n }));\n clearTimeout(timeoutHandle);\n\n if (fetchError || !response) return null;\n\n const [, parsed] = await tryCatch<{ status?: string; errorCode?: string } | null, undefined>(\n async () => await response.json() as { status?: string; errorCode?: string },\n );\n\n return { httpStatus: response.status, parsed: parsed ?? null };\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\nexport interface ContractCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n /**\n * Optional input factory. Defaults to `{}`. When an endpoint requires a\n * specific shape to not crash, callers can override per-endpoint here.\n */\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /**\n * Optional headers (auth tokens, cookies). Applied to every request.\n */\n headers?: Record<string, string>;\n /**\n * Per-call request timeout in ms. Defaults to 5000. Bump for slow endpoints\n * (AI calls, large reports) where the framework default would false-fail\n * the contract check.\n */\n requestTimeoutMs?: number;\n}\n\n//? Contract layer: send any-shape input, accept any response where\n//? `status` is `success` or `error` (with an `errorCode`). Anything else is\n//? a contract violation — 5xx, thrown exceptions, or shapes outside this\n//? envelope.\n//?\n//? The goal here is NOT to prove every endpoint works end-to-end; it's to\n//? catch endpoints that crash the server or return garbage on well-formed\n//? empty input. Real behavior tests go in the per-endpoint test file.\nexport const runContractCheck = async (input: ContractCheckInput): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: input.headers,\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return {\n endpoint,\n status: 'fail',\n reason: 'fetch returned no response',\n durationMs: Date.now() - started,\n };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (!parsed) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'JSON parse failed',\n durationMs,\n };\n }\n\n if (parsed.status !== 'success' && parsed.status !== 'error') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'Response missing standard `status` envelope',\n durationMs,\n };\n }\n\n if (parsed.status === 'error' && !parsed.errorCode) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'error',\n reason: 'Error response missing `errorCode`',\n durationMs,\n };\n }\n\n //? When `inputFor` produced a non-empty sample body but the route returned an\n //? error, the probe may have failed its own Zod validation rather than proving\n //? the contract. Counting that as a PASS is misleading — the happy-path was\n //? never exercised. Classify as `skipped` so the caller knows the body was\n //? rejected before the handler ran and can supply a better sample.\n const bodyIsNonEmpty = body !== null\n && typeof body === 'object'\n && !Array.isArray(body)\n && Object.keys(body).length > 0;\n if (input.inputFor && bodyIsNonEmpty && parsed.status === 'error') {\n return {\n endpoint,\n status: 'skipped',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n reason: `inputFor produced a non-empty sample but route returned error (${parsed.errorCode ?? 'unknown'}); provide a valid sample via inputFor to test the happy path`,\n durationMs,\n };\n }\n\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: parsed.status,\n errorCode: parsed.errorCode,\n durationMs,\n };\n};\n","//? Shared helpers for the sweep test layers (contract, auth-enforcement,\n//? rate-limit, fuzz). Centralizes the skip-matching, meta-map queries, and\n//? summary-count math that were previously copy-pasted across each layer file.\n\nimport type {\n ApiMetaMap,\n ContractCheckResult,\n EndpointDescriptor,\n HttpMethod,\n RunContractSummary,\n} from './types';\n\n//? Single source of truth for the canonical layer names used in both the\n//? orchestrator (`runAllTests`) and the reporter (`logRunAllSummary`). Keeping\n//? them here prevents silent drift between the reporting labels and the summary\n//? property keys when layers are added or renamed.\nexport const LAYER_KEYS = {\n contract: 'contract',\n auth: 'auth-enforcement',\n rateLimit: 'rate-limit',\n csrf: 'csrf-enforcement',\n fuzz: 'fuzz',\n custom: 'custom',\n} as const;\n\n//? HTTP methods that carry a body and mutate server state. The CSRF middleware\n//? enforces a token only on these. Fuzz / rate-limit layers share this set to\n//? avoid firing junk bodies at mutating authenticated routes in cookie-mode.\nexport const STATE_CHANGING_METHODS = new Set<HttpMethod>(['POST', 'PUT', 'DELETE']);\n\n/**\n * Whether an endpoint is in the explicit skip list. Matched against\n * `<page>/<name>` (version-agnostic) and `<page>/<name>/<version>`\n * (version-specific).\n */\nexport const shouldSkip = (endpoint: EndpointDescriptor, skip: string[]): boolean => {\n if (skip.length === 0) return false;\n const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;\n const versionless = `${endpoint.page}/${endpoint.name}`;\n return skip.includes(versioned) || skip.includes(versionless);\n};\n\n/** Whether the endpoint's meta entry declares `auth.login: true`. */\nexport const requiresLogin = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): boolean => {\n const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n //? `apiMetaMap` is a consumer-generated RUNTIME artifact — the static type\n //? declares `auth` as required, but a stale/partial entry can be missing it.\n //? Read defensively so a malformed entry surfaces as `false` (no login) and\n //? gets handled by the layer's skip path, instead of throwing a TypeError\n //? outside the per-endpoint tryCatch and aborting the whole layer.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime-defensive read of a generated artifact\n return meta?.auth?.login ?? false;\n};\n\n/** Whether the meta map carries an entry for this endpoint at all. */\nexport const hasMetaEntry = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): boolean =>\n apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version] !== undefined;\n\n/**\n * The endpoint's declared numeric rate limit, or `null` when it has none\n * (`false`/`undefined`) or no meta entry exists.\n */\nexport const getRateLimit = (apiMetaMap: ApiMetaMap, endpoint: EndpointDescriptor): number | null => {\n const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n if (!meta || meta.rateLimit === false || meta.rateLimit === undefined) return null;\n return meta.rateLimit;\n};\n\n/** Aggregate pass/fail/skip counts over a sweep layer's results. */\nexport const calculateSummary = (results: ContractCheckResult[]): RunContractSummary => ({\n total: results.length,\n passed: results.filter(r => r.status === 'pass').length,\n failed: results.filter(r => r.status === 'fail').length,\n skipped: results.filter(r => r.status === 'skipped').length,\n results,\n});\n","import { walkEndpoints } from './walkEndpoints';\nimport { runContractCheck } from './contractCheck';\nimport { shouldSkip, calculateSummary } from './testLayerHelpers';\nimport type { ApiMethodMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunContractTestsInput {\n apiMethodMap: ApiMethodMap;\n baseUrl: string;\n /**\n * Endpoints to skip. Matched against `<page>/<name>` (version-agnostic) and\n * `<page>/<name>/<version>` (version-specific). Useful for endpoints that\n * are known to need real input (e.g. a file upload).\n */\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runContractTests = async (input: RunContractTestsInput): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n //? Serial rather than parallel so we don't trip rate limiters or mask\n //? endpoint ordering bugs. Swap to a pool later if walk-time matters.\n const result = await runContractCheck({\n endpoint,\n baseUrl: input.baseUrl,\n inputFor: input.inputFor,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Canonical error code the framework emits for unauthenticated requests\n//? to `auth.login: true` endpoints. See packages/api/src/handleHttpApiRequest.ts.\nconst EXPECTED_ERROR_CODE = 'auth.required';\n\nexport interface AuthEnforcementCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n}\n\n//? Auth-enforcement layer: for each `auth.login: true` endpoint, call it with\n//? NO session headers/cookies. Expect the server to reject with `auth.required`.\n//?\n//? A passing endpoint here proves the login guard isn't silently bypassed.\n//? Endpoints that return `success` without a session are a critical finding.\nexport const runAuthEnforcementCheck = async (\n input: AuthEnforcementCheckInput,\n): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n\n //? No session cookie, no auth header — deliberately. Origin IS sent so the\n //? server's origin policy doesn't 403 before the auth check can run.\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return {\n endpoint,\n status: 'fail',\n reason: 'fetch returned no response',\n durationMs: Date.now() - started,\n };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (parsed?.status === 'success') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'success',\n reason: 'auth.login endpoint returned success without a session',\n durationMs,\n };\n }\n\n if (parsed?.status !== 'error') {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'unknown',\n reason: 'Response missing standard `status` envelope',\n durationMs,\n };\n }\n\n if (parsed.errorCode !== EXPECTED_ERROR_CODE) {\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n reason: `expected errorCode '${EXPECTED_ERROR_CODE}' but got '${parsed.errorCode ?? '(missing)'}'`,\n durationMs,\n };\n }\n\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n durationMs,\n };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runAuthEnforcementCheck } from './authEnforcementCheck';\nimport { shouldSkip, requiresLogin, calculateSummary } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunAuthEnforcementTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runAuthEnforcementTests = async (\n input: RunAuthEnforcementTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n //? Order matters: requiresLogin must run BEFORE shouldSkip so that a\n //? login-required endpoint in the explicit skip list is still recorded as\n //? `skipped` in the results (not silently dropped as a public route).\n //? Reversing the order would hide the skip from the summary.\n if (!requiresLogin(input.apiMetaMap, endpoint)) {\n //? Public endpoints can't be tested by this layer — skip silently, no\n //? noise. They're still covered by the contract layer.\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runAuthEnforcementCheck({\n endpoint,\n baseUrl: input.baseUrl,\n inputFor: input.inputFor,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\nconst EXPECTED_ERROR_CODE = 'api.rateLimitExceeded';\n\n//? Error codes the server emits BEFORE the rate-limit pipeline runs (CSRF gate,\n//? auth guard, schema validation). When a drain response carries one of these, the\n//? bucket was never incremented — the final probe will fail for the wrong reason\n//? and should be classified as `skipped` rather than a real failure.\nconst PRE_LIMITER_ERROR_CODES = new Set([\n 'auth.required',\n 'auth.csrfTokenMissing',\n 'auth.csrfTokenInvalid',\n 'auth.unauthorized',\n 'api.csrfTokenMissing',\n 'api.csrfTokenInvalid',\n 'api.unauthorized',\n 'api.validationError',\n]);\n\nexport interface RateLimitCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n rateLimit: number;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n /** Per-call request timeout in ms. Defaults to 10000. */\n requestTimeoutMs?: number;\n}\n\n//? Rate-limit layer: fire `rateLimit + 1` requests back to back and assert the\n//? last one is rejected with `api.rateLimitExceeded`. All preceding requests\n//? are allowed to return either success or any other envelope — only the N+1\n//? matters for this layer.\n//?\n//? This layer mutates server-side state (limiter counters). Run it after the\n//? contract + auth layers, or against a dedicated test instance. The server's\n//? clearAllRateLimits() utility can reset state between runs.\nexport const runRateLimitCheck = async (input: RateLimitCheckInput): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl, rateLimit } = input;\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\n const send = () => sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: input.headers,\n requestTimeoutMs,\n });\n\n //? Fire `rateLimit` allowed requests first (drain the bucket), then a final\n //? request that should be blocked. Serial so the server sees a clean order.\n //? Inspect EVERY drain response for pre-limiter rejections (CSRF/auth/\n //? validation): if the server rejects before the limiter runs at any point,\n //? the bucket was never incremented for that request, the final probe will\n //? fail for the wrong reason, and we classify the check as `skipped`.\n for (let i = 0; i < rateLimit; i += 1) {\n const drained = await send();\n if (!drained) continue;\n const errorCode = drained.parsed?.errorCode;\n if (errorCode && PRE_LIMITER_ERROR_CODES.has(errorCode)) {\n return {\n endpoint,\n status: 'skipped',\n durationMs: Date.now() - started,\n reason: `drain request ${i + 1} rejected pre-limiter (${errorCode}); bucket was never incremented — check CSRF/auth headers`,\n };\n }\n }\n\n const final = await send();\n const durationMs = Date.now() - started;\n\n if (!final) {\n return {\n endpoint,\n status: 'fail',\n reason: 'Final rate-limit probe request failed to return a response',\n durationMs,\n };\n }\n\n const { httpStatus, parsed } = final;\n\n if (parsed?.status === 'error' && parsed.errorCode === EXPECTED_ERROR_CODE) {\n return {\n endpoint,\n status: 'pass',\n httpStatus,\n responseStatus: 'error',\n errorCode: parsed.errorCode,\n durationMs,\n };\n }\n\n const responseStatus: ContractCheckResult['responseStatus'] =\n parsed?.status === 'success' || parsed?.status === 'error' ? parsed.status : 'unknown';\n return {\n endpoint,\n status: 'fail',\n httpStatus,\n responseStatus,\n errorCode: parsed?.errorCode,\n reason: `expected '${EXPECTED_ERROR_CODE}' on request ${rateLimit + 1} but got ${parsed?.status ?? 'none'}/${parsed?.errorCode ?? '(none)'}`,\n durationMs,\n };\n};\n","import { tryCatch } from '@luckystack/core';\n\n//? Calls the server's /_test/reset endpoint. Used between rate-limit test\n//? endpoints to drain the shared IP-based limiter bucket so one endpoint's\n//? N+1 assertion doesn't consume the next endpoint's window.\n//?\n//? /_test/reset is gated by `NODE_ENV in { 'development', 'test' }` on the\n//? server side AND requires `TEST_RESET_TOKEN` to be set unconditionally\n//? (no token = 403). For staging/preview deploys that enable it for CI,\n//? set `TEST_RESET_TOKEN` on the server and pass the same value here.\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\nexport interface ResetServerStateInput {\n baseUrl: string;\n token?: string;\n requestTimeoutMs?: number;\n}\n\nexport const resetServerState = async ({ baseUrl, token, requestTimeoutMs }: ResetServerStateInput): Promise<boolean> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/_test/reset`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n if (token) headers['X-Test-Reset-Token'] = token;\n\n //? Bound the call so a server that accepts the connection but never responds\n //? returns `false` (the existing failure path) instead of wedging the sweep.\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);\n const [error, response] = await tryCatch(() => fetch(url, { method: 'POST', headers, signal: controller.signal }));\n clearTimeout(timeoutHandle);\n if (error || !response) return false;\n return response.ok;\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runRateLimitCheck } from './rateLimitCheck';\nimport { resetServerState } from './resetServerState';\nimport { shouldSkip, requiresLogin, getRateLimit, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunRateLimitTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n headers?: Record<string, string>;\n /**\n * Max rateLimit value to test. Endpoints with a higher limit are skipped\n * to avoid firing thousands of requests in CI.\n */\n maxRateLimitToTest?: number;\n /**\n * When true, hit `/_test/reset` before each endpoint so limiter state is\n * clean. Requires the server to allow the endpoint (NODE_ENV !== production\n * or `TEST_RESET_TOKEN` configured). Without resetting, the shared IP bucket\n * leaks state between endpoints.\n */\n resetBetweenEndpoints?: boolean;\n /**\n * Token forwarded as `X-Test-Reset-Token` when resetting. Only relevant in\n * staging/preview deploys that enable the endpoint behind a token.\n */\n resetToken?: string;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runRateLimitTests = async (\n input: RunRateLimitTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const maxRateLimit = input.maxRateLimitToTest ?? 50;\n //? A login-required route returns `auth.required` BEFORE the rate limiter\n //? runs, so it can only be rate-limit-tested with a session. A Cookie header\n //? means an authenticated sweep. We deliberately do NOT auto-authenticate the\n //? sweep — that would execute real mutations (delete/update) with junk input.\n //? Set TEST_AUTH_TOKEN to opt in and cover login-gated routes.\n const isAuthenticatedSweep = Boolean(input.headers?.Cookie);\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n const rateLimit = getRateLimit(input.apiMetaMap, endpoint);\n if (rateLimit === null) continue;\n\n if (!isAuthenticatedSweep && requiresLogin(input.apiMetaMap, endpoint)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'login-required route — set TEST_AUTH_TOKEN to rate-limit-test it (unauthenticated calls hit auth.required before the limiter)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n //? In cookie-mode the rate-limit check fires the endpoint `rateLimit + 1`\n //? times with junk input. State-changing routes (POST/PUT/DELETE) execute\n //? real mutation handlers on each hit — skip them to avoid side-effects on\n //? the test database. Their rate limits are better covered by a custom test\n //? that supplies valid input.\n if (isAuthenticatedSweep && STATE_CHANGING_METHODS.has(endpoint.method)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'state-changing route skipped in cookie-mode rate-limit sweep (mutation risk with junk bodies)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (rateLimit > maxRateLimit) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: `rateLimit ${rateLimit} exceeds maxRateLimitToTest=${maxRateLimit}`,\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (input.resetBetweenEndpoints) {\n await resetServerState({ baseUrl: input.baseUrl, token: input.resetToken });\n }\n\n const result = await runRateLimitCheck({\n endpoint,\n baseUrl: input.baseUrl,\n rateLimit,\n inputFor: input.inputFor,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Canonical error code the framework's CSRF middleware emits when a\n//? state-changing, cookie-authenticated request arrives without a matching\n//? CSRF token. See packages/server/src/httpRoutes/csrfMiddleware.ts.\nconst DEFAULT_EXPECTED_ERROR_CODE = 'auth.csrfMismatch';\nconst DEFAULT_EXPECTED_HTTP_STATUS = 403;\n\nexport interface CsrfEnforcementCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n /**\n * A valid session Cookie header (`<name>=<token>`). The probe deliberately\n * carries it so the request passes the auth guard and reaches the CSRF check.\n */\n authCookie: string;\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n /** Error code expected on CSRF rejection. Defaults to `auth.csrfMismatch`. */\n expectedErrorCode?: string;\n /** HTTP status expected on CSRF rejection. Defaults to 403; pass `false` to skip. */\n expectedHttpStatus?: number | false;\n}\n\n//? CSRF-enforcement layer: for each `auth.login: true` state-changing route,\n//? send an authenticated request (valid session Cookie) WITHOUT the CSRF\n//? header. Expect the framework's CSRF middleware to reject it. This is the\n//? inverse of the custom layer's CSRF token-passing — it proves the protection\n//? actually fires, so a consumer who misconfigures CSRF doesn't get a green run.\nexport const runCsrfEnforcementCheck = async (\n input: CsrfEnforcementCheckInput,\n): Promise<ContractCheckResult> => {\n const { endpoint, baseUrl } = input;\n //? CSRF is only enforced on state-changing methods (POST/PUT/DELETE). A GET\n //? probe would never reach the CSRF middleware regardless of headers, so it\n //? would always appear to pass — a false green. Guard here so the check is\n //? safe to call directly (the runner already filters, but direct callers may not).\n if (endpoint.method === 'GET') {\n return {\n endpoint,\n status: 'skipped',\n reason: `CSRF enforcement does not apply to ${endpoint.method} requests`,\n durationMs: 0,\n };\n }\n const url = `${baseUrl.replace(/\\/$/, '')}/${endpoint.fullPath}`;\n const body = input.inputFor ? input.inputFor(endpoint) : {};\n const started = Date.now();\n const expectedErrorCode = input.expectedErrorCode ?? DEFAULT_EXPECTED_ERROR_CODE;\n const expectedHttpStatus = input.expectedHttpStatus ?? DEFAULT_EXPECTED_HTTP_STATUS;\n\n //? Valid session Cookie + Origin, but NO CSRF header — that omission is the\n //? whole point of the probe.\n const result = await sendProbe({\n url,\n method: endpoint.method,\n baseUrl,\n body,\n headers: { Cookie: input.authCookie },\n requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (!result) {\n return { endpoint, status: 'fail', reason: 'fetch returned no response', durationMs: Date.now() - started };\n }\n\n const { httpStatus, parsed } = result;\n const durationMs = Date.now() - started;\n\n if (parsed?.status === 'success') {\n return {\n endpoint, status: 'fail', httpStatus, responseStatus: 'success',\n reason: 'state-changing endpoint accepted an authenticated request with NO CSRF header — CSRF protection is not enforced',\n durationMs,\n };\n }\n\n if (parsed?.status !== 'error' || parsed.errorCode !== expectedErrorCode) {\n const responseStatus: ContractCheckResult['responseStatus'] =\n parsed?.status === 'success' || parsed?.status === 'error' ? parsed.status : 'unknown';\n return {\n endpoint, status: 'fail', httpStatus, responseStatus, errorCode: parsed?.errorCode,\n reason: `expected CSRF rejection '${expectedErrorCode}' but got ${parsed?.status ?? 'none'}/${parsed?.errorCode ?? '(none)'}`,\n durationMs,\n };\n }\n\n if (expectedHttpStatus !== false && httpStatus !== expectedHttpStatus) {\n return {\n endpoint, status: 'fail', httpStatus, responseStatus: 'error', errorCode: parsed.errorCode,\n reason: `expected HTTP ${expectedHttpStatus} on CSRF rejection but got ${httpStatus}`,\n durationMs,\n };\n }\n\n return { endpoint, status: 'pass', httpStatus, responseStatus: 'error', errorCode: parsed.errorCode, durationMs };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runCsrfEnforcementCheck } from './csrfEnforcementCheck';\nimport { shouldSkip, requiresLogin, hasMetaEntry, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\n\nexport interface RunCsrfEnforcementTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n baseUrl: string;\n /** Valid session Cookie header (`<name>=<token>`) the probes carry so they reach the CSRF check. */\n authCookie: string;\n skip?: string[];\n inputFor?: (endpoint: EndpointDescriptor) => unknown;\n /** Per-call request timeout in ms forwarded to each check. */\n requestTimeoutMs?: number;\n /** Error code expected on CSRF rejection. Defaults to `auth.csrfMismatch`. */\n expectedErrorCode?: string;\n /** HTTP status expected on CSRF rejection. Defaults to 403; pass `false` to skip. */\n expectedHttpStatus?: number | false;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runCsrfEnforcementTests = async (\n input: RunCsrfEnforcementTestsInput,\n): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n //? Only login-required, state-changing routes are CSRF-protected. A missing\n //? meta entry means we can't know the auth requirement — skip silently here\n //? (the auth layer already surfaces the stale-map gap).\n if (!hasMetaEntry(input.apiMetaMap, endpoint)) continue;\n if (!requiresLogin(input.apiMetaMap, endpoint)) continue;\n if (!STATE_CHANGING_METHODS.has(endpoint.method)) continue;\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runCsrfEnforcementCheck({\n endpoint,\n baseUrl: input.baseUrl,\n authCookie: input.authCookie,\n inputFor: input.inputFor,\n requestTimeoutMs: input.requestTimeoutMs,\n expectedErrorCode: input.expectedErrorCode,\n expectedHttpStatus: input.expectedHttpStatus,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { ContractCheckResult, EndpointDescriptor } from './types';\nimport { sendProbe } from './probeRequest';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 5000;\n\n//? Lightweight fuzz layer — no Zod yet. Sends well-known junk shapes and\n//? asserts the server doesn't 5xx or return a non-envelope body. Real\n//? schema-driven fuzzing (TS type -> random valid input) is deferred until\n//? the generator emits Zod schemas alongside the types.\nconst JUNK_PAYLOADS: readonly unknown[] = [\n null,\n [],\n [1, 2, 3],\n 'string-instead-of-object',\n 1_234_567_890,\n true,\n { nested: { deeply: { nested: { value: 'x'.repeat(10_000) } } } },\n //? `{ __proto__: ... }` serializes to `{}` via JSON.stringify (prototype keys\n //? are not own-enumerable), so it would probe nothing. Use `constructor` instead\n //? — a named own-key that some parsers misinterpret but survives JSON round-trip.\n { constructor: { polluted: true } },\n { key: null, other: undefined, third: Number.NaN },\n];\n\nexport interface FuzzCheckInput {\n endpoint: EndpointDescriptor;\n baseUrl: string;\n headers?: Record<string, string>;\n /** Per-call request timeout in ms. Defaults to 5000. */\n requestTimeoutMs?: number;\n}\n\nexport const runFuzzCheck = async (input: FuzzCheckInput): Promise<ContractCheckResult> => {\n const started = Date.now();\n const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const url = `${input.baseUrl.replace(/\\/$/, '')}/${input.endpoint.fullPath}`;\n\n for (const payload of JUNK_PAYLOADS) {\n const result = await sendProbe({\n url,\n method: input.endpoint.method,\n baseUrl: input.baseUrl,\n body: payload,\n headers: input.headers,\n requestTimeoutMs,\n });\n const durationMs = Date.now() - started;\n\n if (!result) {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n reason: `fuzz probe crashed (no response) with payload: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n\n //? A 5xx that STILL returns a valid `{ status:'error', errorCode }` envelope\n //? is the framework gracefully catching an error (e.g. a route designed to\n //? throw), not a crash. The fuzz layer's concern is crashes / hangs /\n //? garbage — so we only fail on a non-envelope body below (which catches raw\n //? 5xx, HTML error pages, and truncated responses). A controlled 500\n //? envelope passes.\n if (!result.parsed || (result.parsed.status !== 'success' && result.parsed.status !== 'error')) {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n httpStatus: result.httpStatus,\n reason: `fuzz payload produced non-envelope response: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n\n if (result.parsed.status === 'error' && typeof result.parsed.errorCode !== 'string') {\n return {\n endpoint: input.endpoint,\n status: 'fail',\n httpStatus: result.httpStatus,\n reason: `fuzz error response missing errorCode for payload: ${JSON.stringify(payload).slice(0, 80)}`,\n durationMs,\n };\n }\n }\n\n return {\n endpoint: input.endpoint,\n status: 'pass',\n durationMs: Date.now() - started,\n };\n};\n","import { walkEndpoints } from './walkEndpoints';\nimport { runFuzzCheck } from './fuzzCheck';\nimport { shouldSkip, calculateSummary, STATE_CHANGING_METHODS } from './testLayerHelpers';\nimport type { ApiMethodMap, ContractCheckResult, RunContractSummary } from './types';\n\nexport interface RunFuzzTestsInput {\n apiMethodMap: ApiMethodMap;\n baseUrl: string;\n skip?: string[];\n headers?: Record<string, string>;\n onResult?: (result: ContractCheckResult) => void;\n}\n\nexport const runFuzzTests = async (input: RunFuzzTestsInput): Promise<RunContractSummary> => {\n const endpoints = walkEndpoints(input.apiMethodMap);\n const skip = input.skip ?? [];\n //? In cookie-mode (authenticated sweep) junk bodies sent to state-changing\n //? routes (POST/PUT/DELETE) bypass the body-ignore short-circuit and reach real\n //? mutation handlers — risking unintended side-effects on the test database.\n //? Skip those endpoints here; the contract layer already validates their shape\n //? with well-formed input, and the CSRF layer probes their auth guard separately.\n const isAuthenticatedSweep = Boolean(input.headers?.Cookie);\n const results: ContractCheckResult[] = [];\n\n for (const endpoint of endpoints) {\n if (isAuthenticatedSweep && STATE_CHANGING_METHODS.has(endpoint.method)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'state-changing route skipped in cookie-mode fuzz sweep (mutation risk with junk bodies)',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n if (shouldSkip(endpoint, skip)) {\n const skipped: ContractCheckResult = {\n endpoint,\n status: 'skipped',\n durationMs: 0,\n reason: 'Explicitly skipped',\n };\n results.push(skipped);\n input.onResult?.(skipped);\n continue;\n }\n\n const result = await runFuzzCheck({\n endpoint,\n baseUrl: input.baseUrl,\n headers: input.headers,\n });\n results.push(result);\n input.onResult?.(result);\n }\n\n return calculateSummary(results);\n};\n","import type { z } from 'zod';\n\n//? Walk a Zod schema and produce a minimal valid value. Used by the\n//? contract/fuzz runners so \"valid input\" tests don't require hand-written\n//? fixtures per endpoint. Handles the shapes the generator emits\n//? (primitives, literals, unions, optionals, objects, records, arrays).\n//?\n//? Not a property-based generator — returns the SAME deterministic value\n//? every call. For randomized fuzz, swap to `fast-check` in a later session.\n\n//? zod 4 renamed the internal def accessor from `._def` to `.def`; recurse\n//? through whichever a child node exposes so the walker handles both versions.\nconst innerDef = (node: { _def?: unknown; def?: unknown } | undefined): unknown =>\n node ? (node.def ?? node._def) : undefined;\n\nconst sampleForDef = (def: unknown): unknown => {\n //? Zod stores its runtime shape on `_def.type` (v3) or `_def.typeName`.\n //? Access is untyped because `def` can be any of dozens of internal shapes.\n const d = def as {\n typeName?: string;\n type?: string;\n innerType?: { _def?: unknown; def?: unknown };\n values?: unknown[] | Record<string, unknown>;\n entries?: Record<string, unknown>;\n value?: unknown;\n options?: { _def?: unknown; def?: unknown }[];\n shape?: (() => Record<string, { _def?: unknown; def?: unknown }>) | Record<string, { _def?: unknown; def?: unknown }>;\n valueType?: { _def: unknown };\n element?: { _def: unknown };\n items?: { _def?: unknown; def?: unknown }[];\n schema?: { _def?: unknown; def?: unknown };\n in?: { _def?: unknown; def?: unknown };\n };\n\n const name = d.typeName ?? d.type;\n\n switch (name) {\n case 'ZodString':\n case 'string': {\n return 'test';\n }\n case 'ZodNumber':\n case 'number': {\n return 0;\n }\n case 'ZodBoolean':\n case 'boolean': {\n return false;\n }\n case 'ZodNull':\n case 'null': {\n return null;\n }\n case 'ZodUndefined':\n case 'undefined': {\n return undefined;\n }\n case 'ZodAny':\n case 'any':\n case 'ZodUnknown':\n case 'unknown': {\n return null;\n }\n case 'ZodLiteral':\n case 'literal': {\n //? zod 3 stores the literal on `_def.value`; zod 4 stores `_def.values` (array).\n return d.value ?? (Array.isArray(d.values) ? d.values[0] : undefined);\n }\n case 'ZodOptional':\n case 'optional':\n case 'ZodNullable':\n case 'nullable': {\n //? Omitting optional fields is the most conservative valid input.\n return undefined;\n }\n case 'ZodEnum':\n case 'enum':\n case 'ZodNativeEnum':\n case 'nativeEnum': {\n //? Return the first declared value so an enum-typed required field gets a\n //? VALID member rather than `null` (which the route's own Zod rejects,\n //? silently turning the contract probe into a validation-error test).\n //? zod 3 stores members on `_def.values` (array); zod 4 on `_def.entries`\n //? (object) or `_def.values` (Set/array).\n if (Array.isArray(d.values)) return d.values[0];\n const entries = d.entries ?? (d.values && !Array.isArray(d.values) ? d.values : undefined);\n if (entries) {\n const first = Object.values(entries)[0];\n if (first !== undefined) return first;\n }\n return undefined;\n }\n case 'ZodDefault':\n case 'default':\n case 'ZodCatch':\n case 'catch': {\n //? Unwrap to the inner schema's sample (the field is still required at the\n //? type level; the default only applies when omitted).\n return sampleForDef(innerDef(d.innerType));\n }\n case 'ZodEffects':\n case 'effects':\n case 'ZodPipeline':\n case 'pipe':\n case 'ZodBranded':\n case 'branded':\n case 'ZodReadonly':\n case 'readonly': {\n //? `.refine`/`.transform`/`.pipe`/`.brand`/`.readonly` wrap an inner\n //? schema — recurse into it. A `.refine` may still reject the sample, but\n //? recursing yields a structurally-valid base far more often than `null`.\n return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType));\n }\n case 'ZodTuple':\n case 'tuple': {\n const items = d.items ?? [];\n return items.map(item => sampleForDef(innerDef(item)));\n }\n case 'ZodUnion':\n case 'union':\n case 'ZodDiscriminatedUnion':\n case 'discriminatedUnion': {\n return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0])) : undefined;\n }\n case 'ZodArray':\n case 'array': {\n return [];\n }\n case 'ZodObject':\n case 'object': {\n const out: Record<string, unknown> = {};\n //? zod 3 exposes `_def.shape` as a function; zod 4 as a plain object.\n const shape = typeof d.shape === 'function' ? d.shape() : (d.shape ?? {});\n for (const [key, valueSchema] of Object.entries(shape)) {\n const sample = sampleForDef(innerDef(valueSchema));\n if (sample !== undefined) out[key] = sample;\n }\n return out;\n }\n case 'ZodRecord':\n case 'record': {\n return {};\n }\n case 'ZodDate':\n case 'date': {\n return new Date().toISOString();\n }\n default: {\n //? Unknown node: OMIT (undefined) rather than emit `null`. A `null` for a\n //? required field of an unhandled type makes the route's own Zod reject\n //? the body, silently turning the \"happy path\" contract probe into a\n //? validation-error test. Omitting lets the object walker drop the key so\n //? the probe only fails if the field was genuinely required (a real,\n //? visible signal) instead of always.\n return undefined;\n }\n }\n};\n\nexport const sampleSchemaInput = (schema: z.ZodType): unknown => {\n //? zod 4 renamed the public def accessor from `._def` to `.def`; fall back to\n //? `._def` (zod 3) so the top-level entry handles both, matching the\n //? recursive `innerDef` helper.\n const node = schema as { def?: unknown; _def?: unknown };\n return sampleForDef(node.def ?? node._def);\n};\n","import type { ContractCheckResult, RunContractSummary } from './types';\n\nconst statusIcon = (status: ContractCheckResult['status']): string => {\n switch (status) {\n case 'pass': { return '[PASS]';\n }\n case 'fail': { return '[FAIL]';\n }\n case 'skipped': { return '[SKIP]';\n }\n }\n};\n\nexport const logContractResult = (result: ContractCheckResult): void => {\n const { endpoint, durationMs, httpStatus, responseStatus, errorCode, reason } = result;\n const icon = statusIcon(result.status);\n const core = `${icon} ${endpoint.method} /${endpoint.fullPath} ${durationMs}ms`;\n\n if (result.status === 'pass') {\n const tail = responseStatus === 'error' ? ` (error: ${errorCode ?? 'unknown'})` : '';\n console.log(`${core} http=${httpStatus ?? '-'}${tail}`);\n return;\n }\n\n if (result.status === 'skipped') {\n console.log(`${core} ${reason ?? ''}`);\n return;\n }\n\n console.log(`${core} http=${httpStatus ?? '-'} reason=${reason ?? '(none)'}`);\n};\n\nexport const logContractSummary = (summary: RunContractSummary): void => {\n const { total, passed, failed, skipped } = summary;\n console.log('');\n console.log(`Contract tests: ${passed}/${total} passed, ${failed} failed, ${skipped} skipped`);\n\n if (failed > 0) {\n console.log('');\n console.log('Failures:');\n for (const result of summary.results) {\n if (result.status !== 'fail') continue;\n console.log(` - /${result.endpoint.fullPath}: ${result.reason ?? '(no reason)'}`);\n }\n }\n};\n","//? Per-route business-logic test layer. Discovers `*.tests.ts` files\n//? alongside `_api/` and `_sync/` route source, dynamic-imports each,\n//? builds a route-bound `TestContext`, runs the exported `customTests`\n//? cases. Spec: docs/ARCHITECTURE_TESTING.md.\n\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { getCsrfConfig, getPrismaClient, getProjectConfig, getSrcDir, tryCatch, tryCatchSync } from '@luckystack/core';\n\nimport {\n openStreamWatcher,\n type StreamChunkFrame,\n type StreamWatcher,\n} from './streamWatcher';\n//? `ApiMethodMap` mirrors the generated `apiMethodMap` shape (page → name →\n//? version → method) and is a type-only import, so there's still no import-time\n//? coupling to the consumer's generated artifact.\nimport type { ApiMethodMap } from './types';\n\nconst API_TEST_FILE_PATTERN = /_v(\\d+)\\.tests\\.ts$/;\nconst SYNC_SERVER_TEST_FILE_PATTERN = /_server_v(\\d+)\\.tests\\.ts$/;\n\n//? Default per-request timeout for the Layer-5 call helpers. Mirrors the sweep\n//? layers so a hung (non-erroring) server can't wedge the whole custom-test run\n//? indefinitely — without it the case's own tryCatch never fires.\nconst DEFAULT_CALL_TIMEOUT_MS = 10_000;\n\nexport interface CustomTestCase {\n name: string;\n run: (ctx: TestContext) => Promise<void>;\n /**\n * Mark this case as a KNOWN, accepted failure (e.g. a documented bug not\n * yet fixed, or a scenario the current implementation cannot satisfy). The\n * string is the reason shown in the report. A case marked `expectedToFail`\n * that throws is reported as `xfail` — counted separately, NOT a red\n * failure. If it unexpectedly PASSES it is reported as `xpass` so the stale\n * marker can be removed. Leave undefined for normal cases.\n */\n expectedToFail?: string;\n}\n\nexport interface CustomTestResult {\n routePath: string;\n caseName: string;\n //? `xfail` = marked expectedToFail and failed as expected (known issue).\n //? `xpass` = marked expectedToFail but passed (remove the marker).\n status: 'pass' | 'fail' | 'xfail' | 'xpass';\n durationMs: number;\n reason?: string;\n /** Documented reason when the case carried an `expectedToFail` marker. */\n expectedToFail?: string;\n}\n\nexport interface RunCustomTestsSummary {\n total: number;\n passed: number;\n failed: number;\n /** Cases marked `expectedToFail` that failed as expected (known issues). */\n xfailed: number;\n /** Cases marked `expectedToFail` that unexpectedly passed (remove the marker). */\n xpassed: number;\n results: CustomTestResult[];\n}\n\nexport interface RunCustomTestsInput {\n baseUrl: string;\n /** Optional cookie name for the session token. Defaults to the project's configured cookie. */\n sessionCookieName?: string;\n /** Filter to a subset of routes — substring match against `<page>/<name>/<version>`. */\n filter?: string;\n /**\n * Generated `apiMethodMap` so the harness sends each API route its declared\n * HTTP method (e.g. logout is DELETE). Falls back to POST when absent.\n */\n apiMethodMap?: ApiMethodMap;\n onResult?: (result: CustomTestResult) => void;\n}\n\ninterface DiscoveredTestFile {\n /** Absolute path to the test file. */\n filePath: string;\n /** `api` or `sync` — which transport this route uses. */\n kind: 'api' | 'sync';\n /** Route descriptor parsed from the filename + directory location. */\n route: { page: string; name: string; version: string };\n}\n\n//? Walk `src/` recursively. A file matches when its directory contains an\n//? `_api/` or `_sync/` segment AND the filename matches the version suffix\n//? pattern. The route's `page` is everything between `src/` and the marker\n//? segment, joined with `/`. Nested page folders are supported (e.g.\n//? `src/admin/users/_api/getUser_v1.tests.ts` → page `admin/users`).\nexport const discoverCustomTestFiles = (srcDir: string = getSrcDir()): DiscoveredTestFile[] => {\n if (!fs.existsSync(srcDir)) return [];\n const found: DiscoveredTestFile[] = [];\n const stack: string[] = [srcDir];\n //? Track visited real-paths so a cyclic symlink under `src/` can't drive an\n //? infinite walk / unbounded stack growth.\n const visited = new Set<string>();\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined) break;\n //? Resolve to the canonical path and skip if already seen (symlink cycle).\n const [realError, realPath] = tryCatchSync(() => fs.realpathSync(current));\n const canonical = realError || realPath === null ? current : realPath;\n if (visited.has(canonical)) continue;\n visited.add(canonical);\n //? Guard the readdir: a permission error or a directory deleted mid-walk\n //? throws synchronously and would otherwise abort the whole custom layer.\n //? Skip the unreadable directory instead.\n const [readError, entries] = tryCatchSync(() => fs.readdirSync(current, { withFileTypes: true }));\n if (readError || entries === null) continue;\n for (const entry of entries) {\n const fullPath = path.join(current, entry.name);\n //? Skip symlinked entries entirely — following them risks a cycle and\n //? a discovered test file should live in the real source tree.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n stack.push(fullPath);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.tests.ts')) continue;\n const parentDir = path.basename(current);\n if (parentDir !== '_api' && parentDir !== '_sync') continue;\n const pattern = parentDir === '_api' ? API_TEST_FILE_PATTERN : SYNC_SERVER_TEST_FILE_PATTERN;\n const versionMatch = pattern.exec(entry.name);\n if (!versionMatch) continue;\n const version = `v${versionMatch[1]}`;\n const namePart = entry.name.replace(parentDir === '_api' ? /_v\\d+\\.tests\\.ts$/ : /_server_v\\d+\\.tests\\.ts$/, '');\n const pageDir = path.dirname(current);\n //? Top-level `src/_api/` (no page folder) routes under the framework's\n //? `system` page — mirror that, otherwise an empty page produces the\n //? malformed URL `/api//logout/v1` and the call 404s.\n const relativePage = path.relative(srcDir, pageDir).replaceAll('\\\\', '/') || 'system';\n found.push({\n filePath: fullPath,\n kind: parentDir === '_api' ? 'api' : 'sync',\n route: { page: relativePage, name: namePart, version },\n });\n }\n }\n return found.toSorted((a, b) => a.filePath.localeCompare(b.filePath));\n};\n\n//? Test fixture shapes. The TestContext is built per route — each per-route\n//? test file's `customTests` cases receive a context already bound to the\n//? route the file lives next to, so authors don't repeat `page/name/version`.\nexport interface TestContext {\n /** Invoke the route under test. Page / name / version are baked in. */\n //? Two type parameters by design — `TInput` lets callers document the\n //? typed shape they're sending even though we don't validate against it\n //? here (the route's own runtime validation does). The ESLint rule\n //? otherwise flags it as \"used once\"; the documentation value is real.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n callApi: <TInput = unknown, TOutput = unknown>(input: TInput) => Promise<TOutput>;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n callSync: <TInput = unknown, TOutput = unknown>(input: TInput, opts?: { receiver?: string }) => Promise<TOutput>;\n /**\n * Open a second socket connection that joins `roomCode` as a pure\n * subscriber and exposes the incoming chunk stream for assertions.\n *\n * The route is auto-derived from the test file location, so a watcher\n * only surfaces chunks for the route this test belongs to. Multiple\n * watchers per test are allowed; all are auto-closed when the test\n * case finishes (whether it passes or throws).\n */\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-documentation use\n watchStream: <TChunk extends StreamChunkFrame = StreamChunkFrame>(roomCode: string) => Promise<StreamWatcher<TChunk>>;\n session: {\n login: (user?: { email?: string; id?: string; name?: string }) => Promise<{ token: string; userId: string }>;\n logout: () => Promise<void>;\n current: () => { token: string | null; userId: string | null };\n };\n prisma: ReturnType<typeof getPrismaClient>;\n expect: TestExpect;\n}\n\nexport interface TestExpect {\n eq: <T>(actual: T, expected: T, message?: string) => void;\n ok: (value: unknown, message?: string) => void;\n throws: (fn: () => Promise<unknown>, message?: string) => Promise<Error>;\n matches: (value: string, pattern: RegExp, message?: string) => void;\n}\n\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\nconst buildExpect = (): TestExpect => ({\n eq: (actual, expected, message) => {\n const ok = Object.is(actual, expected)\n || (typeof actual === 'object' && typeof expected === 'object' && JSON.stringify(actual) === JSON.stringify(expected));\n if (!ok) {\n const prefix = message ? `${message}: ` : '';\n throw new AssertionError(`${prefix}expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);\n }\n },\n ok: (value, message) => {\n if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);\n },\n throws: async (fn, message) => {\n const [err] = await tryCatch(fn);\n if (!err) throw new AssertionError(message ?? 'expected fn to throw, but it did not');\n return err;\n },\n matches: (value, pattern, message) => {\n if (!pattern.test(value)) {\n const prefix = message ? `${message}: ` : '';\n throw new AssertionError(`${prefix}\"${value}\" does not match ${pattern}`);\n }\n },\n});\n\ninterface TestSessionState {\n token: string | null;\n userId: string | null;\n //? CSRF token minted by saveSession on login. Sent on authenticated\n //? state-changing requests so the server's CSRF middleware doesn't 403.\n csrfToken: string | null;\n //? Raw JSON body of the most recent callApi / callSync response. Surfaced in\n //? a failed case's reason so the report shows the server's actual errorCode\n //? (e.g. `api.rateLimitExceeded`) instead of only the assertion text.\n lastResponse: unknown;\n}\n\n//? Pull the server's errorCode (or non-success status) out of the last HTTP\n//? response so a failure's reason shows WHY the server rejected the call, not\n//? just the assertion text. Returns undefined for success / non-envelope bodies.\nconst extractErrorCode = (response: unknown): string | undefined => {\n if (!response || typeof response !== 'object') return undefined;\n const r = response as { errorCode?: unknown; status?: unknown };\n if (typeof r.errorCode === 'string' && r.errorCode.length > 0) return r.errorCode;\n if (typeof r.status === 'string' && r.status !== 'success') return r.status;\n return undefined;\n};\n\n//? Minimal session login: mints a session in Redis via the consumer's\n//? registered SessionAdapter (from `@luckystack/login`). Deliberately does\n//? NOT create the Prisma user record — User-table shape varies per\n//? project. If your test needs a real user row, create it via\n//? `ctx.prisma.user.create(...)` first, then pass `{ id, email }` to\n//? `session.login()`.\nconst buildSessionHelpers = (state: TestSessionState): TestContext['session'] => {\n return {\n login: async (user) => {\n const { saveSession, getSession } = await import('@luckystack/login');\n const userId = user?.id ?? `test-user-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;\n const email = user?.email ?? `test-${Date.now()}-${crypto.randomBytes(8).toString('hex')}@example.com`;\n const name = user?.name ?? 'Test User';\n const token = `test-${Date.now()}-${crypto.randomBytes(16).toString('hex')}`;\n const sessionData = {\n id: userId,\n email,\n name,\n token,\n provider: 'credentials' as const,\n };\n await saveSession(token, sessionData, true);\n state.token = token;\n state.userId = userId;\n //? Read back the CSRF token saveSession minted so authenticated\n //? state-changing requests can pass the server's CSRF middleware.\n const persisted = await getSession(token);\n state.csrfToken = persisted?.csrfToken ?? null;\n return { token, userId };\n },\n logout: async () => {\n if (!state.token) return;\n const { deleteSession } = await import('@luckystack/login');\n await deleteSession(state.token);\n state.token = null;\n state.userId = null;\n state.csrfToken = null;\n },\n current: () => ({ token: state.token, userId: state.userId }),\n };\n};\n\n//? Parse a fetch Response as JSON behind tryCatch and store the raw body on the\n//? session state. A non-JSON body (HTML 500, empty 204, truncated) would\n//? otherwise throw a raw SyntaxError with `state.lastResponse` null, hiding the\n//? server's actual output. On parse failure we keep a snippet of the text body\n//? so a failed case's reason can show what the server actually returned.\nconst parseResponse = async (response: Response, state: TestSessionState): Promise<unknown> => {\n const [textError, text] = await tryCatch(() => response.text());\n if (textError || text === null) {\n state.lastResponse = null;\n throw new Error(`failed to read response body (HTTP ${response.status}): ${textError?.message ?? 'unknown'}`);\n }\n const [parseError, parsed] = tryCatchSync(() => JSON.parse(text) as unknown);\n if (parseError) {\n const snippet = text.length > 500 ? `${text.slice(0, 500)}…` : text;\n state.lastResponse = snippet;\n throw new Error(`response was not valid JSON (HTTP ${response.status}): ${snippet}`);\n }\n state.lastResponse = parsed;\n return parsed;\n};\n\nconst buildCallApi = (\n baseUrl: string,\n routePath: string,\n state: TestSessionState,\n cookieName: string,\n //? The route's declared HTTP method (from the generated apiMethodMap). The\n //? server validates the method, so DELETE/PUT routes (e.g. logout) 405 if we\n //? always sent POST.\n method: string,\n): TestContext['callApi'] => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- must match TestContext['callApi'] generic signature\n return async <TInput = unknown, TOutput = unknown>(input: TInput): Promise<TOutput> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/api/${routePath}`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json', Origin: new URL(baseUrl).origin };\n if (state.token) headers.Cookie = `${cookieName}=${state.token}`;\n if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, DEFAULT_CALL_TIMEOUT_MS);\n const init: RequestInit = { method, headers, signal: controller.signal };\n //? GET / HEAD requests cannot carry a body (fetch throws). Every other\n //? method sends the JSON payload.\n if (method !== 'GET' && method !== 'HEAD') init.body = JSON.stringify(input ?? {});\n const [fetchError, response] = await tryCatch(() => fetch(url, init));\n clearTimeout(timeoutHandle);\n if (fetchError || !response) {\n state.lastResponse = null;\n throw new Error(`callApi ${method} ${routePath} failed: ${fetchError?.message ?? 'no response'}`);\n }\n //? `parseResponse` returns `unknown`; TOutput is the caller-supplied expectation.\n //? The runtime shape is unverifiable here — the route's own validator owns that\n //? contract. The cast is required because parseResponse must return `unknown`.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax -- parseResponse returns unknown; TOutput is the caller's type contract, verified at the call site\n return await parseResponse(response, state) as TOutput;\n };\n};\n\nconst buildCallSync = (\n baseUrl: string,\n routePath: string,\n state: TestSessionState,\n cookieName: string,\n): TestContext['callSync'] => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- must match TestContext['callSync'] generic signature\n return async <TInput = unknown, TOutput = unknown>(input: TInput, opts?: { receiver?: string }): Promise<TOutput> => {\n const url = `${baseUrl.replace(/\\/$/, '')}/sync/${routePath}`;\n const headers: Record<string, string> = { 'Content-Type': 'application/json', Origin: new URL(baseUrl).origin };\n if (state.token) headers.Cookie = `${cookieName}=${state.token}`;\n if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;\n const body = { data: input ?? {}, receiver: opts?.receiver };\n const controller = new AbortController();\n const timeoutHandle = setTimeout(() => { controller.abort(); }, DEFAULT_CALL_TIMEOUT_MS);\n const [fetchError, response] = await tryCatch(() => fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n }));\n clearTimeout(timeoutHandle);\n if (fetchError || !response) {\n state.lastResponse = null;\n throw new Error(`callSync ${routePath} failed: ${fetchError?.message ?? 'no response'}`);\n }\n //? parseResponse returns unknown; TOutput is the caller's type contract, verified at the call site\n // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax -- parseResponse returns unknown; cast to TOutput is the boundary\n return await parseResponse(response, state) as TOutput;\n };\n};\n\ninterface BuiltContext {\n ctx: TestContext;\n /** Close every watcher opened during the case. Called by `runCustomTests` after each case. */\n closeAllWatchers: () => Promise<void>;\n /** Per-case session state — read after the run to surface the last response's errorCode. */\n state: TestSessionState;\n}\n\nconst buildContext = (\n discovery: DiscoveredTestFile,\n input: RunCustomTestsInput,\n): BuiltContext => {\n //? Default to the PROJECT's configured session cookie name (the server reads\n //? the token from this cookie). Hardcoding a name silently breaks every\n //? authenticated custom test when the project uses a different name.\n const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;\n const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;\n const routeFullName = `sync/${routePath}`;\n //? API routes are invoked with their declared HTTP method; sync routes always\n //? POST. Default to POST when the map lacks the route (e.g. sync, or a map\n //? wasn't threaded through).\n const apiMethod = input.apiMethodMap?.[discovery.route.page]?.[discovery.route.name]?.[discovery.route.version] ?? 'POST';\n const state: TestSessionState = { token: null, userId: null, csrfToken: null, lastResponse: null };\n const prisma = getPrismaClient();\n\n //? Watchers are tracked per case so a missing explicit `close()` can't\n //? leak socket B between tests.\n const watchers: StreamWatcher[] = [];\n const watchStream = async <TChunk extends StreamChunkFrame = StreamChunkFrame>(\n roomCode: string,\n ): Promise<StreamWatcher<TChunk>> => {\n const watcher = await openStreamWatcher<TChunk>({\n baseUrl: input.baseUrl,\n roomCode,\n token: state.token,\n routeFullName,\n });\n //? Track watchers under the base `StreamChunkFrame` shape; the\n //? returned reference keeps the caller-supplied generic.\n watchers.push(watcher);\n return watcher;\n };\n\n const closeAllWatchers = async (): Promise<void> => {\n while (watchers.length > 0) {\n const watcher = watchers.pop();\n if (!watcher) continue;\n const [closeError] = await tryCatch(() => watcher.close());\n if (closeError) {\n //? Swallow — closing in cleanup must never mask the original test\n //? failure. Surface via logger if a project wires one up.\n //? (Intentionally not using getLogger here to avoid an extra import.)\n }\n }\n };\n\n const ctx: TestContext = {\n callApi: buildCallApi(input.baseUrl, routePath, state, cookieName, apiMethod),\n callSync: buildCallSync(input.baseUrl, routePath, state, cookieName),\n watchStream,\n session: buildSessionHelpers(state),\n prisma,\n expect: buildExpect(),\n };\n\n return { ctx, closeAllWatchers, state };\n};\n\nexport const runCustomTests = async (input: RunCustomTestsInput): Promise<RunCustomTestsSummary> => {\n const srcDir = getSrcDir();\n const files = discoverCustomTestFiles(srcDir);\n const results: CustomTestResult[] = [];\n\n for (const discovery of files) {\n const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;\n if (input.filter && !routePath.includes(input.filter)) continue;\n\n //? Canonical-path containment guard: resolve to the real path and verify\n //? it is still inside srcDir. Protects against symlink-escaped paths or\n //? an adversarially crafted filePath that slipped through the walker.\n const [realPathErr, realFilePath] = tryCatchSync(() => fs.realpathSync(discovery.filePath));\n const resolvedPath = realPathErr ? discovery.filePath : (realFilePath ?? discovery.filePath);\n const relToSrc = path.relative(srcDir, resolvedPath);\n if (relToSrc.startsWith('..') || path.isAbsolute(relToSrc)) {\n const r: CustomTestResult = {\n routePath, caseName: '(import)', status: 'fail', durationMs: 0,\n reason: `test file path escapes srcDir and was rejected: ${discovery.filePath}`,\n };\n results.push(r);\n input.onResult?.(r);\n continue;\n }\n\n const [importError, mod] = await tryCatch(\n () => import(pathToFileURL(resolvedPath).href) as Promise<Record<string, unknown>>,\n );\n if (importError || !mod) {\n const r: CustomTestResult = {\n routePath, caseName: '(import)', status: 'fail', durationMs: 0,\n reason: `failed to import ${discovery.filePath}: ${importError?.message ?? 'unknown'}`,\n };\n results.push(r);\n input.onResult?.(r);\n continue;\n }\n\n const exported = (mod as { customTests?: unknown }).customTests;\n //? DD-TR-various — empty-stub contract (documented decision):\n //? A test file that exports an empty array (or no `customTests` export) is\n //? silently skipped, NOT emitted as a `skipped` result. Rationale: AI-generated\n //? stub files start life empty; surfacing them as \"skipped\" every run adds noise\n //? before the author fills them in. The `CustomTestResult` status union therefore\n //? stays `'pass' | 'fail' | 'xfail' | 'xpass'` — no `'skipped'` variant.\n //? If you need to track unfilled stubs, scan `discoverCustomTestFiles()` and\n //? compare against the results array.\n if (!Array.isArray(exported) || exported.length === 0) {\n continue;\n }\n\n for (const rawCase of exported) {\n const c = rawCase as CustomTestCase;\n if (typeof c.name !== 'string' || typeof c.run !== 'function') continue;\n const expectedToFail = typeof c.expectedToFail === 'string' ? c.expectedToFail : undefined;\n const built = buildContext(discovery, input);\n const started = Date.now();\n const [runError] = await tryCatch(async () => c.run(built.ctx));\n //? Always close watchers regardless of pass/fail so the next case\n //? starts clean. tryCatch on the cleanup itself prevents a teardown\n //? throw from masking the original failure.\n await tryCatch(() => built.closeAllWatchers());\n //? Best-effort session cleanup: if `session.login()` minted a Redis\n //? session but the case didn't call `session.logout()`, the token\n //? accumulates until its TTL expires. Delete it now to avoid a slow\n //? session-key leak across a full sweep run.\n const residualToken = built.state.token;\n if (residualToken) {\n await tryCatch(async () => {\n const { deleteSession } = await import('@luckystack/login');\n await deleteSession(residualToken);\n });\n }\n const durationMs = Date.now() - started;\n\n let r: CustomTestResult;\n if (runError) {\n //? Augment the assertion message with the server's actual errorCode so\n //? the report explains the rejection, not just the failed expectation.\n const errorCode = extractErrorCode(built.state.lastResponse);\n const reason = errorCode ? `${runError.message} (server: ${errorCode})` : runError.message;\n r = expectedToFail\n ? { routePath, caseName: c.name, status: 'xfail', durationMs, reason, expectedToFail }\n : { routePath, caseName: c.name, status: 'fail', durationMs, reason };\n } else {\n r = expectedToFail\n ? {\n routePath, caseName: c.name, status: 'xpass', durationMs,\n reason: `marked expectedToFail (\"${expectedToFail}\") but passed — remove the marker`,\n expectedToFail,\n }\n : { routePath, caseName: c.name, status: 'pass', durationMs };\n }\n results.push(r);\n input.onResult?.(r);\n }\n }\n\n return {\n total: results.length,\n passed: results.filter(r => r.status === 'pass').length,\n failed: results.filter(r => r.status === 'fail').length,\n xfailed: results.filter(r => r.status === 'xfail').length,\n xpassed: results.filter(r => r.status === 'xpass').length,\n results,\n };\n};\n","//? Second-socket harness for Layer 5 custom tests. Opens a separate\n//? socket.io-client connection (socket B), joins the receiver room as a\n//? pure subscriber, and exposes the broadcast/streamTo chunk stream for\n//? assertions. Without this, `ctx.callSync` only surfaces the final\n//? envelope — chunks (LLM tokens, progress ticks) emitted via\n//? `broadcastStream` / `streamTo` flow on the `sync` event channel to\n//? room members other than the request originator's socket.\n//?\n//? Spec: see `docs/contract-tests.md` §watchStream and CLAUDE Rule 21\n//? (no `as unknown as X` / `as any` — typed generics throughout).\n//?\n//? Wire shape, sourced from `@luckystack/sync` `_shared/streamEmitters.ts`:\n//? io.to(receiver).emit('sync', { ...payload, cb, fullName, status: 'stream' })\n//? where `fullName` = `sync/<page>/<name>/<version>`. We filter incoming\n//? frames by that `fullName` so a watcher only sees chunks for the route\n//? its test file lives next to.\n\nimport { io as createSocketIoClient, Socket } from 'socket.io-client';\nimport {\n buildJoinRoomResponseEventName,\n socketEventNames,\n tryCatch,\n} from '@luckystack/core';\n\nexport interface StreamChunkFrame {\n /** Per-request correlation id stamped by the server. */\n cb?: string;\n /** Full route name (`sync/<page>/<name>/<version>`). */\n fullName?: string;\n /** Always `'stream'` for chunk frames. */\n status?: 'stream';\n /** Payload fields spread on top of the envelope. */\n [key: string]: unknown;\n}\n\nexport interface StreamWatcher<TChunk extends StreamChunkFrame = StreamChunkFrame> {\n /** Chunks received so far. Array reference is stable; entries are pushed in arrival order. */\n readonly chunks: TChunk[];\n /** Wait until `predicate(chunks)` returns true, or until timeout (default 5000ms). */\n stopAt(predicate: (chunks: TChunk[]) => boolean, timeoutMs?: number): Promise<void>;\n /** Wait until N total chunks received, or until timeout. */\n waitForCount(n: number, timeoutMs?: number): Promise<void>;\n /** Close socket B. Tests SHOULD call this in cleanup; ctx auto-closes all watchers when the test ends. */\n close(): Promise<void>;\n}\n\nexport interface OpenStreamWatcherInput {\n /** Server URL (same as `ctx`'s `baseUrl`). */\n baseUrl: string;\n /** Room to join as a pure subscriber. */\n roomCode: string;\n /** Active session token (used for `joinRoom` auth). May be null for routes that don't require auth. */\n token: string | null;\n /**\n * Resolved route name `sync/<page>/<name>/<version>` — chunk frames are\n * filtered by their `fullName` property so a watcher only surfaces chunks\n * for the route its test is bound to.\n */\n routeFullName: string;\n /** Default 5000ms — overridable per `stopAt` / `waitForCount` call. */\n defaultTimeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst JOIN_RESPONSE_TIMEOUT_MS = 3000;\nconst CONNECT_TIMEOUT_MS = 3000;\n\ninterface JoinRoomResponse {\n status?: string;\n errorCode?: string;\n}\n\nconst isJoinRoomResponse = (value: unknown): value is JoinRoomResponse =>\n typeof value === 'object' && value !== null;\n\nconst isStreamChunkFrame = (value: unknown): value is StreamChunkFrame =>\n typeof value === 'object' && value !== null;\n\n//? Monotonic counter — each watcher uses a fresh responseIndex so the\n//? `buildJoinRoomResponseEventName(...)` listener doesn't collide with a\n//? sibling watcher on the same process.\nlet responseIndexCounter = 0;\nconst nextResponseIndex = (): number => {\n responseIndexCounter += 1;\n return responseIndexCounter;\n};\n\nconst waitForConnect = (socket: Socket): Promise<void> => {\n return new Promise<void>((resolve, reject) => {\n if (socket.connected) {\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`watchStream: socket failed to connect within ${CONNECT_TIMEOUT_MS}ms`));\n }, CONNECT_TIMEOUT_MS);\n const onConnect = (): void => {\n cleanup();\n resolve();\n };\n const onConnectError = (err: Error): void => {\n cleanup();\n reject(new Error(`watchStream: connect_error — ${err.message}`));\n };\n const cleanup = (): void => {\n clearTimeout(timer);\n socket.off('connect', onConnect);\n socket.off('connect_error', onConnectError);\n };\n socket.on('connect', onConnect);\n socket.on('connect_error', onConnectError);\n });\n};\n\nconst waitForJoinAck = (socket: Socket, responseIndex: number): Promise<void> => {\n const eventName = buildJoinRoomResponseEventName(responseIndex);\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n socket.off(eventName, onAck);\n reject(new Error(`watchStream: joinRoom ack timeout after ${JOIN_RESPONSE_TIMEOUT_MS}ms`));\n }, JOIN_RESPONSE_TIMEOUT_MS);\n const onAck = (payload: unknown): void => {\n clearTimeout(timer);\n socket.off(eventName, onAck);\n if (isJoinRoomResponse(payload) && payload.status === 'error') {\n reject(new Error(`watchStream: joinRoom rejected — ${payload.errorCode ?? 'unknown'}`));\n return;\n }\n resolve();\n };\n socket.on(eventName, onAck);\n });\n};\n\nexport const openStreamWatcher = async <TChunk extends StreamChunkFrame = StreamChunkFrame>(\n input: OpenStreamWatcherInput,\n): Promise<StreamWatcher<TChunk>> => {\n const defaultTimeoutMs = input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;\n const chunks: TChunk[] = [];\n const waiters: { predicate: (chunks: TChunk[]) => boolean; resolve: () => void }[] = [];\n\n const socket: Socket = createSocketIoClient(input.baseUrl, {\n transports: ['websocket'],\n forceNew: true,\n reconnection: false,\n auth: input.token ? { token: input.token } : undefined,\n });\n\n const handleChunk = (raw: unknown): void => {\n if (!isStreamChunkFrame(raw)) return;\n if (raw.status !== 'stream') return;\n if (raw.fullName !== input.routeFullName) return;\n //? Server emits chunk frames typed as `StreamChunkFrame` — caller's\n //? `TChunk` narrows the payload shape (e.g. `{ chunk: string }` for\n //? `streamBroadcast`). The runtime guards above confirm the wire shape;\n //? narrowing the static type is the test author's responsibility.\n const chunk = raw as TChunk;\n chunks.push(chunk);\n //? Drain waiters whose predicates are now satisfied. Iterate in reverse\n //? so we can splice without breaking indices.\n for (let i = waiters.length - 1; i >= 0; i--) {\n const waiter = waiters[i];\n if (!waiter) continue;\n if (waiter.predicate(chunks)) {\n waiters.splice(i, 1);\n waiter.resolve();\n }\n }\n };\n\n socket.on(socketEventNames.sync, handleChunk);\n\n //? On a connect timeout / connect_error the socket (and its `sync` listener)\n //? must be torn down — otherwise every failed attempt leaks one socket that\n //? is never auto-closed (the caller only tracks the watcher after this\n //? resolves). Mirror the join-ack cleanup path below.\n const [connectError] = await tryCatch(() => waitForConnect(socket));\n if (connectError) {\n socket.off(socketEventNames.sync, handleChunk);\n socket.disconnect();\n throw connectError;\n }\n\n const responseIndex = nextResponseIndex();\n socket.emit(socketEventNames.joinRoom, {\n group: input.roomCode,\n responseIndex,\n });\n const [joinError] = await tryCatch(() => waitForJoinAck(socket, responseIndex));\n if (joinError) {\n socket.off(socketEventNames.sync, handleChunk);\n socket.disconnect();\n throw joinError;\n }\n\n let closed = false;\n const close = (): Promise<void> => {\n if (closed) return Promise.resolve();\n closed = true;\n socket.off(socketEventNames.sync, handleChunk);\n //? Drain any outstanding waiters so callers don't hang past test end.\n for (const waiter of waiters) waiter.resolve();\n waiters.length = 0;\n socket.disconnect();\n return Promise.resolve();\n };\n\n const stopAt = (\n predicate: (chunks: TChunk[]) => boolean,\n timeoutMs: number = defaultTimeoutMs,\n ): Promise<void> => {\n if (predicate(chunks)) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const waiter = {\n predicate,\n resolve: () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve();\n },\n };\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n const idx = waiters.indexOf(waiter);\n if (idx !== -1) waiters.splice(idx, 1);\n reject(new Error(\n `watchStream.stopAt: predicate not satisfied within ${timeoutMs}ms (received ${chunks.length} chunks)`,\n ));\n }, timeoutMs);\n waiters.push(waiter);\n });\n };\n\n const waitForCount = (n: number, timeoutMs: number = defaultTimeoutMs): Promise<void> => {\n return stopAt((current) => current.length >= n, timeoutMs);\n };\n\n return {\n chunks,\n stopAt,\n waitForCount,\n close,\n };\n};\n","//? Orchestrator — runs all five test layers (contract, auth-enforcement,\n//? rate-limit, fuzz, custom) against a running server and returns a single\n//? combined summary. Consumers call this from their own thin script that\n//? imports the generated `apiMethodMap` + `apiInputSchemas`.\n\nimport { clearAllRateLimits, getCsrfConfig, getProjectConfig } from '@luckystack/core';\n\nimport { sampleSchemaInput } from './schemaSampleInput';\nimport { runContractTests } from './runContractTests';\nimport { runAuthEnforcementTests } from './runAuthEnforcementTests';\nimport { runRateLimitTests } from './runRateLimitTests';\nimport { runCsrfEnforcementTests } from './runCsrfEnforcementTests';\nimport { runFuzzTests } from './runFuzzTests';\nimport { runCustomTests } from './customTests';\nimport { calculateSummary, LAYER_KEYS } from './testLayerHelpers';\nimport type { CustomTestResult, RunCustomTestsSummary } from './customTests';\nimport type { ApiMethodMap, ApiMetaMap, ContractCheckResult, EndpointDescriptor, RunContractSummary } from './types';\nimport type { ZodType } from 'zod';\n\ntype ApiInputSchemas = Partial<Record<string, Partial<Record<string, Partial<Record<string, ZodType | undefined>>>>>>;\n\nexport interface RunAllTestsInput {\n apiMethodMap: ApiMethodMap;\n apiMetaMap: ApiMetaMap;\n apiInputSchemas: ApiInputSchemas;\n baseUrl: string;\n /** Cookie name for the session token. Defaults to `luckystack_token`. */\n sessionCookieName?: string;\n /** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */\n authToken?: string;\n /** Endpoints to skip from the sweep layers, e.g. routes that need a file upload. */\n skip?: string[];\n /** Substring filter applied to `<page>/<name>/<version>`. */\n filter?: string;\n /** Disable individual layers (faster local iteration). */\n noSweep?: boolean;\n noFuzz?: boolean;\n noRateLimit?: boolean;\n /**\n * Disable the CSRF-enforcement layer. Defaults to ON (the layer runs) — but it\n * can only probe past the auth guard with a session, so it is a no-op unless an\n * `authToken` is supplied.\n */\n noCsrf?: boolean;\n noCustom?: boolean;\n}\n\nexport interface RunAllTestsSummary {\n contract?: RunContractSummary;\n auth?: RunContractSummary;\n rateLimit?: RunContractSummary;\n csrf?: RunContractSummary;\n fuzz?: RunContractSummary;\n custom?: RunCustomTestsSummary;\n totalPassed: number;\n totalFailed: number;\n}\n\nconst inputForEndpoint = (apiInputSchemas: ApiInputSchemas) =>\n (endpoint: EndpointDescriptor): unknown => {\n const schema = apiInputSchemas[endpoint.page]?.[endpoint.name]?.[endpoint.version];\n return schema ? sampleSchemaInput(schema) : {};\n };\n\nconst matchesFilter = (endpoint: EndpointDescriptor, filter: string | undefined): boolean => {\n if (!filter) return true;\n return `${endpoint.page}/${endpoint.name}/${endpoint.version}`.includes(filter);\n};\n\nconst filterResults = (results: ContractCheckResult[], filter: string | undefined): ContractCheckResult[] =>\n filter ? results.filter(r => matchesFilter(r.endpoint, filter)) : results;\n\nconst cloneSummary = (summary: RunContractSummary, filter: string | undefined): RunContractSummary => {\n if (!filter) return summary;\n return calculateSummary(filterResults(summary.results, filter));\n};\n\n//? Build the Cookie (and CSRF) headers sweep layers send when an `authToken`\n//? is supplied. In cookie-mode the server's CSRF middleware rejects POST/PUT/DELETE\n//? without a matching CSRF token, so we look up the session record and attach the\n//? token the same way customTests.ts does: call getSession(authToken) and read\n//? csrfToken off the persisted record. Token-mode sweeps (no Cookie header) and\n//? runs without authToken are unaffected — the CSRF header is never added.\nconst buildAuthHeaders = async (input: RunAllTestsInput): Promise<Record<string, string>> => {\n const headers: Record<string, string> = {};\n if (!input.authToken) return headers;\n\n const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;\n headers.Cookie = `${cookieName}=${input.authToken}`;\n\n //? Resolve the CSRF token lazily — import @luckystack/login at call time so\n //? projects that run the test-runner without login installed don't crash on\n //? import. Mirror the same dynamic import pattern used in customTests.ts.\n try {\n const { getSession } = await import('@luckystack/login');\n const session = await getSession(input.authToken);\n if (session?.csrfToken) {\n headers[getCsrfConfig().headerName] = session.csrfToken;\n }\n } catch (error) {\n //? @luckystack/login not installed or session lookup failed.\n //? Warn so the operator knows the sweep is running without a CSRF header\n //? (degraded mode) — the sweep continues, but CSRF-protected state-changing\n //? endpoints will be unreachable in cookie-mode.\n console.warn(\n '[test-runner] buildAuthHeaders: could not resolve CSRF token —',\n error instanceof Error ? error.message : String(error),\n '— sweep will run without the CSRF header (degraded mode).',\n );\n }\n\n return headers;\n};\n\n//? Run the four auto-sweep layers (contract → auth → rate-limit → fuzz) in\n//? order, honoring the per-layer disable flags, and write each filtered\n//? summary onto `summary`. Mutates `summary` in place — identical to the\n//? prior inline block. The auth and rate-limit / fuzz nesting + ordering is\n//? preserved exactly (auth always runs in the sweep; rate-limit + fuzz are\n//? individually gated).\nconst runSweepLayers = async (\n input: RunAllTestsInput,\n summary: RunAllTestsSummary,\n headers: Record<string, string>,\n inputFor: (endpoint: EndpointDescriptor) => unknown,\n): Promise<void> => {\n const contract = await runContractTests({\n apiMethodMap: input.apiMethodMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n inputFor,\n });\n summary.contract = cloneSummary(contract, input.filter);\n\n const auth = await runAuthEnforcementTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n inputFor,\n });\n summary.auth = cloneSummary(auth, input.filter);\n\n if (!input.noRateLimit) {\n const rateLimit = await runRateLimitTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n inputFor,\n //? Reset the shared per-IP rate-limit bucket between endpoints so a\n //? neighbour saturating the window doesn't cause false pass/fail on the\n //? next endpoint. Requires the server to expose /_test/reset (allowed in\n //? NODE_ENV !== production or behind TEST_RESET_TOKEN).\n resetBetweenEndpoints: true,\n });\n summary.rateLimit = cloneSummary(rateLimit, input.filter);\n }\n\n //? CSRF-enforcement layer: probes that the framework's CSRF middleware rejects\n //? state-changing authenticated requests carrying NO CSRF header. It can only\n //? reach the CSRF check past the auth guard with a valid session, so it runs\n //? only when an `authToken` is supplied (and the layer isn't disabled). Without\n //? a token there is nothing to probe — the layer is silently skipped, leaving\n //? behavior unchanged for tokenless sweeps.\n if (!input.noCsrf && headers.Cookie) {\n const csrf = await runCsrfEnforcementTests({\n apiMethodMap: input.apiMethodMap,\n apiMetaMap: input.apiMetaMap,\n baseUrl: input.baseUrl,\n authCookie: headers.Cookie,\n skip: input.skip,\n inputFor,\n });\n summary.csrf = cloneSummary(csrf, input.filter);\n }\n\n if (!input.noFuzz) {\n const fuzz = await runFuzzTests({\n apiMethodMap: input.apiMethodMap,\n baseUrl: input.baseUrl,\n skip: input.skip,\n headers,\n });\n summary.fuzz = cloneSummary(fuzz, input.filter);\n }\n};\n\n//? Run the custom (Layer 5) sweep, after clearing the per-route rate-limit\n//? buckets the sweep layers drained. Mutates `summary` in place — identical to\n//? the prior inline block.\nconst runCustomLayer = async (input: RunAllTestsInput, summary: RunAllTestsSummary): Promise<void> => {\n //? The rate-limit layer drains per-route buckets; clear them before the\n //? custom layer so business-logic tests on low-limit routes (e.g.\n //? confirmReset, sendReset) aren't spuriously rejected with\n //? api.rateLimitExceeded. Runs in-process against the same Redis the server\n //? uses. Non-fatal if the active strategy has no clear().\n try {\n await clearAllRateLimits();\n } catch {\n //? swallow — a missing clear() is a test-quality degrade, not a crash.\n }\n const custom = await runCustomTests({\n baseUrl: input.baseUrl,\n sessionCookieName: input.sessionCookieName,\n filter: input.filter,\n //? Pass the generated method map so the harness invokes each API route\n //? with its declared HTTP method (e.g. logout is DELETE, not POST).\n apiMethodMap: input.apiMethodMap,\n });\n summary.custom = custom;\n};\n\n//? The ordered list of sweep-layer keys that map to `RunContractSummary`\n//? slots on `RunAllTestsSummary`. Drives `computeTotals` and the reporter so\n//? adding a new sweep layer only requires adding it here + to the summary type.\nconst SWEEP_LAYER_ORDER = [\n LAYER_KEYS.contract,\n LAYER_KEYS.auth,\n LAYER_KEYS.rateLimit,\n LAYER_KEYS.csrf,\n LAYER_KEYS.fuzz,\n] as const;\n\ntype SweepLayerKey = typeof SWEEP_LAYER_ORDER[number];\n\n//? Map from each canonical layer key to the matching summary property name on\n//? `RunAllTestsSummary`. Property names diverged from LAYER_KEYS values early\n//? (LAYER_KEYS uses display strings like `'auth-enforcement'`); keeping this\n//? map here means neither the reporter nor computeTotals need to hand-code the\n//? mapping.\nconst SWEEP_SUMMARY_PROP: Record<SweepLayerKey, keyof Pick<RunAllTestsSummary, 'contract' | 'auth' | 'rateLimit' | 'csrf' | 'fuzz'>> = {\n [LAYER_KEYS.contract]: 'contract',\n [LAYER_KEYS.auth]: 'auth',\n [LAYER_KEYS.rateLimit]: 'rateLimit',\n [LAYER_KEYS.csrf]: 'csrf',\n [LAYER_KEYS.fuzz]: 'fuzz',\n};\n\n//? Iterate every sweep layer + the custom layer to sum passed/failed.\n//? DD-TR-various — xpass exit-code policy (documented decision):\n//? `xpass` (a case marked `expectedToFail` that unexpectedly passes) is NOT\n//? counted in `totalFailed`. Rationale: an xpass is a positive signal — the\n//? known issue is gone and the marker just needs removing. Treating it as a\n//? failure would make CI red when a bug is fixed, which is backwards.\n//? It IS shown prominently in the summary line and the \"Unexpectedly passed\"\n//? section so the stale marker can't rot silently behind a green run.\n//? To opt into strict mode (xpass = CI failure), inspect `summary.custom?.xpassed`\n//? in your own script and `process.exit(1)` when it is > 0.\nconst computeTotals = (summary: RunAllTestsSummary): void => {\n let passed = 0;\n let failed = 0;\n for (const key of SWEEP_LAYER_ORDER) {\n const s = summary[SWEEP_SUMMARY_PROP[key]];\n passed += s?.passed ?? 0;\n failed += s?.failed ?? 0;\n }\n summary.totalPassed = passed + (summary.custom?.passed ?? 0);\n summary.totalFailed = failed + (summary.custom?.failed ?? 0);\n};\n\nexport const runAllTests = async (input: RunAllTestsInput): Promise<RunAllTestsSummary> => {\n const summary: RunAllTestsSummary = { totalPassed: 0, totalFailed: 0 };\n\n //? Warn when a non-empty apiMethodMap is paired with an empty apiMetaMap.\n //? The auth-enforcement and rate-limit layers both use the meta map to decide\n //? which routes require login and which have declared rate limits. An absent\n //? or stale map silently skips every auth/rate-limit check — the run looks\n //? green but provides no real coverage.\n if (!input.noSweep) {\n const hasRoutes = Object.keys(input.apiMethodMap).length > 0;\n const hasMeta = Object.keys(input.apiMetaMap).length > 0;\n if (hasRoutes && !hasMeta) {\n console.warn(\n '[test-runner] runAllTests: apiMetaMap is empty but apiMethodMap has routes. '\n + 'The auth-enforcement and rate-limit layers will skip every route. '\n + 'Pass the generated apiMetaMap to enable those checks.',\n );\n }\n }\n\n const headers = await buildAuthHeaders(input);\n const inputFor = inputForEndpoint(input.apiInputSchemas);\n\n if (!input.noSweep) {\n await runSweepLayers(input, summary, headers, inputFor);\n }\n\n if (!input.noCustom) {\n await runCustomLayer(input, summary);\n }\n\n computeTotals(summary);\n\n return summary;\n};\n\n//? ── Colored, list-based reporting ───────────────────────────────────────\n//? Honors NO_COLOR / FORCE_COLOR (https://no-color.org) and falls back to\n//? plain text when stdout is not a TTY (piped to a file / CI log).\nconst useColor = ((): boolean => {\n if (process.env.NO_COLOR) return false;\n if (process.env.FORCE_COLOR) return true;\n return process.stdout.isTTY;\n})();\nconst paint = (code: string, text: string): string => (useColor ? `\\u001B[${code}m${text}\\u001B[0m` : text);\nconst green = (t: string): string => paint('32', t);\nconst red = (t: string): string => paint('31', t);\nconst yellow = (t: string): string => paint('33', t);\nconst dim = (t: string): string => paint('2', t);\nconst bold = (t: string): string => paint('1', t);\n\ninterface ReportRow {\n label: string;\n reason?: string;\n}\n\n//? DD-TR-various — leaky summary union tightened:\n//? `LayerSummary` is the minimal shape the reporter cares about — the fields\n//? every layer summary has in common. The extra-field helpers discriminate via\n//? `in` to stay type-safe without widening callers to `unknown`.\ninterface LayerSummary {\n total: number;\n passed: number;\n failed: number;\n}\n\nconst xfailOf = (s: LayerSummary | undefined): number => {\n if (s === undefined || !('xfailed' in s)) return 0;\n const v = (s as Record<string, unknown>).xfailed;\n return typeof v === 'number' ? v : 0;\n};\nconst skippedOf = (s: LayerSummary | undefined): number => {\n if (s === undefined || !('skipped' in s)) return 0;\n const v = (s as Record<string, unknown>).skipped;\n return typeof v === 'number' ? v : 0;\n};\n\n//? Per-layer headline: green \"X/Y passed\" when clean, red \"Z failed\" when not,\n//? plus dim xfail / skipped counts. This is the \"14/20 in groen, 6/20 in rood\"\n//? the report leads with.\nconst formatLayerLine = (label: string, s: LayerSummary | undefined): string => {\n if (!s) return ` ${dim('–')} ${label.padEnd(18)} ${dim('(layer skipped)')}`;\n const clean = s.failed === 0;\n const mark = clean ? green('✓') : red('✗');\n const head = clean\n ? green(`${s.passed}/${s.total} passed`)\n : `${green(`${s.passed}/${s.total} passed`)} ${red(`${s.failed}/${s.total} failed`)}`;\n const extras: string[] = [];\n const xf = xfailOf(s);\n const sk = skippedOf(s);\n if (xf) extras.push(yellow(`${xf} expected-fail`));\n if (sk) extras.push(dim(`${sk} skipped`));\n const extra = extras.length > 0 ? ` ${dim('·')} ${extras.join(dim(', '))}` : '';\n return ` ${mark} ${label.padEnd(18)} ${head}${extra}`;\n};\n\nconst sweepFailRows = (s: RunContractSummary | undefined): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === 'fail')\n .map((r) => ({\n label: `${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,\n reason: r.errorCode ? `${r.reason ?? 'failed'} [${r.errorCode}]` : r.reason,\n }));\n\nconst sweepSkipRows = (layer: string, s: RunContractSummary | undefined): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === 'skipped')\n .map((r) => ({\n label: `${layer}: ${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,\n reason: r.reason,\n }));\n\nconst customRows = (s: RunCustomTestsSummary | undefined, status: CustomTestResult['status']): ReportRow[] =>\n (s?.results ?? [])\n .filter((r) => r.status === status)\n .map((r) => ({ label: `${r.routePath} :: ${r.caseName}`, reason: r.reason }));\n\nconst printSection = (title: (t: string) => string, heading: string, rows: ReportRow[], bullet: string): void => {\n if (rows.length === 0) return;\n console.log('');\n console.log(title(bold(`${heading} (${rows.length})`)));\n for (const row of rows) {\n console.log(` ${title(bullet)} ${row.label}`);\n if (row.reason) console.log(` ${dim(row.reason)}`);\n }\n};\n\nexport const logRunAllSummary = (summary: RunAllTestsSummary): void => {\n console.log('');\n\n //? Print a headline line for every sweep layer (ordered) then the custom layer.\n for (const key of SWEEP_LAYER_ORDER) {\n console.log(formatLayerLine(key, summary[SWEEP_SUMMARY_PROP[key]]));\n }\n console.log(formatLayerLine(LAYER_KEYS.custom, summary.custom));\n\n //? Real failures across every layer — these MUST be fixed (a green test that\n //? went red, or a wrong test). Sweep failures + un-marked custom failures.\n const failed: ReportRow[] = [\n ...SWEEP_LAYER_ORDER.flatMap((key) => sweepFailRows(summary[SWEEP_SUMMARY_PROP[key]])),\n ...customRows(summary.custom, 'fail'),\n ];\n printSection(red, 'Failed — must be fixed (real bugs / wrong tests)', failed, '✗');\n\n //? Known, accepted failures — red is OK here; they are tracked, not regressions.\n printSection(yellow, 'Expected failures — known issues, allowed to fail', customRows(summary.custom, 'xfail'), '⚠');\n\n //? Stale markers — these passed but are still flagged expectedToFail.\n printSection(yellow, 'Unexpectedly passed — remove the expectedToFail marker', customRows(summary.custom, 'xpass'), '?');\n\n //? Skipped with reason — not run in this mode (login-gated, high rate-limit, …).\n const skipped: ReportRow[] = SWEEP_LAYER_ORDER.flatMap((key) =>\n sweepSkipRows(key, summary[SWEEP_SUMMARY_PROP[key]]),\n );\n printSection(dim, 'Skipped — not run (with reason)', skipped, '–');\n\n //? Sum xfail and skipped across all layers (sweep + custom) in one pass.\n let totalXfail = 0;\n let totalSkipped = 0;\n for (const key of SWEEP_LAYER_ORDER) {\n const s = summary[SWEEP_SUMMARY_PROP[key]];\n totalXfail += xfailOf(s);\n totalSkipped += skippedOf(s);\n }\n totalXfail += xfailOf(summary.custom);\n totalSkipped += skippedOf(summary.custom);\n\n //? `xpass` (a case still marked expectedToFail that now passes) is NOT a\n //? failure, so it doesn't move the exit code — but it MUST be visible in the\n //? final line, otherwise a stale marker rots silently behind a green run.\n const totalXpass = summary.custom?.xpassed ?? 0;\n\n console.log('');\n const parts = [\n green(`${summary.totalPassed} passed`),\n summary.totalFailed > 0 ? red(`${summary.totalFailed} failed`) : dim('0 failed'),\n totalXfail > 0 ? yellow(`${totalXfail} expected-fail`) : dim('0 expected-fail'),\n totalXpass > 0 ? yellow(`${totalXpass} unexpectedly-passed`) : dim('0 unexpectedly-passed'),\n totalSkipped > 0 ? yellow(`${totalSkipped} skipped`) : dim('0 skipped'),\n ];\n console.log(`Summary: ${parts.join(dim(' · '))}`);\n console.log(dim(' legend: ✗ red = must fix · ⚠ yellow = known/allowed · – skipped (not run)'));\n};\n","//? Extension registry for the test runner. Consumers plug in custom test\n//? layers, realistic-payload fixtures, and result reporters without\n//? forking the framework.\n//?\n//? Three slots:\n//?\n//? 1. Test layers — additional checks per endpoint (CORS enforcement,\n//? business rules, custom auth schemes, multi-tenant isolation).\n//? 2. Fixtures — realistic input payloads (valid + invalid) per typeKey\n//? so the fuzz layer can use them before falling back to schema-random.\n//? 3. Reporters — per-result and per-summary callbacks + optional\n//? webhook URL the runner POSTs the batch summary to.\n//?\n//? All slots are append-only at registration time; clearing requires\n//? `resetTestExtensionsForTests()` (kept out of the production surface\n//? to prevent accidental wipes from a misbehaving plugin).\n\nexport interface TestLayerInput {\n /** Resolved route name (`api/billing/getInvoice/v1`). */\n endpoint: string;\n /** Method when applicable, undefined for sync endpoints. */\n method?: string;\n /** Bearer token / session token to use for auth-aware checks. */\n authToken?: string;\n}\n\nexport interface TestLayerResult {\n passed: boolean;\n /** Short reason key — i18n-style errorCode when failed. */\n message?: string;\n /** Optional extras for the reporter (latency, response sample, ...). */\n metadata?: Record<string, unknown>;\n}\n\nexport interface TestLayer {\n name: string;\n run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;\n}\n\nexport interface TestFixture<TPayload = unknown> {\n valid: TPayload[];\n invalid: TPayload[];\n}\n\nexport interface TestResult {\n layer: string;\n endpoint: string;\n passed: boolean;\n message?: string;\n durationMs?: number;\n metadata?: Record<string, unknown>;\n}\n\nexport interface TestSummary {\n totalLayers: number;\n totalEndpoints: number;\n passed: number;\n failed: number;\n results: TestResult[];\n}\n\nexport interface TestReporter {\n /** Called once per individual TestResult as the runner progresses. */\n onResult?: (result: TestResult) => void | Promise<void>;\n /** Called once at the end with the aggregated summary. */\n onSummary?: (summary: TestSummary) => void | Promise<void>;\n /** Optional webhook the runner POSTs the summary to. */\n webhookUrl?: string;\n webhookAuth?: { type: 'bearer'; token: string };\n}\n\nconst layers = new Map<string, TestLayer>();\nconst fixtures = new Map<string, TestFixture>();\nlet reporter: TestReporter | null = null;\n\n/**\n * Register a custom test layer. Re-registering the same name replaces\n * the previous entry. Use for CORS enforcement, business-rule checks,\n * custom auth schemes, multi-tenant isolation, GDPR data-flow checks.\n */\nexport const registerTestLayer = (layer: TestLayer): void => {\n layers.set(layer.name, layer);\n};\n\nexport const listTestLayers = (): TestLayer[] => [...layers.values()];\n\n/**\n * Register a realistic payload fixture for a specific type key. The fuzz\n * layer prefers these over schema-random generated inputs when a fixture\n * exists for the endpoint's input type.\n *\n * `valid` payloads are used to verify happy-path behavior; `invalid`\n * payloads probe the validator's error responses.\n */\nexport const registerTestFixture = <TPayload = unknown>(typeKey: string, fixture: TestFixture<TPayload>): void => {\n fixtures.set(typeKey, fixture);\n};\n\nexport const getTestFixture = (typeKey: string): TestFixture | undefined => fixtures.get(typeKey);\n\n/**\n * Register a test reporter. Set `null` to unregister. The runner calls\n * `onResult` per individual result as the run progresses and `onSummary`\n * once at the end. When `webhookUrl` is set, the runner additionally\n * POSTs the JSON-serialised summary to that URL.\n */\nexport const registerTestReporter = (newReporter: TestReporter | null): void => {\n reporter = newReporter;\n};\n\nexport const getTestReporter = (): TestReporter | null => reporter;\n\nexport const resetTestExtensionsForTests = (): void => {\n layers.clear();\n fixtures.clear();\n reporter = null;\n};\n"],"mappings":"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","names":["DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","EXPECTED_ERROR_CODE","tryCatch","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_REQUEST_TIMEOUT_MS","tryCatch","tryCatch","tryCatch","getCsrfConfig","getProjectConfig","getProjectConfig","getCsrfConfig"]}
|
package/docs/fuzz-tests.md
CHANGED
|
@@ -71,7 +71,7 @@ The current catalogue lives in `src/fuzzCheck.ts:JUNK_PAYLOADS`. It is intention
|
|
|
71
71
|
| `1234567890` | Number-as-body. |
|
|
72
72
|
| `true` | Boolean-as-body. |
|
|
73
73
|
| `{ nested: { deeply: { nested: { value: 'x'.repeat(10000) } } } }` | Oversized nested strings. Catches handlers that log the entire body without truncation. |
|
|
74
|
-
| `{
|
|
74
|
+
| `{ constructor: { polluted: true } }` | Prototype-pollution attempt via a named own-key. `{ __proto__: ... }` serializes to `{}` via JSON.stringify (prototype keys are not own-enumerable), so the probe uses `constructor` instead — it survives the JSON round-trip while still exercising parsers that misinterpret the key. |
|
|
75
75
|
| `{ key: null, other: undefined, third: Number.NaN }` | Edge values that survive JSON encoding in non-obvious ways (`undefined` keys drop, `NaN` becomes `null`). |
|
|
76
76
|
|
|
77
77
|
This is a lightweight fuzzer, not a property-based one. Real schema-driven fuzzing (TS type -> random valid input via something like `fast-check`) is deferred until the devkit emits Zod schemas alongside the runtime types. When that lands, the fuzz layer will preferentially use registered fixtures (see below), fall back to schema-random, and finally fall back to this fixed catalogue.
|
|
@@ -241,7 +241,7 @@ A nightly custom layer can then walk every endpoint, look up the fixture by rout
|
|
|
241
241
|
- **Binary uploads.** Endpoints expecting `multipart/form-data` will fail every JUNK payload because the Content-Type header is `application/json`. Skip these endpoints explicitly; they need a separate, format-aware fuzzer.
|
|
242
242
|
- **Streaming endpoints.** Endpoints that hold the connection open (SSE, long-poll) will hit `requestTimeoutMs` and report as crashed. Skip them or add support for streaming responses in a custom layer.
|
|
243
243
|
- **Endpoints with filesystem side-effects.** A fuzz probe that triggers a real file write happens on every endpoint where the validator is permissive. Add cleanup in a custom layer or run the fuzz sweep against a disposable test instance.
|
|
244
|
-
- **Prototype pollution.** The runner sends `{
|
|
244
|
+
- **Prototype pollution.** The runner sends `{ constructor: { polluted: true } }`. `{ __proto__: ... }` serializes to `{}` via JSON.stringify (prototype keys are not own-enumerable), so the probe uses `constructor` instead — it survives the JSON round-trip while still exercising parsers that misinterpret the key. A failure here means a handler is using a custom merge function that conflates `constructor` with `Object.prototype.constructor`.
|
|
245
245
|
- **`NaN` and `undefined` after stringify.** `Number.NaN` JSON-stringifies as `null`; `undefined`-valued keys are dropped. The payload that hits the server is `{ key: null, third: null }`. This is intentional — it mirrors what a malicious client can actually transmit.
|
|
246
246
|
- **Auth-protected endpoints with no session header.** They will all fail with `errorCode: 'auth.required'` (which passes the fuzz envelope check, since the response is a typed error). The fuzz sweep will report them as `pass`. Whether that is what you want depends on whether you trust the auth layer's earlier verdict — most projects pass headers to fuzz so the handler logic is actually reached.
|
|
247
247
|
- **The `<preview>` in the `reason`** is `JSON.stringify(payload).slice(0, 80)`. Long payloads (oversized nested) are truncated. Look at the source `JUNK_PAYLOADS` to identify which probe caused the failure when the preview is cut.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckystack/test-runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "git+https://github.com/ItsLucky23/LuckyStack-v2.git",
|
|
16
16
|
"directory": "packages/test-runner"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/
|
|
18
|
+
"homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/main/packages/test-runner#readme",
|
|
19
19
|
"bugs": {
|
|
20
20
|
"url": "https://github.com/ItsLucky23/LuckyStack-v2/issues"
|
|
21
21
|
},
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"test": "vitest run"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@luckystack/core": "^0.2.
|
|
57
|
+
"@luckystack/core": "^0.2.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@luckystack/login": "^0.2.
|
|
60
|
+
"@luckystack/login": "^0.2.6",
|
|
61
61
|
"socket.io-client": "^4.8.0",
|
|
62
62
|
"zod": "^4.0.0"
|
|
63
63
|
},
|