@luckystack/test-runner 0.14.0 → 0.15.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.
@@ -8,7 +8,7 @@ export interface ProbeRequestInput {
8
8
  requestTimeoutMs: number;
9
9
  }
10
10
  export interface ProbeResponse {
11
- /** HTTP status. Absent when the probe travelled over Socket.io. */
11
+ /** HTTP status. Over Socket.io: the envelope's `httpStatus`, when it carries one. */
12
12
  httpStatus?: number;
13
13
  parsed: {
14
14
  status?: string;
@@ -25,7 +25,7 @@ export interface ProbeTransportRequest {
25
25
  baseUrl: string;
26
26
  /** The route payload — the API `data`, or the sync `data` (the transport adds the sync envelope). */
27
27
  body: unknown;
28
- /** HTTP headers (session Cookie, CSRF). A socket transport reads only the Cookie. */
28
+ /** HTTP headers (session Cookie or Bearer, CSRF). A socket transport reads only the session. */
29
29
  headers?: Record<string, string>;
30
30
  requestTimeoutMs: number;
31
31
  }
@@ -19,6 +19,19 @@ export interface RunAllTestsInput {
19
19
  * probes over HTTP — CSRF is an HTTP-only guard.
20
20
  */
21
21
  syncTransport?: 'socket' | 'http';
22
+ /**
23
+ * How API routes are probed. Defaults to the project's own
24
+ * `transport.invocation`: `'socket'` → Socket.io (the `apiRequest` pipeline
25
+ * real clients hit), `'routed-http'` → the `/api/*` HTTP route. The CSRF
26
+ * layer always probes over HTTP — CSRF is an HTTP-only guard.
27
+ */
28
+ apiTransport?: 'socket' | 'http';
29
+ /**
30
+ * Routes designed to fail on the server (e.g. a playground route that throws
31
+ * on purpose), matched like `skip`. The contract layer lets their shaped 5xx
32
+ * pass; any other route answering 5xx fails it.
33
+ */
34
+ expectServerError?: string[];
22
35
  /**
23
36
  * Write the full `RunAllTestsSummary` as JSON to this path (relative to the
24
37
  * process cwd). Defaults to `TEST_OUTPUT_FILE`, else `test-results.json`;
@@ -34,9 +47,13 @@ export interface RunAllTestsInput {
34
47
  apiMetaMap: ApiMetaMap;
35
48
  apiInputSchemas: InputSchemaMap;
36
49
  baseUrl: string;
37
- /** Cookie name for the session token. Defaults to the project's configured session cookie name (`projectConfig.http.sessionCookieName`). */
50
+ /** Cookie name for the session token. Defaults to the project's configured session cookie name (`projectConfig.http.sessionCookieName`, with `http.sessionCookiePrefix` applied). */
38
51
  sessionCookieName?: string;
39
- /** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */
52
+ /**
53
+ * Session token for sweep-layer requests that need a session: sent as the
54
+ * session `Cookie` in cookie mode, as `Authorization: Bearer` in token mode
55
+ * (`session.basedToken`).
56
+ */
40
57
  authToken?: string;
41
58
  /** Endpoints to skip from the sweep layers, e.g. routes that need a file upload. */
42
59
  skip?: string[];
@@ -10,8 +10,10 @@ export interface RunAuthEnforcementTestsInput {
10
10
  /** Substring filter on `<page>/<name>/<version>`; other routes are not requested at all. */
11
11
  filter?: string;
12
12
  inputFor?: (endpoint: EndpointDescriptor) => unknown;
13
- /** Transport for sync routes (e.g. the socket transport). API routes always use HTTP. */
13
+ /** Transport for sync routes (e.g. the socket transport). Defaults to HTTP. */
14
14
  syncTransport?: ProbeTransport;
15
+ /** Transport for API routes (e.g. the socket transport). Defaults to HTTP. */
16
+ apiTransport?: ProbeTransport;
15
17
  onResult?: (result: ContractCheckResult) => void;
16
18
  }
17
19
  export declare const runAuthEnforcementTests: (input: RunAuthEnforcementTestsInput) => Promise<RunContractSummary>;
@@ -11,12 +11,26 @@ export interface RunContractTestsInput {
11
11
  * are known to need real input (e.g. a file upload).
12
12
  */
13
13
  skip?: string[];
14
+ /**
15
+ * Routes designed to fail on the server (e.g. a playground route that throws
16
+ * on purpose), matched like `skip`. Their shaped 5xx reply passes; every
17
+ * other route's 5xx fails the layer.
18
+ */
19
+ expectServerError?: string[];
14
20
  /** Substring filter on `<page>/<name>/<version>`; other routes are not requested at all. */
15
21
  filter?: string;
16
22
  inputFor?: (endpoint: EndpointDescriptor) => unknown;
17
23
  headers?: Record<string, string>;
18
- /** Transport for sync routes (e.g. the socket transport). API routes always use HTTP. */
24
+ /** Transport for sync routes (e.g. the socket transport). Defaults to HTTP. */
19
25
  syncTransport?: ProbeTransport;
26
+ /** Transport for API routes (e.g. the socket transport). Defaults to HTTP. */
27
+ apiTransport?: ProbeTransport;
28
+ /**
29
+ * When given, rate limits are reset (`/_test/reset?only=rateLimits`) before
30
+ * each route, so the shared per-IP bucket cannot fill up mid-sweep and turn
31
+ * the remaining probes into limiter rejections.
32
+ */
33
+ resetToken?: string;
20
34
  onResult?: (result: ContractCheckResult) => void;
21
35
  }
22
36
  export declare const runContractTests: (input: RunContractTestsInput) => Promise<RunContractSummary>;
@@ -9,8 +9,16 @@ export interface RunFuzzTestsInput {
9
9
  /** Substring filter on `<page>/<name>/<version>`; other routes are not requested at all. */
10
10
  filter?: string;
11
11
  headers?: Record<string, string>;
12
- /** Transport for sync routes (e.g. the socket transport). API routes always use HTTP. */
12
+ /** Transport for sync routes (e.g. the socket transport). Defaults to HTTP. */
13
13
  syncTransport?: ProbeTransport;
14
+ /** Transport for API routes (e.g. the socket transport). Defaults to HTTP. */
15
+ apiTransport?: ProbeTransport;
16
+ /**
17
+ * When given, rate limits are reset (`/_test/reset?only=rateLimits`) before
18
+ * each route, so the shared per-IP bucket cannot fill up mid-sweep and turn
19
+ * the remaining payloads into limiter rejections.
20
+ */
21
+ resetToken?: string;
14
22
  onResult?: (result: ContractCheckResult) => void;
15
23
  }
16
24
  export declare const runFuzzTests: (input: RunFuzzTestsInput) => Promise<RunContractSummary>;
@@ -11,8 +11,10 @@ export interface RunRateLimitTestsInput {
11
11
  filter?: string;
12
12
  inputFor?: (endpoint: EndpointDescriptor) => unknown;
13
13
  headers?: Record<string, string>;
14
- /** Transport for sync routes (e.g. the socket transport). API routes always use HTTP. */
14
+ /** Transport for sync routes (e.g. the socket transport). Defaults to HTTP. */
15
15
  syncTransport?: ProbeTransport;
16
+ /** Transport for API routes (e.g. the socket transport). Defaults to HTTP. */
17
+ apiTransport?: ProbeTransport;
16
18
  /**
17
19
  * Max rateLimit value to test. Endpoints with a higher limit are skipped
18
20
  * to avoid firing thousands of requests in CI.
@@ -29,8 +29,18 @@ export interface SelectEndpointsInput {
29
29
  export declare const selectEndpoints: (input: SelectEndpointsInput) => EndpointDescriptor[];
30
30
  /** The meta map that describes `endpoint`: `syncMetaMap` for sync routes, else `apiMetaMap`. */
31
31
  export declare const metaMapFor: (endpoint: EndpointDescriptor, apiMetaMap: ApiMetaMap, syncMetaMap: SyncMetaMap | undefined) => RouteMetaMap;
32
- /** The transport for `endpoint`: `syncTransport` for sync routes when given, else HTTP. */
33
- export declare const transportFor: (endpoint: EndpointDescriptor, syncTransport: ProbeTransport | undefined) => ProbeTransport;
32
+ /**
33
+ * The transport for `endpoint`: `syncTransport` for sync routes and
34
+ * `apiTransport` for API routes when given, else HTTP.
35
+ */
36
+ export declare const transportFor: (endpoint: EndpointDescriptor, syncTransport: ProbeTransport | undefined, apiTransport?: ProbeTransport) => ProbeTransport;
37
+ /**
38
+ * Whether sweep headers carry a session: a session Cookie (cookie mode) or an
39
+ * `Authorization: Bearer` token (token mode). Layers that must not fire junk
40
+ * at mutating routes treat both as an authenticated sweep.
41
+ */
42
+ export declare const hasSessionHeaders: (headers: Record<string, string> | undefined) => boolean;
43
+ export declare const isRateLimitedCode: (errorCode: string | undefined) => boolean;
34
44
  /**
35
45
  * Whether an endpoint is in the explicit skip list. Matched against
36
46
  * `<page>/<name>` (version-agnostic) and `<page>/<name>/<version>`
@@ -1,4 +1,5 @@
1
1
  import { Socket } from 'socket.io-client';
2
+ export declare const projectSessionCookieName: () => string;
2
3
  export declare const nextResponseIndex: () => number;
3
4
  export interface ConnectTestSocketInput {
4
5
  baseUrl: string;
@@ -56,3 +57,15 @@ export type EmitSyncOutcome = {
56
57
  * resolve with the server's single reply on `sync-<responseIndex>`.
57
58
  */
58
59
  export declare const emitSyncOverSocket: (socket: Socket, input: EmitSyncInput) => Promise<EmitSyncOutcome>;
60
+ export interface EmitApiInput {
61
+ /** `<page>/<name>/<version>` — the route path without the `api/` prefix. */
62
+ routePath: string;
63
+ data: unknown;
64
+ timeoutMs: number;
65
+ }
66
+ /**
67
+ * Send one API request over `socket` exactly like the real client does in
68
+ * socket mode and resolve with the server's single reply on
69
+ * `apiResponse-<responseIndex>`.
70
+ */
71
+ export declare const emitApiOverSocket: (socket: Socket, input: EmitApiInput) => Promise<EmitSyncOutcome>;
@@ -4,13 +4,19 @@ Generated route maps drive a smoke check over the API surface so tests catch mis
4
4
 
5
5
  ## Probes follow the project transport
6
6
 
7
- A route is only meaningfully tested over the path real users hit (ADR 0064). API routes go over HTTP. Sync routes go over the project's own `transport.invocation`: Socket.io in a socket-mode project (the default), the routed `/sync/*` HTTP path in a routed-HTTP project; the caller may override this. The CSRF layer always probes sync over HTTP, because CSRF is an HTTP-only guard. Layer-5 `ctx.callSync` follows the same rule, so a socket-mode project's custom sync tests run through the socket pipeline and differences between the two pipelines surface as failures instead of being masked.
7
+ A route is only meaningfully tested over the path real users hit (ADR 0064). API and sync routes both go over the project's own `transport.invocation`: Socket.io in a socket-mode project (the default), the routed `/api/*` and `/sync/*` HTTP paths in a routed-HTTP project; the caller may override each. The CSRF layer always probes over HTTP, because CSRF is an HTTP-only guard. Layer-5 `ctx.callApi` and `ctx.callSync` follow the same rule, so a socket-mode project's custom tests run through the socket pipelines and differences between the pipelines surface as failures instead of being masked.
8
8
 
9
9
  - Sync routes are swept only when the sweep receives the generated sync metadata (and sync input schemas for valid samples). Without it the sweep warns and covers API routes only.
10
10
  - All sync probes in one run target a single throwaway room that the sweep's own session joins, so room-membership enforcement sees a member rather than rejecting every probe.
11
11
  - Sockets the runner opens on the caller's session close as an intentional disconnect, the signal a real client sends; a plain close would let presence log that session out shortly after the sweep.
12
12
  - GET probes carry their payload in the query string under the reserved key the real client uses (ADR 0062); `fetch` cannot send a GET body, and dropping the payload made every GET route with a required field fail validation.
13
13
 
14
+ ## What counts as a pass
15
+
16
+ A shaped error envelope is not enough on its own. A routing error (unknown route, wrong method, invalid name) means the generated route map and the live server disagree, and a 5xx or execution-failed reply means the handler crashed on a well-formed probe; both fail the layer. A route that is designed to fail on the server is listed explicitly as expected to error, so its 5xx passes without hiding everyone else's. A rate-limited reply never reached validation or the handler, so it is reported as skipped rather than passed; with a reset token the sweep clears rate limits between routes so the shared per-IP bucket does not fill up mid-sweep.
17
+
18
+ In token mode (`session.basedToken`) the sweep sends its session as a Bearer token, the token-mode credential, and skips the CSRF layer: token-mode requests carry no ambient cookie, so the server does not enforce CSRF, and probing would only run mutating handlers.
19
+
14
20
  ## Sweep boundaries
15
21
 
16
22
  A route filter narrows the selection before any request is sent; filtered-out routes are never called. An authenticated sweep calls every selected route with its own session, logout-type routes included: a route that destroys the current session (logout, account deletion, session revoke) leaves every later layer running anonymously, so skip such routes explicitly.
@@ -4,4 +4,4 @@ This slow layer deliberately sends malformed inputs to exercise error shaping an
4
4
 
5
5
  In an authenticated sweep, state-changing routes are reported as skipped so junk bodies cannot mutate the test database. Every sync route counts as state-changing, so an authenticated sweep does not fuzz sync routes at all; an anonymous sweep does. GET probes carry the junk payload in the query string like the real client (ADR 0062).
6
6
 
7
- A rejected unauthenticated request does not prove protected handler input was fuzzed; create authenticated, project-specific tests where necessary. Keep test traffic away from production and avoid payloads that trigger irreversible side effects. The fuzz source defines the actual probe catalogue; do not duplicate it as a static list here.
7
+ A rate-limited reply means the payload never reached validation or the handler, so a route whose probes hit the limiter is reported as skipped, not passed; with a reset token the sweep clears rate limits before each route. A rejected unauthenticated request does not prove protected handler input was fuzzed; create authenticated, project-specific tests where necessary. Keep test traffic away from production and avoid payloads that trigger irreversible side effects. The fuzz source defines the actual probe catalogue; do not duplicate it as a static list here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/test-runner",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -54,11 +54,11 @@
54
54
  "test": "vitest run"
55
55
  },
56
56
  "dependencies": {
57
- "@luckystack/core": "^0.14.0"
57
+ "@luckystack/core": "^0.15.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@luckystack/login": "^0.14.0",
61
- "@luckystack/secret-manager": "^0.14.0",
60
+ "@luckystack/login": "^0.15.0",
61
+ "@luckystack/secret-manager": "^0.15.0",
62
62
  "socket.io-client": "^4.8.0",
63
63
  "zod": "^4.0.0"
64
64
  },