@luckystack/test-runner 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -40,7 +40,8 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
40
40
  | `runFuzzCheck({ endpoint, baseUrl, headers? })` | Single-endpoint crash-resistance fuzz | → docs/fuzz-tests.md |
41
41
  | `runFuzzTests({ apiMethodMap, baseUrl, skip?, headers?, onResult? })` | Sweep fuzz layer (nightly CI) | → docs/fuzz-tests.md |
42
42
  | `resetServerState({ baseUrl, token? })` | POST naar `/_test/reset` om DB+Redis schoon te maken | → docs/rate-limit-tests.md |
43
- | `sampleSchemaInput(schema)` | Genereert Zod-valid sample payload uit een schema | → docs/contract-tests.md |
43
+ | `sampleSchemaInput(schema, options?)` | Genereert Zod-valid sample payload uit een schema. `options.stringPrefix` tagt UNCONSTRAINED strings (finding #98) — format/checked strings blijven ongeprefixt zodat ze valid blijven. | → docs/contract-tests.md |
44
+ | `createTestDataMarker()` / `TEST_DATA_PREFIX` | Run-unique `lstest_<uuid>_` marker (+ vaste `lstest_` prefix-constant) waarmee `runAllTests` gegenereerde test-strings identificeerbaar maakt. | → docs/contract-tests.md |
44
45
  | `logContractResult(result)` | Pretty-print één `ContractCheckResult` | → docs/extension-hooks.md |
45
46
  | `logContractSummary(summary)` | Pretty-print aggregate `RunContractSummary` | → docs/extension-hooks.md |
46
47
  | `registerTestLayer(layer)` | Plug-in custom test-layer (CORS, business rules, multi-tenant) | → docs/extension-hooks.md |
@@ -79,13 +80,14 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
79
80
  - `TEST_RESET_TOKEN` (env, optional, server-side) — wanneer gezet moet `resetServerState({ token })` deze meesturen als `X-Test-Reset-Token`. Verplicht voor staging/preview deploys die het endpoint over het netwerk exposen.
80
81
  - `RunRateLimitTestsInput.maxRateLimitToTest` (default `50`) — endpoints met hogere `rateLimit` worden geskipt om duizenden requests in CI te vermijden.
81
82
  - `RunRateLimitTestsInput.resetBetweenEndpoints` (default `false`) — hit `/_test/reset` voor elke endpoint zodat het shared IP-bucket schoon is.
83
+ - `RunAllTestsInput.resetBookends` (default `true` wanneer een reset-token beschikbaar is) — POST `/_test/reset` één keer vóór en één keer ná de auto-sweep (finding #98: clean slate + cleanup zodat geen echte rows worden geëdit en geen `lstest_*` testdata achterblijft). Zonder token (`resetToken`/`TEST_RESET_TOKEN`) worden de bookends met een logregel geskipt, niet gefaald.
82
84
  - `registerTestLayer({ name, run })` — custom check per endpoint (CORS, business rules, custom auth-schemes, multi-tenant isolation).
83
85
  - `registerTestFixture(typeKey, { valid, invalid })` — realistic payloads die de fuzz layer prefereert boven schema-random.
84
86
  - `registerTestReporter({ onResult?, onSummary?, webhookUrl?, webhookAuth? })` — wire per-result/per-summary callbacks of POST de summary naar een webhook.
85
87
 
86
88
  ## Peer dependencies
87
89
 
88
- - **Required**: `zod@^4.0.0` (peer), `socket.io-client@^4.8.0` (peer — `streamWatcher.ts` opent een tweede socket voor `ctx.watchStream`), `@luckystack/core@^0.1.0` (dependency — leest `getApiMethodMapPath()` voor de default map-locatie).
90
+ - **Required**: `zod@^4.0.0` (peer), `socket.io-client@^4.8.0` (peer — `streamWatcher.ts` opent een tweede socket voor `ctx.watchStream`), `@luckystack/core@^0.1.0` (dependency — levert de helpers die de runner consumeert: `tryCatch`/`tryCatchSync`, `getProjectConfig`/`getCsrfConfig`, `getSrcDir` (default src-dir voor custom-test discovery in `customTests.ts`), `getPrismaClient` (Layer-5 `ctx.prisma`) en `clearAllRateLimits`. De `apiMethodMap` zelf wordt door de consumer als argument doorgegeven, niet uit een pad gelezen).
89
91
  - **Required server-side**: `@luckystack/server` met `/_test/reset` endpoint gemount (default ingebouwd, gated op `NODE_ENV !== 'production'`).
90
92
  - **Required generated artefact**: `apiMethodMap.generated.ts` (en `apiMetaMap.generated.ts` voor de auth/rate-limit layers), geproduceerd door `@luckystack/devkit`.
91
93
  - **Optional**: geen — fixtures, reporters en custom layers zijn pure-JS opt-in.
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ interface ApiMetaEntry {
9
9
  auth: {
10
10
  login: boolean;
11
11
  additional?: Record<string, unknown>[];
12
+ hasAdditional?: boolean;
12
13
  };
13
14
  rateLimit?: number | false;
14
15
  }
@@ -122,9 +123,10 @@ interface RunRateLimitTestsInput {
122
123
  maxRateLimitToTest?: number;
123
124
  /**
124
125
  * When true, hit `/_test/reset` before each endpoint so limiter state is
125
- * clean. Requires the server to allow the endpoint (NODE_ENV !== production
126
- * or `TEST_RESET_TOKEN` configured). Without resetting, the shared IP bucket
127
- * leaks state between endpoints.
126
+ * clean. Requires the server to allow the endpoint: `NODE_ENV` in
127
+ * { 'development', 'test' } AND `TEST_RESET_TOKEN` set on the server with a
128
+ * matching `resetToken` here (an unset token is 403, never open). Without
129
+ * resetting, the shared IP bucket leaks state between endpoints.
128
130
  */
129
131
  resetBetweenEndpoints?: boolean;
130
132
  /**
@@ -197,7 +199,20 @@ interface ResetServerStateInput {
197
199
  }
198
200
  declare const resetServerState: ({ baseUrl, token, requestTimeoutMs }: ResetServerStateInput) => Promise<boolean>;
199
201
 
200
- declare const sampleSchemaInput: (schema: z.ZodType) => unknown;
202
+ interface SampleSchemaOptions {
203
+ /**
204
+ * Marker prefix applied to generated UNCONSTRAINED string values so any rows
205
+ * a sweep creates are identifiable + collision-free (finding #98). Strings
206
+ * that carry a format (email/url/uuid/datetime/…) or a check (min/max/
207
+ * length/regex) are left as-is, since prefixing could break the route's own
208
+ * validation and defeat the contract layer's "valid input" guarantee.
209
+ */
210
+ stringPrefix?: string;
211
+ }
212
+ declare const sampleSchemaInput: (schema: z.ZodType, options?: SampleSchemaOptions) => unknown;
213
+
214
+ declare const TEST_DATA_PREFIX = "lstest_";
215
+ declare const createTestDataMarker: () => string;
201
216
 
202
217
  declare const logContractResult: (result: ContractCheckResult) => void;
203
218
  declare const logContractSummary: (summary: RunContractSummary) => void;
@@ -346,7 +361,7 @@ interface RunAllTestsInput {
346
361
  apiMetaMap: ApiMetaMap;
347
362
  apiInputSchemas: ApiInputSchemas;
348
363
  baseUrl: string;
349
- /** Cookie name for the session token. Defaults to `luckystack_token`. */
364
+ /** Cookie name for the session token. Defaults to the project's configured session cookie name (`projectConfig.http.sessionCookieName`). */
350
365
  sessionCookieName?: string;
351
366
  /** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */
352
367
  authToken?: string;
@@ -365,6 +380,24 @@ interface RunAllTestsInput {
365
380
  */
366
381
  noCsrf?: boolean;
367
382
  noCustom?: boolean;
383
+ /**
384
+ * Token sent as `X-Test-Reset-Token` when the rate-limit layer resets the
385
+ * shared bucket between endpoints. The server's `/_test/reset` requires
386
+ * `TEST_RESET_TOKEN` to be set and matching — an unset/wrong token is 403, so
387
+ * `resetBetweenEndpoints` would silently no-op. Defaults to
388
+ * `process.env.TEST_RESET_TOKEN` so a CI run with the env set works with no
389
+ * extra wiring; pass explicitly to override.
390
+ */
391
+ resetToken?: string;
392
+ /**
393
+ * Contract-test mutation safety (finding #98). When a reset token is
394
+ * available, the sweep POSTs `/_test/reset` ONCE before the first layer
395
+ * (clean slate — no real row the generated payloads could clobber) and ONCE
396
+ * after the last (drop whatever the sweep created, so no test data persists).
397
+ * Defaults to ON; set `false` to opt out. With no token the bookends are
398
+ * silently skipped with a logged note (they can't run — `/_test/reset` 403s).
399
+ */
400
+ resetBookends?: boolean;
368
401
  }
369
402
  interface RunAllTestsSummary {
370
403
  contract?: RunContractSummary;
@@ -456,6 +489,18 @@ declare const registerTestReporter: (newReporter: TestReporter | null) => void;
456
489
  declare const getTestReporter: () => TestReporter | null;
457
490
  declare const resetTestExtensionsForTests: () => void;
458
491
 
492
+ interface RunRegisteredLayersInput {
493
+ apiMethodMap: ApiMethodMap$1;
494
+ /** Session/bearer token forwarded to each layer's `run({ authToken })`. */
495
+ authToken?: string;
496
+ /**
497
+ * Endpoints to skip — matched against `<page>/<name>` (version-agnostic) and
498
+ * `<page>/<name>/<version>`, same convention as the built-in sweeps.
499
+ */
500
+ skip?: string[];
501
+ }
502
+ declare const runRegisteredLayers: (input: RunRegisteredLayersInput) => Promise<TestSummary>;
503
+
459
504
  declare const LAYER_KEYS: {
460
505
  readonly contract: "contract";
461
506
  readonly auth: "auth-enforcement";
@@ -465,4 +510,4 @@ declare const LAYER_KEYS: {
465
510
  readonly custom: "custom";
466
511
  };
467
512
 
468
- export { type ApiMetaEntry, type ApiMetaMap, type ApiMethodMap$1 as ApiMethodMap, type AuthEnforcementCheckInput, type ContractCheckInput, type ContractCheckResult, type CsrfEnforcementCheckInput, type CustomTestCase, type CustomTestResult, type EndpointDescriptor, type FuzzCheckInput, type HttpMethod, LAYER_KEYS, type OpenStreamWatcherInput, type RateLimitCheckInput, type ResetServerStateInput, type RunAllTestsInput, type RunAllTestsSummary, type RunAuthEnforcementTestsInput, type RunContractSummary, type RunContractTestsInput, type RunCsrfEnforcementTestsInput, type RunCustomTestsInput, type RunCustomTestsSummary, type RunFuzzTestsInput, type RunRateLimitTestsInput, type StreamChunkFrame, type StreamWatcher, type SyncMethodMap, type TestContext, type TestExpect, type TestFixture, type TestLayer, type TestLayerInput, type TestLayerResult, type TestReporter, type TestResult, type TestSummary, discoverCustomTestFiles, getTestFixture, getTestReporter, listTestLayers, logContractResult, logContractSummary, logRunAllSummary, openStreamWatcher, registerTestFixture, registerTestLayer, registerTestReporter, resetServerState, resetTestExtensionsForTests, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCsrfEnforcementCheck, runCsrfEnforcementTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, sampleSchemaInput, walkEndpoints, walkSyncEndpoints };
513
+ export { type ApiMetaEntry, type ApiMetaMap, type ApiMethodMap$1 as ApiMethodMap, type AuthEnforcementCheckInput, type ContractCheckInput, type ContractCheckResult, type CsrfEnforcementCheckInput, type CustomTestCase, type CustomTestResult, type EndpointDescriptor, type FuzzCheckInput, type HttpMethod, LAYER_KEYS, type OpenStreamWatcherInput, type RateLimitCheckInput, type ResetServerStateInput, type RunAllTestsInput, type RunAllTestsSummary, type RunAuthEnforcementTestsInput, type RunContractSummary, type RunContractTestsInput, type RunCsrfEnforcementTestsInput, type RunCustomTestsInput, type RunCustomTestsSummary, type RunFuzzTestsInput, type RunRateLimitTestsInput, type RunRegisteredLayersInput, type SampleSchemaOptions, type StreamChunkFrame, type StreamWatcher, type SyncMethodMap, TEST_DATA_PREFIX, type TestContext, type TestExpect, type TestFixture, type TestLayer, type TestLayerInput, type TestLayerResult, type TestReporter, type TestResult, type TestSummary, createTestDataMarker, discoverCustomTestFiles, getTestFixture, getTestReporter, listTestLayers, logContractResult, logContractSummary, logRunAllSummary, openStreamWatcher, registerTestFixture, registerTestLayer, registerTestReporter, resetServerState, resetTestExtensionsForTests, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCsrfEnforcementCheck, runCsrfEnforcementTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, runRegisteredLayers, sampleSchemaInput, walkEndpoints, walkSyncEndpoints };
package/dist/index.js CHANGED
@@ -177,6 +177,10 @@ var requiresLogin = (apiMetaMap, endpoint) => {
177
177
  const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
178
178
  return meta?.auth?.login ?? false;
179
179
  };
180
+ var hasAuthRequirement = (apiMetaMap, endpoint) => {
181
+ const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
182
+ return (meta?.auth?.login ?? false) || (meta?.auth?.hasAdditional ?? false);
183
+ };
180
184
  var hasMetaEntry = (apiMetaMap, endpoint) => apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version] !== void 0;
181
185
  var getRateLimit = (apiMetaMap, endpoint) => {
182
186
  const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
@@ -222,7 +226,7 @@ var runContractTests = async (input) => {
222
226
 
223
227
  // src/authEnforcementCheck.ts
224
228
  var DEFAULT_REQUEST_TIMEOUT_MS2 = 5e3;
225
- var EXPECTED_ERROR_CODE = "auth.required";
229
+ var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set(["auth.required", "auth.forbidden"]);
226
230
  var runAuthEnforcementCheck = async (input) => {
227
231
  const { endpoint, baseUrl } = input;
228
232
  const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
@@ -265,14 +269,14 @@ var runAuthEnforcementCheck = async (input) => {
265
269
  durationMs
266
270
  };
267
271
  }
268
- if (parsed.errorCode !== EXPECTED_ERROR_CODE) {
272
+ if (parsed.errorCode === void 0 || !EXPECTED_ERROR_CODES.has(parsed.errorCode)) {
269
273
  return {
270
274
  endpoint,
271
275
  status: "fail",
272
276
  httpStatus,
273
277
  responseStatus: "error",
274
278
  errorCode: parsed.errorCode,
275
- reason: `expected errorCode '${EXPECTED_ERROR_CODE}' but got '${parsed.errorCode ?? "(missing)"}'`,
279
+ reason: `expected an auth-rejection errorCode (${[...EXPECTED_ERROR_CODES].join(" | ")}) but got '${parsed.errorCode ?? "(missing)"}'`,
276
280
  durationMs
277
281
  };
278
282
  }
@@ -292,7 +296,7 @@ var runAuthEnforcementTests = async (input) => {
292
296
  const skip = input.skip ?? [];
293
297
  const results = [];
294
298
  for (const endpoint of endpoints) {
295
- if (!requiresLogin(input.apiMetaMap, endpoint)) {
299
+ if (!hasAuthRequirement(input.apiMetaMap, endpoint)) {
296
300
  continue;
297
301
  }
298
302
  if (shouldSkip(endpoint, skip)) {
@@ -319,16 +323,11 @@ var runAuthEnforcementTests = async (input) => {
319
323
 
320
324
  // src/rateLimitCheck.ts
321
325
  var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
322
- var EXPECTED_ERROR_CODE2 = "api.rateLimitExceeded";
326
+ var EXPECTED_ERROR_CODE = "api.rateLimitExceeded";
323
327
  var PRE_LIMITER_ERROR_CODES = /* @__PURE__ */ new Set([
324
328
  "auth.required",
325
- "auth.csrfTokenMissing",
326
- "auth.csrfTokenInvalid",
327
- "auth.unauthorized",
328
- "api.csrfTokenMissing",
329
- "api.csrfTokenInvalid",
330
- "api.unauthorized",
331
- "api.validationError"
329
+ "auth.forbidden",
330
+ "auth.csrfMismatch"
332
331
  ]);
333
332
  var runRateLimitCheck = async (input) => {
334
333
  const { endpoint, baseUrl, rateLimit } = input;
@@ -368,7 +367,7 @@ var runRateLimitCheck = async (input) => {
368
367
  };
369
368
  }
370
369
  const { httpStatus, parsed } = final;
371
- if (parsed?.status === "error" && parsed.errorCode === EXPECTED_ERROR_CODE2) {
370
+ if (parsed?.status === "error" && parsed.errorCode === EXPECTED_ERROR_CODE) {
372
371
  return {
373
372
  endpoint,
374
373
  status: "pass",
@@ -385,16 +384,16 @@ var runRateLimitCheck = async (input) => {
385
384
  httpStatus,
386
385
  responseStatus,
387
386
  errorCode: parsed?.errorCode,
388
- reason: `expected '${EXPECTED_ERROR_CODE2}' on request ${rateLimit + 1} but got ${parsed?.status ?? "none"}/${parsed?.errorCode ?? "(none)"}`,
387
+ reason: `expected '${EXPECTED_ERROR_CODE}' on request ${rateLimit + 1} but got ${parsed?.status ?? "none"}/${parsed?.errorCode ?? "(none)"}`,
389
388
  durationMs
390
389
  };
391
390
  };
392
391
 
393
392
  // src/resetServerState.ts
394
- import { tryCatch as tryCatch2 } from "@luckystack/core";
393
+ import { getProjectConfig, tryCatch as tryCatch2 } from "@luckystack/core";
395
394
  var DEFAULT_REQUEST_TIMEOUT_MS4 = 1e4;
396
395
  var resetServerState = async ({ baseUrl, token, requestTimeoutMs }) => {
397
- const url = `${baseUrl.replace(/\/$/, "")}/_test/reset`;
396
+ const url = `${baseUrl.replace(/\/$/, "")}${getProjectConfig().http.testResetEndpoint}`;
398
397
  const headers = { "Content-Type": "application/json" };
399
398
  if (token) headers["X-Test-Reset-Token"] = token;
400
399
  const controller = new AbortController();
@@ -686,12 +685,14 @@ var runFuzzTests = async (input) => {
686
685
 
687
686
  // src/schemaSampleInput.ts
688
687
  var innerDef = (node) => node ? node.def ?? node._def : void 0;
689
- var sampleForDef = (def) => {
688
+ var sampleForDef = (def, options) => {
690
689
  const d = def;
691
690
  const name = d.typeName ?? d.type;
692
691
  switch (name) {
693
692
  case "ZodString":
694
693
  case "string": {
694
+ const isConstrained = Boolean(d.format) || d.check === "string_format" || Array.isArray(d.checks) && d.checks.length > 0;
695
+ if (options?.stringPrefix && !isConstrained) return `${options.stringPrefix}test`;
695
696
  return "test";
696
697
  }
697
698
  case "ZodNumber":
@@ -742,7 +743,7 @@ var sampleForDef = (def) => {
742
743
  case "default":
743
744
  case "ZodCatch":
744
745
  case "catch": {
745
- return sampleForDef(innerDef(d.innerType));
746
+ return sampleForDef(innerDef(d.innerType), options);
746
747
  }
747
748
  case "ZodEffects":
748
749
  case "effects":
@@ -752,18 +753,18 @@ var sampleForDef = (def) => {
752
753
  case "branded":
753
754
  case "ZodReadonly":
754
755
  case "readonly": {
755
- return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType));
756
+ return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType), options);
756
757
  }
757
758
  case "ZodTuple":
758
759
  case "tuple": {
759
760
  const items = d.items ?? [];
760
- return items.map((item) => sampleForDef(innerDef(item)));
761
+ return items.map((item) => sampleForDef(innerDef(item), options));
761
762
  }
762
763
  case "ZodUnion":
763
764
  case "union":
764
765
  case "ZodDiscriminatedUnion":
765
766
  case "discriminatedUnion": {
766
- return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0])) : void 0;
767
+ return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0]), options) : void 0;
767
768
  }
768
769
  case "ZodArray":
769
770
  case "array": {
@@ -774,7 +775,7 @@ var sampleForDef = (def) => {
774
775
  const out = {};
775
776
  const shape = typeof d.shape === "function" ? d.shape() : d.shape ?? {};
776
777
  for (const [key, valueSchema] of Object.entries(shape)) {
777
- const sample = sampleForDef(innerDef(valueSchema));
778
+ const sample = sampleForDef(innerDef(valueSchema), options);
778
779
  if (sample !== void 0) out[key] = sample;
779
780
  }
780
781
  return out;
@@ -792,11 +793,16 @@ var sampleForDef = (def) => {
792
793
  }
793
794
  }
794
795
  };
795
- var sampleSchemaInput = (schema) => {
796
+ var sampleSchemaInput = (schema, options) => {
796
797
  const node = schema;
797
- return sampleForDef(node.def ?? node._def);
798
+ return sampleForDef(node.def ?? node._def, options);
798
799
  };
799
800
 
801
+ // src/testDataMarker.ts
802
+ import { randomUUID } from "crypto";
803
+ var TEST_DATA_PREFIX = "lstest_";
804
+ var createTestDataMarker = () => `${TEST_DATA_PREFIX}${randomUUID().slice(0, 8)}_`;
805
+
800
806
  // src/reporter.ts
801
807
  var statusIcon = (status) => {
802
808
  switch (status) {
@@ -845,7 +851,7 @@ import crypto from "crypto";
845
851
  import fs from "fs";
846
852
  import path from "path";
847
853
  import { pathToFileURL } from "url";
848
- import { getCsrfConfig, getPrismaClient, getProjectConfig, getSrcDir, tryCatch as tryCatch4, tryCatchSync } from "@luckystack/core";
854
+ import { getCsrfConfig, getPrismaClient, getProjectConfig as getProjectConfig2, getSrcDir, tryCatch as tryCatch4, tryCatchSync } from "@luckystack/core";
849
855
 
850
856
  // src/streamWatcher.ts
851
857
  import { io as createSocketIoClient } from "socket.io-client";
@@ -1174,7 +1180,7 @@ var buildCallSync = (baseUrl, routePath, state, cookieName) => {
1174
1180
  };
1175
1181
  };
1176
1182
  var buildContext = (discovery, input) => {
1177
- const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;
1183
+ const cookieName = input.sessionCookieName ?? getProjectConfig2().http.sessionCookieName;
1178
1184
  const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
1179
1185
  const routeFullName = `sync/${routePath}`;
1180
1186
  const apiMethod = input.apiMethodMap?.[discovery.route.page]?.[discovery.route.name]?.[discovery.route.version] ?? "POST";
@@ -1297,10 +1303,21 @@ var runCustomTests = async (input) => {
1297
1303
  };
1298
1304
 
1299
1305
  // src/runAllTests.ts
1300
- import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as getProjectConfig2 } from "@luckystack/core";
1301
- var inputForEndpoint = (apiInputSchemas) => (endpoint) => {
1306
+ import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as getProjectConfig3 } from "@luckystack/core";
1307
+ var inputForEndpoint = (apiInputSchemas, stringPrefix) => (endpoint) => {
1302
1308
  const schema = apiInputSchemas[endpoint.page]?.[endpoint.name]?.[endpoint.version];
1303
- return schema ? sampleSchemaInput(schema) : {};
1309
+ return schema ? sampleSchemaInput(schema, { stringPrefix }) : {};
1310
+ };
1311
+ var resolveResetBookendToken = (input) => {
1312
+ if (input.resetBookends === false) return null;
1313
+ const token = input.resetToken ?? process.env.TEST_RESET_TOKEN;
1314
+ if (!token) {
1315
+ console.warn(
1316
+ "[test-runner] runAllTests: reset bookends skipped \u2014 no reset token (set TEST_RESET_TOKEN or pass resetToken/resetBookends). /_test/reset requires it, so the clean-slate + cleanup resets around the sweep are skipped."
1317
+ );
1318
+ return null;
1319
+ }
1320
+ return token;
1304
1321
  };
1305
1322
  var matchesFilter = (endpoint, filter) => {
1306
1323
  if (!filter) return true;
@@ -1314,7 +1331,7 @@ var cloneSummary = (summary, filter) => {
1314
1331
  var buildAuthHeaders = async (input) => {
1315
1332
  const headers = {};
1316
1333
  if (!input.authToken) return headers;
1317
- const cookieName = input.sessionCookieName ?? getProjectConfig2().http.sessionCookieName;
1334
+ const cookieName = input.sessionCookieName ?? getProjectConfig3().http.sessionCookieName;
1318
1335
  headers.Cookie = `${cookieName}=${input.authToken}`;
1319
1336
  try {
1320
1337
  const { getSession } = await import("@luckystack/login");
@@ -1332,6 +1349,10 @@ var buildAuthHeaders = async (input) => {
1332
1349
  return headers;
1333
1350
  };
1334
1351
  var runSweepLayers = async (input, summary, headers, inputFor) => {
1352
+ const resetBookendToken = resolveResetBookendToken(input);
1353
+ if (resetBookendToken) {
1354
+ await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
1355
+ }
1335
1356
  const contract = await runContractTests({
1336
1357
  apiMethodMap: input.apiMethodMap,
1337
1358
  baseUrl: input.baseUrl,
@@ -1358,9 +1379,13 @@ var runSweepLayers = async (input, summary, headers, inputFor) => {
1358
1379
  inputFor,
1359
1380
  //? Reset the shared per-IP rate-limit bucket between endpoints so a
1360
1381
  //? neighbour saturating the window doesn't cause false pass/fail on the
1361
- //? next endpoint. Requires the server to expose /_test/reset (allowed in
1362
- //? NODE_ENV !== production or behind TEST_RESET_TOKEN).
1363
- resetBetweenEndpoints: true
1382
+ //? next endpoint. Requires the server to expose /_test/reset (NODE_ENV in
1383
+ //? { 'development', 'test' } AND TEST_RESET_TOKEN set + matching header).
1384
+ resetBetweenEndpoints: true,
1385
+ //? Thread the reset token (explicit input, else the TEST_RESET_TOKEN env the
1386
+ //? server validates against) — without it `/_test/reset` returns 403 and the
1387
+ //? between-endpoint reset silently never happens.
1388
+ resetToken: input.resetToken ?? process.env.TEST_RESET_TOKEN
1364
1389
  });
1365
1390
  summary.rateLimit = cloneSummary(rateLimit, input.filter);
1366
1391
  }
@@ -1384,6 +1409,9 @@ var runSweepLayers = async (input, summary, headers, inputFor) => {
1384
1409
  });
1385
1410
  summary.fuzz = cloneSummary(fuzz, input.filter);
1386
1411
  }
1412
+ if (resetBookendToken) {
1413
+ await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
1414
+ }
1387
1415
  };
1388
1416
  var runCustomLayer = async (input, summary) => {
1389
1417
  try {
@@ -1437,7 +1465,7 @@ var runAllTests = async (input) => {
1437
1465
  }
1438
1466
  }
1439
1467
  const headers = await buildAuthHeaders(input);
1440
- const inputFor = inputForEndpoint(input.apiInputSchemas);
1468
+ const inputFor = inputForEndpoint(input.apiInputSchemas, createTestDataMarker());
1441
1469
  if (!input.noSweep) {
1442
1470
  await runSweepLayers(input, summary, headers, inputFor);
1443
1471
  }
@@ -1559,8 +1587,79 @@ var resetTestExtensionsForTests = () => {
1559
1587
  fixtures.clear();
1560
1588
  reporter = null;
1561
1589
  };
1590
+
1591
+ // src/runRegisteredLayers.ts
1592
+ import { tryCatch as tryCatch5, tryCatchSync as tryCatchSync2 } from "@luckystack/core";
1593
+ var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
1594
+ var postWebhook = async (summary) => {
1595
+ const reporter2 = getTestReporter();
1596
+ const webhookUrl = reporter2?.webhookUrl;
1597
+ if (!reporter2 || !webhookUrl) return;
1598
+ const [, parsedUrl] = tryCatchSync2(() => new URL(webhookUrl));
1599
+ if (parsedUrl?.protocol === "http:" && !isLoopbackHost(parsedUrl.hostname)) {
1600
+ console.warn(
1601
+ `[test-runner] reporter.webhookUrl is plaintext http to a non-loopback host (${parsedUrl.hostname}); the summary (endpoint paths, error codes, metadata) and any bearer token are sent unencrypted.`
1602
+ );
1603
+ }
1604
+ const headers = { "Content-Type": "application/json" };
1605
+ if (reporter2.webhookAuth?.type === "bearer") headers.Authorization = `Bearer ${reporter2.webhookAuth.token}`;
1606
+ const controller = new AbortController();
1607
+ const timeoutHandle = setTimeout(() => {
1608
+ controller.abort();
1609
+ }, 1e4);
1610
+ await tryCatch5(() => fetch(webhookUrl, {
1611
+ method: "POST",
1612
+ headers,
1613
+ body: JSON.stringify(summary),
1614
+ signal: controller.signal
1615
+ }));
1616
+ clearTimeout(timeoutHandle);
1617
+ };
1618
+ var runRegisteredLayers = async (input) => {
1619
+ const layers2 = listTestLayers();
1620
+ const endpoints = walkEndpoints(input.apiMethodMap);
1621
+ const reporter2 = getTestReporter();
1622
+ const skip = input.skip ?? [];
1623
+ const results = [];
1624
+ for (const layer of layers2) {
1625
+ for (const endpoint of endpoints) {
1626
+ if (shouldSkip(endpoint, skip)) continue;
1627
+ const started = Date.now();
1628
+ const [runError, layerResult] = await tryCatch5(async () => layer.run({ endpoint: endpoint.fullPath, method: endpoint.method, authToken: input.authToken }));
1629
+ const durationMs = Date.now() - started;
1630
+ const result = runError || !layerResult ? {
1631
+ layer: layer.name,
1632
+ endpoint: endpoint.fullPath,
1633
+ passed: false,
1634
+ message: runError?.message ?? "layer returned no result",
1635
+ durationMs
1636
+ } : {
1637
+ layer: layer.name,
1638
+ endpoint: endpoint.fullPath,
1639
+ passed: layerResult.passed,
1640
+ message: layerResult.message,
1641
+ durationMs,
1642
+ metadata: layerResult.metadata
1643
+ };
1644
+ results.push(result);
1645
+ await tryCatch5(async () => reporter2?.onResult?.(result));
1646
+ }
1647
+ }
1648
+ const summary = {
1649
+ totalLayers: layers2.length,
1650
+ totalEndpoints: endpoints.length,
1651
+ passed: results.filter((r) => r.passed).length,
1652
+ failed: results.filter((r) => !r.passed).length,
1653
+ results
1654
+ };
1655
+ await tryCatch5(async () => reporter2?.onSummary?.(summary));
1656
+ await postWebhook(summary);
1657
+ return summary;
1658
+ };
1562
1659
  export {
1563
1660
  LAYER_KEYS,
1661
+ TEST_DATA_PREFIX,
1662
+ createTestDataMarker,
1564
1663
  discoverCustomTestFiles,
1565
1664
  getTestFixture,
1566
1665
  getTestReporter,
@@ -1586,6 +1685,7 @@ export {
1586
1685
  runFuzzTests,
1587
1686
  runRateLimitCheck,
1588
1687
  runRateLimitTests,
1688
+ runRegisteredLayers,
1589
1689
  sampleSchemaInput,
1590
1690
  walkEndpoints,
1591
1691
  walkSyncEndpoints