@nifrajs/runner 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nifra Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @nifrajs/runner
2
+
3
+ Run requests through a nifra app and capture **structured results** — the shared engine behind the
4
+ website playground (humans) and the agent run/verify tool (an AI writes code → runs it → sees the
5
+ failure → fixes it). It only touches `app.fetch(Request) → Response`, which is Web-standard, so it runs
6
+ unchanged in the browser, Bun, Node, Deno, and on the edge — and depends on nothing.
7
+
8
+ ```ts
9
+ import { runApp } from "@nifrajs/runner"
10
+ import { app } from "./app"
11
+
12
+ const results = await runApp(app, [
13
+ { path: "/users/1" },
14
+ { method: "POST", path: "/users", body: { name: "Ada" } },
15
+ ])
16
+ // → [{ method, path, ok, status, headers, body, durationMs }, …]
17
+ ```
18
+
19
+ - **`runApp(app, requests, options?)`** — run a batch in order; one `RunResult` per request. Never
20
+ throws: a crash on any request becomes that result's `error` and the batch continues.
21
+ - **`runRequest(app, spec, options?)`** — a single request.
22
+
23
+ A plain object/array `body` is JSON-encoded (with a JSON content-type unless you set one); a
24
+ string/`Uint8Array` is sent as-is. Relative paths resolve against `options.origin`
25
+ (default `http://nifra.local`). Body text is captured up to `options.maxBodyChars` (default 64 KB).
26
+
27
+ It's a **runner, not a security sandbox**: it calls the app you hand it in-process, with no isolation.
28
+ Isolation, when you need it, comes from the host — the browser tab for the playground, your own
29
+ process/CI for the agent runner. Don't feed it code you wouldn't already run.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Run requests through a nifra app and capture **structured results** — the shared engine behind the
3
+ * website playground (humans) and the agent run/verify tool (AI writes code → runs it → sees the
4
+ * failure → fixes). It only touches `app.fetch(Request) → Response`, which is Web-standard, so it runs
5
+ * unchanged in the browser, Bun, Node, Deno, and on the edge — and depends on nothing.
6
+ *
7
+ * import { runApp } from "@nifrajs/runner"
8
+ * const results = await runApp(app, [
9
+ * { path: "/users/1" },
10
+ * { method: "POST", path: "/users", body: { name: "Ada" } },
11
+ * ])
12
+ * // → [{ status: 200, ok: true, body: {...}, durationMs }, …]
13
+ *
14
+ * It is a *runner*, not a security sandbox: it calls the app you hand it in-process, with no isolation.
15
+ * Isolation, when you need it, comes from the host — the browser tab for the playground, your own
16
+ * process/CI for the agent runner. Don't feed it code you wouldn't already run.
17
+ */
18
+ /** Anything with a Web-standard fetch handler — a nifra app, or any `(Request) => Response`. Declared
19
+ * structurally so this package has zero dependency on `@nifrajs/core`. */
20
+ export interface AppLike {
21
+ fetch(request: Request): Response | Promise<Response>;
22
+ }
23
+ /** One request to drive through the app. */
24
+ export interface RequestSpec {
25
+ /** HTTP method. Default `"GET"`. */
26
+ readonly method?: string;
27
+ /** Path (`"/users/1"`) or an absolute URL. Relative paths resolve against a local origin. */
28
+ readonly path: string;
29
+ /** Request headers. */
30
+ readonly headers?: Readonly<Record<string, string>>;
31
+ /** Body. A plain object/array is JSON-encoded (with a JSON content-type unless you set one); a
32
+ * string/`Uint8Array` is sent as-is. Ignored for GET/HEAD (those can't carry a body). */
33
+ readonly body?: unknown;
34
+ /** Optional label echoed back on the result (handy in the playground UI). */
35
+ readonly label?: string;
36
+ }
37
+ /** The captured outcome of one request. */
38
+ export interface RunResult {
39
+ readonly label?: string;
40
+ readonly method: string;
41
+ readonly path: string;
42
+ /** `true` when the app returned a 2xx response. `false` for non-2xx, or when `app.fetch` threw. */
43
+ readonly ok: boolean;
44
+ /** HTTP status — present when the app returned a response (absent when it threw). */
45
+ readonly status?: number;
46
+ readonly statusText?: string;
47
+ readonly headers?: Readonly<Record<string, string>>;
48
+ /** Parsed JSON when the response is JSON; otherwise the (possibly truncated) text. */
49
+ readonly body?: unknown;
50
+ /** The response body as text, truncated to `maxBodyChars`. */
51
+ readonly bodyText?: string;
52
+ /** `true` when `bodyText` was cut to the cap. */
53
+ readonly truncated?: boolean;
54
+ /** Wall-clock time for this request, milliseconds. */
55
+ readonly durationMs: number;
56
+ /** Set when `app.fetch` threw (the app crashed) rather than returning a response. */
57
+ readonly error?: {
58
+ readonly name: string;
59
+ readonly message: string;
60
+ readonly stack?: string;
61
+ };
62
+ }
63
+ export interface RunOptions {
64
+ /** Origin used to resolve relative paths. Default `"http://nifra.local"`. */
65
+ readonly origin?: string;
66
+ /** Cap on captured body text, in characters. Default 65536. */
67
+ readonly maxBodyChars?: number;
68
+ }
69
+ /** Drive a single request through the app, capturing the outcome (never throws — a thrown app error
70
+ * becomes `result.error`). */
71
+ export declare function runRequest(app: AppLike, spec: RequestSpec, options?: RunOptions): Promise<RunResult>;
72
+ /**
73
+ * Run a batch of requests through the app, in order, and return a result per request. Never throws:
74
+ * an app crash on any request is captured as that result's `error` and the run continues.
75
+ */
76
+ export declare function runApp(app: AppLike, requests: readonly RequestSpec[], options?: RunOptions): Promise<RunResult[]>;
77
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;0EAC0E;AAC1E,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtD;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uBAAuB;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD;6FACyF;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC9F;AAED,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAgDD;8BAC8B;AAC9B,wBAAsB,UAAU,CAC9B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,SAAS,CAAC,CA+CpB;AASD;;;GAGG;AACH,wBAAgB,MAAM,CACpB,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,SAAS,EAAE,CAAC,CAMtB"}
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Run requests through a nifra app and capture **structured results** — the shared engine behind the
3
+ * website playground (humans) and the agent run/verify tool (AI writes code → runs it → sees the
4
+ * failure → fixes). It only touches `app.fetch(Request) → Response`, which is Web-standard, so it runs
5
+ * unchanged in the browser, Bun, Node, Deno, and on the edge — and depends on nothing.
6
+ *
7
+ * import { runApp } from "@nifrajs/runner"
8
+ * const results = await runApp(app, [
9
+ * { path: "/users/1" },
10
+ * { method: "POST", path: "/users", body: { name: "Ada" } },
11
+ * ])
12
+ * // → [{ status: 200, ok: true, body: {...}, durationMs }, …]
13
+ *
14
+ * It is a *runner*, not a security sandbox: it calls the app you hand it in-process, with no isolation.
15
+ * Isolation, when you need it, comes from the host — the browser tab for the playground, your own
16
+ * process/CI for the agent runner. Don't feed it code you wouldn't already run.
17
+ */
18
+ const DEFAULT_ORIGIN = "http://nifra.local";
19
+ const DEFAULT_MAX_BODY = 64 * 1024;
20
+ const now = () => typeof performance !== "undefined" && typeof performance.now === "function"
21
+ ? performance.now()
22
+ : Date.now();
23
+ const METHODS_WITHOUT_BODY = new Set(["GET", "HEAD"]);
24
+ /** A plain JSON-ish object/array (encode it as JSON) — vs a string/binary body we pass through. */
25
+ function isJsonBody(body) {
26
+ if (body === null || typeof body !== "object")
27
+ return false;
28
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || body instanceof Blob)
29
+ return false;
30
+ if (typeof FormData !== "undefined" && body instanceof FormData)
31
+ return false;
32
+ if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream)
33
+ return false;
34
+ return true;
35
+ }
36
+ function buildRequest(spec, origin) {
37
+ const method = (spec.method ?? "GET").toUpperCase();
38
+ const url = /^https?:\/\//i.test(spec.path) ? spec.path : new URL(spec.path, origin).toString();
39
+ const headers = new Headers(spec.headers);
40
+ const init = { method, headers };
41
+ if (spec.body !== undefined && !METHODS_WITHOUT_BODY.has(method)) {
42
+ if (isJsonBody(spec.body)) {
43
+ if (!headers.has("content-type"))
44
+ headers.set("content-type", "application/json");
45
+ init.body = JSON.stringify(spec.body);
46
+ }
47
+ else {
48
+ init.body = spec.body;
49
+ }
50
+ }
51
+ return new Request(url, init);
52
+ }
53
+ function headersToObject(headers) {
54
+ const out = {};
55
+ headers.forEach((value, key) => {
56
+ out[key] = value;
57
+ });
58
+ return out;
59
+ }
60
+ /** Drive a single request through the app, capturing the outcome (never throws — a thrown app error
61
+ * becomes `result.error`). */
62
+ export async function runRequest(app, spec, options = {}) {
63
+ const origin = options.origin ?? DEFAULT_ORIGIN;
64
+ const maxBody = options.maxBodyChars ?? DEFAULT_MAX_BODY;
65
+ const method = (spec.method ?? "GET").toUpperCase();
66
+ const base = {
67
+ ...(spec.label === undefined ? {} : { label: spec.label }),
68
+ method,
69
+ path: spec.path,
70
+ };
71
+ const start = now();
72
+ let request;
73
+ try {
74
+ request = buildRequest(spec, origin);
75
+ }
76
+ catch (err) {
77
+ // A malformed spec (bad URL, GET-with-body via a custom method, …) — report it, don't throw.
78
+ return { ...base, ok: false, durationMs: now() - start, error: toError(err) };
79
+ }
80
+ try {
81
+ const res = await app.fetch(request);
82
+ const text = await res.text();
83
+ const truncated = text.length > maxBody;
84
+ const bodyText = truncated ? text.slice(0, maxBody) : text;
85
+ const contentType = res.headers.get("content-type") ?? "";
86
+ let body = bodyText;
87
+ if (contentType.includes("application/json") && text.length > 0) {
88
+ try {
89
+ body = JSON.parse(text);
90
+ }
91
+ catch {
92
+ // Content-Type lied, or the body is partial — keep the text form.
93
+ }
94
+ }
95
+ return {
96
+ ...base,
97
+ ok: res.ok,
98
+ status: res.status,
99
+ statusText: res.statusText,
100
+ headers: headersToObject(res.headers),
101
+ body,
102
+ bodyText,
103
+ ...(truncated ? { truncated } : {}),
104
+ durationMs: now() - start,
105
+ };
106
+ }
107
+ catch (err) {
108
+ return { ...base, ok: false, durationMs: now() - start, error: toError(err) };
109
+ }
110
+ }
111
+ function toError(err) {
112
+ if (err instanceof Error) {
113
+ return { name: err.name, message: err.message, ...(err.stack ? { stack: err.stack } : {}) };
114
+ }
115
+ return { name: "Error", message: String(err) };
116
+ }
117
+ /**
118
+ * Run a batch of requests through the app, in order, and return a result per request. Never throws:
119
+ * an app crash on any request is captured as that result's `error` and the run continues.
120
+ */
121
+ export function runApp(app, requests, options = {}) {
122
+ return requests.reduce(async (acc, spec) => {
123
+ const results = await acc;
124
+ results.push(await runRequest(app, spec, options));
125
+ return results;
126
+ }, Promise.resolve([]));
127
+ }
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@nifrajs/runner",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Run requests through a nifra app's fetch and capture structured results — the engine behind the playground and the agent run/verify tool. Dependency-free; runs in the browser, Bun, Node, Deno, and on the edge.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "edge",
12
+ "ssr"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "bun": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "sideEffects": false,
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
32
+ },
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/nifrajs/nifra.git",
40
+ "directory": "packages/runner"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/nifrajs/nifra/issues"
44
+ },
45
+ "homepage": "https://github.com/nifrajs/nifra#readme"
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Run requests through a nifra app and capture **structured results** — the shared engine behind the
3
+ * website playground (humans) and the agent run/verify tool (AI writes code → runs it → sees the
4
+ * failure → fixes). It only touches `app.fetch(Request) → Response`, which is Web-standard, so it runs
5
+ * unchanged in the browser, Bun, Node, Deno, and on the edge — and depends on nothing.
6
+ *
7
+ * import { runApp } from "@nifrajs/runner"
8
+ * const results = await runApp(app, [
9
+ * { path: "/users/1" },
10
+ * { method: "POST", path: "/users", body: { name: "Ada" } },
11
+ * ])
12
+ * // → [{ status: 200, ok: true, body: {...}, durationMs }, …]
13
+ *
14
+ * It is a *runner*, not a security sandbox: it calls the app you hand it in-process, with no isolation.
15
+ * Isolation, when you need it, comes from the host — the browser tab for the playground, your own
16
+ * process/CI for the agent runner. Don't feed it code you wouldn't already run.
17
+ */
18
+
19
+ /** Anything with a Web-standard fetch handler — a nifra app, or any `(Request) => Response`. Declared
20
+ * structurally so this package has zero dependency on `@nifrajs/core`. */
21
+ export interface AppLike {
22
+ fetch(request: Request): Response | Promise<Response>
23
+ }
24
+
25
+ /** One request to drive through the app. */
26
+ export interface RequestSpec {
27
+ /** HTTP method. Default `"GET"`. */
28
+ readonly method?: string
29
+ /** Path (`"/users/1"`) or an absolute URL. Relative paths resolve against a local origin. */
30
+ readonly path: string
31
+ /** Request headers. */
32
+ readonly headers?: Readonly<Record<string, string>>
33
+ /** Body. A plain object/array is JSON-encoded (with a JSON content-type unless you set one); a
34
+ * string/`Uint8Array` is sent as-is. Ignored for GET/HEAD (those can't carry a body). */
35
+ readonly body?: unknown
36
+ /** Optional label echoed back on the result (handy in the playground UI). */
37
+ readonly label?: string
38
+ }
39
+
40
+ /** The captured outcome of one request. */
41
+ export interface RunResult {
42
+ readonly label?: string
43
+ readonly method: string
44
+ readonly path: string
45
+ /** `true` when the app returned a 2xx response. `false` for non-2xx, or when `app.fetch` threw. */
46
+ readonly ok: boolean
47
+ /** HTTP status — present when the app returned a response (absent when it threw). */
48
+ readonly status?: number
49
+ readonly statusText?: string
50
+ readonly headers?: Readonly<Record<string, string>>
51
+ /** Parsed JSON when the response is JSON; otherwise the (possibly truncated) text. */
52
+ readonly body?: unknown
53
+ /** The response body as text, truncated to `maxBodyChars`. */
54
+ readonly bodyText?: string
55
+ /** `true` when `bodyText` was cut to the cap. */
56
+ readonly truncated?: boolean
57
+ /** Wall-clock time for this request, milliseconds. */
58
+ readonly durationMs: number
59
+ /** Set when `app.fetch` threw (the app crashed) rather than returning a response. */
60
+ readonly error?: { readonly name: string; readonly message: string; readonly stack?: string }
61
+ }
62
+
63
+ export interface RunOptions {
64
+ /** Origin used to resolve relative paths. Default `"http://nifra.local"`. */
65
+ readonly origin?: string
66
+ /** Cap on captured body text, in characters. Default 65536. */
67
+ readonly maxBodyChars?: number
68
+ }
69
+
70
+ const DEFAULT_ORIGIN = "http://nifra.local"
71
+ const DEFAULT_MAX_BODY = 64 * 1024
72
+
73
+ const now = (): number =>
74
+ typeof performance !== "undefined" && typeof performance.now === "function"
75
+ ? performance.now()
76
+ : Date.now()
77
+
78
+ const METHODS_WITHOUT_BODY: ReadonlySet<string> = new Set(["GET", "HEAD"])
79
+
80
+ /** A plain JSON-ish object/array (encode it as JSON) — vs a string/binary body we pass through. */
81
+ function isJsonBody(body: unknown): body is Record<string, unknown> | unknown[] {
82
+ if (body === null || typeof body !== "object") return false
83
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || body instanceof Blob)
84
+ return false
85
+ if (typeof FormData !== "undefined" && body instanceof FormData) return false
86
+ if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) return false
87
+ return true
88
+ }
89
+
90
+ function buildRequest(spec: RequestSpec, origin: string): Request {
91
+ const method = (spec.method ?? "GET").toUpperCase()
92
+ const url = /^https?:\/\//i.test(spec.path) ? spec.path : new URL(spec.path, origin).toString()
93
+
94
+ const headers = new Headers(spec.headers as Record<string, string> | undefined)
95
+ const init: RequestInit = { method, headers }
96
+
97
+ if (spec.body !== undefined && !METHODS_WITHOUT_BODY.has(method)) {
98
+ if (isJsonBody(spec.body)) {
99
+ if (!headers.has("content-type")) headers.set("content-type", "application/json")
100
+ init.body = JSON.stringify(spec.body)
101
+ } else {
102
+ init.body = spec.body as RequestInit["body"]
103
+ }
104
+ }
105
+ return new Request(url, init)
106
+ }
107
+
108
+ function headersToObject(headers: Headers): Record<string, string> {
109
+ const out: Record<string, string> = {}
110
+ headers.forEach((value, key) => {
111
+ out[key] = value
112
+ })
113
+ return out
114
+ }
115
+
116
+ /** Drive a single request through the app, capturing the outcome (never throws — a thrown app error
117
+ * becomes `result.error`). */
118
+ export async function runRequest(
119
+ app: AppLike,
120
+ spec: RequestSpec,
121
+ options: RunOptions = {},
122
+ ): Promise<RunResult> {
123
+ const origin = options.origin ?? DEFAULT_ORIGIN
124
+ const maxBody = options.maxBodyChars ?? DEFAULT_MAX_BODY
125
+ const method = (spec.method ?? "GET").toUpperCase()
126
+ const base = {
127
+ ...(spec.label === undefined ? {} : { label: spec.label }),
128
+ method,
129
+ path: spec.path,
130
+ }
131
+
132
+ const start = now()
133
+ let request: Request
134
+ try {
135
+ request = buildRequest(spec, origin)
136
+ } catch (err) {
137
+ // A malformed spec (bad URL, GET-with-body via a custom method, …) — report it, don't throw.
138
+ return { ...base, ok: false, durationMs: now() - start, error: toError(err) }
139
+ }
140
+
141
+ try {
142
+ const res = await app.fetch(request)
143
+ const text = await res.text()
144
+ const truncated = text.length > maxBody
145
+ const bodyText = truncated ? text.slice(0, maxBody) : text
146
+ const contentType = res.headers.get("content-type") ?? ""
147
+ let body: unknown = bodyText
148
+ if (contentType.includes("application/json") && text.length > 0) {
149
+ try {
150
+ body = JSON.parse(text)
151
+ } catch {
152
+ // Content-Type lied, or the body is partial — keep the text form.
153
+ }
154
+ }
155
+ return {
156
+ ...base,
157
+ ok: res.ok,
158
+ status: res.status,
159
+ statusText: res.statusText,
160
+ headers: headersToObject(res.headers),
161
+ body,
162
+ bodyText,
163
+ ...(truncated ? { truncated } : {}),
164
+ durationMs: now() - start,
165
+ }
166
+ } catch (err) {
167
+ return { ...base, ok: false, durationMs: now() - start, error: toError(err) }
168
+ }
169
+ }
170
+
171
+ function toError(err: unknown): { name: string; message: string; stack?: string } {
172
+ if (err instanceof Error) {
173
+ return { name: err.name, message: err.message, ...(err.stack ? { stack: err.stack } : {}) }
174
+ }
175
+ return { name: "Error", message: String(err) }
176
+ }
177
+
178
+ /**
179
+ * Run a batch of requests through the app, in order, and return a result per request. Never throws:
180
+ * an app crash on any request is captured as that result's `error` and the run continues.
181
+ */
182
+ export function runApp(
183
+ app: AppLike,
184
+ requests: readonly RequestSpec[],
185
+ options: RunOptions = {},
186
+ ): Promise<RunResult[]> {
187
+ return requests.reduce<Promise<RunResult[]>>(async (acc, spec) => {
188
+ const results = await acc
189
+ results.push(await runRequest(app, spec, options))
190
+ return results
191
+ }, Promise.resolve([]))
192
+ }