@elench/testkit 0.1.82 → 0.1.83

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.
Files changed (50) hide show
  1. package/README.md +37 -7
  2. package/lib/cli/agents/index.mjs +64 -0
  3. package/lib/cli/agents/investigate.mjs +75 -0
  4. package/lib/cli/agents/investigation-context.mjs +102 -0
  5. package/lib/cli/agents/investigation-context.test.mjs +144 -0
  6. package/lib/cli/agents/prompt-builder.mjs +25 -0
  7. package/lib/cli/agents/providers/claude.mjs +74 -0
  8. package/lib/cli/agents/providers/claude.test.mjs +95 -0
  9. package/lib/cli/agents/providers/codex.mjs +83 -0
  10. package/lib/cli/agents/providers/codex.test.mjs +93 -0
  11. package/lib/cli/agents/providers/shared.mjs +134 -0
  12. package/lib/cli/command-helpers.mjs +53 -25
  13. package/lib/cli/command-helpers.test.mjs +122 -0
  14. package/lib/cli/commands/investigate.mjs +87 -0
  15. package/lib/cli/commands/investigate.test.mjs +83 -0
  16. package/lib/cli/entrypoint.mjs +3 -0
  17. package/lib/cli/presentation/colors.mjs +12 -0
  18. package/lib/cli/presentation/events-reporter.mjs +135 -0
  19. package/lib/cli/presentation/events-reporter.test.mjs +73 -0
  20. package/lib/cli/presentation/summary-box.mjs +11 -11
  21. package/lib/cli/presentation/summary-box.test.mjs +17 -0
  22. package/lib/cli/presentation/tree-reporter.mjs +159 -0
  23. package/lib/cli/presentation/tree-reporter.test.mjs +166 -0
  24. package/lib/cli/tui/run-app.mjs +1 -0
  25. package/lib/cli/tui/run-session-app.mjs +370 -0
  26. package/lib/cli/tui/run-session-app.test.mjs +50 -0
  27. package/lib/cli/tui/run-session-state.mjs +481 -0
  28. package/lib/cli/tui/run-tree-state.mjs +1 -0
  29. package/lib/cli/tui/run-tree-state.test.mjs +324 -0
  30. package/lib/config-api/auth-fixtures.mjs +15 -10
  31. package/lib/config-api/index.test.mjs +54 -0
  32. package/lib/discovery/index.mjs +1 -1
  33. package/lib/index.d.ts +5 -1
  34. package/lib/runner/orchestrator.mjs +1 -0
  35. package/lib/runtime/index.d.ts +138 -5
  36. package/lib/runtime/index.mjs +68 -2
  37. package/lib/runtime-src/k6/http-assertions.js +31 -1
  38. package/lib/runtime-src/k6/http-checks.js +120 -0
  39. package/lib/runtime-src/k6/http-checks.test.mjs +120 -0
  40. package/lib/runtime-src/k6/http-suite-runtime.js +5 -1
  41. package/lib/runtime-src/k6/http.js +213 -23
  42. package/lib/runtime-src/k6/http.test.mjs +205 -0
  43. package/lib/runtime-src/shared/error-body.mjs +42 -0
  44. package/lib/runtime-src/shared/http-parsing.mjs +68 -0
  45. package/lib/runtime-src/shared/http-parsing.test.mjs +69 -0
  46. package/node_modules/@elench/next-analysis/package.json +1 -1
  47. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  48. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  49. package/node_modules/@elench/ts-analysis/package.json +1 -1
  50. package/package.json +5 -5
@@ -0,0 +1,324 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createRunTreeState } from "./run-tree-state.mjs";
3
+
4
+ function makePlans(suites = []) {
5
+ return [
6
+ {
7
+ config: { name: "api" },
8
+ skipped: false,
9
+ suites: suites.length > 0 ? suites : [
10
+ {
11
+ name: "users",
12
+ type: "integration",
13
+ displayType: "int",
14
+ framework: "k6",
15
+ files: ["tests/api/users.int.testkit.ts", "tests/api/users-create.int.testkit.ts"],
16
+ },
17
+ {
18
+ name: "auth",
19
+ type: "integration",
20
+ displayType: "int",
21
+ framework: "k6",
22
+ files: ["tests/api/auth.int.testkit.ts"],
23
+ },
24
+ ],
25
+ },
26
+ ];
27
+ }
28
+
29
+ describe("run-tree-state", () => {
30
+ it("builds tree hierarchy from service plans", () => {
31
+ const state = createRunTreeState();
32
+ state.initFromPlans(makePlans());
33
+ const snap = state.getSnapshot();
34
+
35
+ expect(snap.services).toHaveLength(1);
36
+ expect(snap.services[0].name).toBe("api");
37
+ expect(snap.services[0].skipped).toBe(false);
38
+ expect(snap.services[0].types).toHaveLength(1);
39
+ expect(snap.services[0].types[0].type).toBe("int");
40
+ expect(snap.services[0].types[0].suites).toHaveLength(2);
41
+ expect(snap.services[0].types[0].suites[0].groupLabel).toBe("Users");
42
+ expect(snap.services[0].types[0].suites[0].visibleFiles).toHaveLength(2);
43
+ expect(snap.services[0].types[0].suites[1].groupLabel).toBe("Auth");
44
+ });
45
+
46
+ it("marks file running", () => {
47
+ const state = createRunTreeState();
48
+ state.initFromPlans(makePlans());
49
+ state.markFileRunning("api", "int:users", "tests/api/users.int.testkit.ts");
50
+ const snap = state.getSnapshot();
51
+ const file = snap.services[0].types[0].suites[0].visibleFiles.find(
52
+ (f) => f.path === "tests/api/users.int.testkit.ts"
53
+ );
54
+ expect(file.status).toBe("running");
55
+ });
56
+
57
+ it("collapses suite when all files pass", () => {
58
+ const state = createRunTreeState();
59
+ state.initFromPlans(makePlans());
60
+ state.setTotalFileCount(3);
61
+
62
+ state.markFileFinished(
63
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users.int.testkit.ts" },
64
+ { failed: false, durationMs: 1200 }
65
+ );
66
+ state.markFileFinished(
67
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users-create.int.testkit.ts" },
68
+ { failed: false, durationMs: 800 }
69
+ );
70
+
71
+ const snap = state.getSnapshot();
72
+ const suite = snap.services[0].types[0].suites[0];
73
+ expect(suite.collapsed).toBe(true);
74
+ expect(suite.collapseStatus).toBe("all_passed");
75
+ expect(suite.visibleFiles).toHaveLength(0);
76
+ expect(suite.totalDurationMs).toBe(2000);
77
+ expect(snap.completedCount).toBe(2);
78
+ });
79
+
80
+ it("collapses suite when all files skipped", () => {
81
+ const state = createRunTreeState();
82
+ state.initFromPlans(makePlans());
83
+
84
+ state.markFileFinished(
85
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users.int.testkit.ts" },
86
+ { status: "skipped", reason: "ci skip", durationMs: 0 }
87
+ );
88
+ state.markFileFinished(
89
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users-create.int.testkit.ts" },
90
+ { status: "skipped", reason: "ci skip", durationMs: 0 }
91
+ );
92
+
93
+ const snap = state.getSnapshot();
94
+ const suite = snap.services[0].types[0].suites[0];
95
+ expect(suite.collapsed).toBe(true);
96
+ expect(suite.collapseStatus).toBe("all_skipped");
97
+ });
98
+
99
+ it("keeps suite expanded when a file fails", () => {
100
+ const state = createRunTreeState();
101
+ state.initFromPlans(makePlans());
102
+
103
+ state.markFileFinished(
104
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users.int.testkit.ts" },
105
+ { failed: false, durationMs: 1200 }
106
+ );
107
+ state.markFileFinished(
108
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users-create.int.testkit.ts" },
109
+ { failed: true, error: "assertion failed", durationMs: 500, failureDetails: [] }
110
+ );
111
+
112
+ const snap = state.getSnapshot();
113
+ const suite = snap.services[0].types[0].suites[0];
114
+ expect(suite.collapsed).toBe(false);
115
+ // Only failed files visible when no running/pending
116
+ expect(suite.visibleFiles).toHaveLength(1);
117
+ expect(suite.visibleFiles[0].status).toBe("failed");
118
+ expect(suite.passedCount).toBe(1);
119
+ });
120
+
121
+ it("keeps suite expanded while files are running", () => {
122
+ const state = createRunTreeState();
123
+ state.initFromPlans(makePlans());
124
+
125
+ state.markFileRunning("api", "int:users", "tests/api/users.int.testkit.ts");
126
+ state.markFileFinished(
127
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users-create.int.testkit.ts" },
128
+ { failed: false, durationMs: 800 }
129
+ );
130
+
131
+ const snap = state.getSnapshot();
132
+ const suite = snap.services[0].types[0].suites[0];
133
+ expect(suite.collapsed).toBe(false);
134
+ expect(suite.visibleFiles).toHaveLength(2);
135
+ });
136
+
137
+ it("type collapses when all suites are collapsed", () => {
138
+ const state = createRunTreeState();
139
+ state.initFromPlans(makePlans());
140
+
141
+ // Pass all files in both suites
142
+ state.markFileFinished(
143
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users.int.testkit.ts" },
144
+ { failed: false, durationMs: 1000 }
145
+ );
146
+ state.markFileFinished(
147
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users-create.int.testkit.ts" },
148
+ { failed: false, durationMs: 500 }
149
+ );
150
+ state.markFileFinished(
151
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "auth", file: "tests/api/auth.int.testkit.ts" },
152
+ { failed: false, durationMs: 2000 }
153
+ );
154
+
155
+ const snap = state.getSnapshot();
156
+ const type = snap.services[0].types[0];
157
+ expect(type.collapsed).toBe(true);
158
+ });
159
+
160
+ it("tracks completed and total counts", () => {
161
+ const state = createRunTreeState();
162
+ state.initFromPlans(makePlans());
163
+ state.setTotalFileCount(3);
164
+
165
+ expect(state.getSnapshot().totalCount).toBe(3);
166
+ expect(state.getSnapshot().completedCount).toBe(0);
167
+
168
+ state.markFileFinished(
169
+ { serviceName: "api", type: "integration", displayType: "int", framework: "k6", suiteName: "users", file: "tests/api/users.int.testkit.ts" },
170
+ { failed: false, durationMs: 100 }
171
+ );
172
+ expect(state.getSnapshot().completedCount).toBe(1);
173
+ });
174
+
175
+ it("handles skipped services", () => {
176
+ const state = createRunTreeState();
177
+ state.initFromPlans([{ config: { name: "web" }, skipped: true, suites: [] }]);
178
+ const snap = state.getSnapshot();
179
+ expect(snap.services[0].skipped).toBe(true);
180
+ });
181
+
182
+ it("markServiceSkipped updates existing service", () => {
183
+ const state = createRunTreeState();
184
+ state.initFromPlans(makePlans());
185
+ state.markServiceSkipped("api", "no matching types");
186
+ const snap = state.getSnapshot();
187
+ expect(snap.services[0].skipped).toBe(true);
188
+ expect(snap.services[0].skipReason).toBe("no matching types");
189
+ });
190
+
191
+ it("finish populates summaryData", () => {
192
+ const state = createRunTreeState();
193
+ state.initFromPlans(makePlans());
194
+
195
+ const mockResults = [
196
+ {
197
+ name: "api",
198
+ failed: false,
199
+ skipped: false,
200
+ suiteCount: 2,
201
+ completedSuiteCount: 2,
202
+ failedSuiteCount: 0,
203
+ skippedSuiteCount: 0,
204
+ totalFileCount: 3,
205
+ passedFileCount: 3,
206
+ failedFileCount: 0,
207
+ skippedFileCount: 0,
208
+ notRunFileCount: 0,
209
+ suites: [],
210
+ errors: [],
211
+ },
212
+ ];
213
+
214
+ state.finish(mockResults, 5000, null);
215
+ const snap = state.getSnapshot();
216
+ expect(snap.finished).toBe(true);
217
+ expect(snap.summaryData.result).toBe("PASSED");
218
+ expect(snap.summaryData.rows.length).toBeGreaterThanOrEqual(7);
219
+ });
220
+
221
+ it("subscribe fires callback on state mutations", () => {
222
+ const state = createRunTreeState();
223
+ state.initFromPlans(makePlans());
224
+ let callCount = 0;
225
+ state.subscribe(() => { callCount += 1; });
226
+
227
+ state.setTotalFileCount(3);
228
+ state.setPhase("running");
229
+ expect(callCount).toBe(2);
230
+ });
231
+
232
+ it("markPlannedSkip sets file status and increments completed", () => {
233
+ const state = createRunTreeState();
234
+ state.initFromPlans(makePlans());
235
+ state.setTotalFileCount(3);
236
+
237
+ state.markPlannedSkip({ serviceName: "api", file: "tests/api/users.int.testkit.ts", reason: "ci skip" });
238
+ const snap = state.getSnapshot();
239
+ const file = snap.services[0].types[0].suites[0].visibleFiles.find(
240
+ (f) => f.path === "tests/api/users.int.testkit.ts"
241
+ );
242
+ expect(file.status).toBe("skipped");
243
+ expect(snap.completedCount).toBe(1);
244
+ });
245
+
246
+ it("markRuntimeError marks file failed", () => {
247
+ const state = createRunTreeState();
248
+ state.initFromPlans(makePlans());
249
+
250
+ state.markRuntimeError(
251
+ { serviceName: "api", file: "tests/api/users.int.testkit.ts" },
252
+ "process crashed"
253
+ );
254
+ const snap = state.getSnapshot();
255
+ const file = snap.services[0].types[0].suites[0].visibleFiles.find(
256
+ (f) => f.path === "tests/api/users.int.testkit.ts"
257
+ );
258
+ expect(file.status).toBe("failed");
259
+ expect(file.error).toBe("process crashed");
260
+ });
261
+
262
+ it("tracks the selected failure after a file fails", () => {
263
+ const state = createRunTreeState();
264
+ state.initFromPlans(makePlans());
265
+
266
+ state.markFileFinished(
267
+ {
268
+ serviceName: "api",
269
+ type: "integration",
270
+ displayType: "int",
271
+ framework: "k6",
272
+ suiteName: "users",
273
+ file: "tests/api/users-create.int.testkit.ts",
274
+ },
275
+ { failed: true, error: "assertion failed", durationMs: 500, failureDetails: [] }
276
+ );
277
+
278
+ const snap = state.getSnapshot();
279
+ expect(snap.failures).toHaveLength(1);
280
+ expect(snap.selectedFailure.filePath).toBe("tests/api/users-create.int.testkit.ts");
281
+ });
282
+
283
+ it("cycles through failures", () => {
284
+ const state = createRunTreeState();
285
+ state.initFromPlans(makePlans());
286
+
287
+ state.markRuntimeError(
288
+ { serviceName: "api", file: "tests/api/users.int.testkit.ts" },
289
+ "process crashed"
290
+ );
291
+ state.markRuntimeError(
292
+ { serviceName: "api", file: "tests/api/auth.int.testkit.ts" },
293
+ "another crash"
294
+ );
295
+
296
+ const first = state.getSnapshot().selectedFailure.filePath;
297
+ state.selectNextFailure();
298
+ const second = state.getSnapshot().selectedFailure.filePath;
299
+ expect(second).not.toBe(first);
300
+ state.selectPreviousFailure();
301
+ expect(state.getSnapshot().selectedFailure.filePath).toBe(first);
302
+ });
303
+
304
+ it("records investigation transcript state", () => {
305
+ const state = createRunTreeState();
306
+ state.initFromPlans(makePlans());
307
+ state.markRuntimeError(
308
+ { serviceName: "api", file: "tests/api/users.int.testkit.ts" },
309
+ "process crashed"
310
+ );
311
+
312
+ state.beginInvestigation({ provider: "codex", userMessage: "Investigate this failure" });
313
+ state.appendAgentEvent({ type: "start" });
314
+ state.appendAgentEvent({ type: "status", message: "Inspecting repository" });
315
+ state.appendAgentEvent({ type: "delta", text: "Likely root cause." });
316
+ state.completeAgentSession({ finalText: "Likely root cause.", exitCode: 0 });
317
+
318
+ const snap = state.getSnapshot();
319
+ expect(snap.mode).toBe("investigating");
320
+ expect(snap.agentSession.status).toBe("complete");
321
+ expect(snap.agentSession.entries.some((entry) => entry.kind === "assistant")).toBe(true);
322
+ expect(snap.agentSession.finalText).toContain("Likely root cause");
323
+ });
324
+ });
@@ -250,16 +250,21 @@ function resolveActorSession({ actorDefinition, actorIndex, contract, env }) {
250
250
  };
251
251
 
252
252
  if (contract.signup.enabled) {
253
- runProfileRequest({
254
- requestConfig: {
255
- body: () => buildSignupBody(actorDefinition),
256
- expect: contract.signup.expect,
257
- method: "POST",
258
- path: contract.signup.path,
259
- },
260
- context: { ...context, phase: "signup" },
261
- label: `auth.fixture signup for actor "${actorDefinition.actorName}"`,
262
- });
253
+ try {
254
+ runProfileRequest({
255
+ requestConfig: {
256
+ body: () => buildSignupBody(actorDefinition),
257
+ expect: contract.signup.expect,
258
+ method: "POST",
259
+ path: contract.signup.path,
260
+ },
261
+ context: { ...context, phase: "signup" },
262
+ label: `auth.fixture signup for actor "${actorDefinition.actorName}"`,
263
+ });
264
+ } catch {
265
+ // Provisioning is best-effort. Some apps report duplicate-account races as 500s
266
+ // instead of a clean 409, and a successful login is the authoritative signal.
267
+ }
263
268
  }
264
269
 
265
270
  const response = runProfileRequest({
@@ -137,6 +137,60 @@ describe("config api", () => {
137
137
  expect(topology.actors.primary.organizationName).toBe("Testkit Sample App Primary Org");
138
138
  });
139
139
 
140
+ it("treats signup as best-effort when login still succeeds", () => {
141
+ registerRuntimeContext({
142
+ env: {
143
+ BASE: "http://api.test",
144
+ routeParams: {},
145
+ },
146
+ http: {
147
+ post(url, body) {
148
+ const payload = JSON.parse(body);
149
+ if (url.endsWith("/signup")) {
150
+ return {
151
+ status: 500,
152
+ body: JSON.stringify({ error: "Failed to create account" }),
153
+ headers: {},
154
+ };
155
+ }
156
+
157
+ return {
158
+ status: 200,
159
+ body: JSON.stringify({
160
+ data: {
161
+ organizations: [{ id: "org-primary" }],
162
+ },
163
+ }),
164
+ headers: {
165
+ "set-cookie": ["fixture_session=jwt-primary; Path=/"],
166
+ },
167
+ };
168
+ },
169
+ },
170
+ });
171
+
172
+ const fixture = auth.fixture({
173
+ contract: auth.contracts.jsonSession({
174
+ authCookie: "fixture_session",
175
+ organizationIdPath: "data.organizations[0].id",
176
+ }),
177
+ topology: auth.topologies.singleOrg({
178
+ namespace: "signup-race",
179
+ actors: ["primary"],
180
+ }),
181
+ });
182
+
183
+ const profiles = fixture.profiles({
184
+ default: auth.profile.actor("primary"),
185
+ });
186
+
187
+ const setup = profiles.default.auth.setup({ env: { BASE: "http://api.test", routeParams: {} } });
188
+ expect(setup.actors.primary.organizationId).toBe("org-primary");
189
+ expect(setup.actors.primary.jwt).toBe("jwt-primary");
190
+
191
+ clearRuntimeContext();
192
+ });
193
+
140
194
  it("defines file-local metadata plainly", () => {
141
195
  expect(defineFile({ skip: "Auth is stubbed", locks: ["background-workers"] })).toEqual({
142
196
  skip: "Auth is stubbed",
@@ -494,7 +494,7 @@ function normalizePath(filePath) {
494
494
  return String(filePath).split(path.sep).join("/").replace(/^\.\/+/, "");
495
495
  }
496
496
 
497
- function fileDisplayName(filePath) {
497
+ export function fileDisplayName(filePath) {
498
498
  const base = path.posix
499
499
  .basename(filePath)
500
500
  .replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.pw)\.testkit\.ts$/, "");
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  ActorRequestClient,
3
3
  HttpClient,
4
4
  HttpClientConfig,
5
+ RawRequestClient,
5
6
  RuntimeDb,
6
7
  RuntimeDalContext,
7
8
  RuntimeEnv,
@@ -33,11 +34,14 @@ export interface AuthAdapter<TSetup = unknown> {
33
34
 
34
35
  export interface SuiteActor<TSession = Record<string, unknown>> {
35
36
  email: string | null;
37
+ headers: RuntimeHeaders;
36
38
  index: number;
37
39
  key: string;
38
40
  name: string | null;
39
41
  organizationKey: string | null;
40
42
  organizationName: string | null;
43
+ rawHeaders: RuntimeHeaders;
44
+ rawReq: RawRequestClient;
41
45
  req: ActorRequestClient;
42
46
  session: TSession | null;
43
47
  }
@@ -55,7 +59,7 @@ export interface HttpSuiteContext<TSession = Record<string, unknown>> {
55
59
  actors: SuiteActors<TSession>;
56
60
  env: RuntimeEnv;
57
61
  req: HttpClient<TSession>;
58
- rawReq: HttpClient["raw"];
62
+ rawReq: RawRequestClient;
59
63
  }
60
64
 
61
65
  export interface ScenarioStepResult {
@@ -85,6 +85,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
85
85
  execution,
86
86
  reporter
87
87
  );
88
+ reporter?.setServicePlans?.(servicePlans);
88
89
  const trackers = buildServiceTrackers(servicePlans, startedAt);
89
90
  let writeLiveSnapshot = () => {};
90
91
  const setupRegistry = createSetupOperationRegistry({ logRegistry, onChange: () => writeLiveSnapshot() });
@@ -141,9 +141,36 @@ export interface HttpClientConfig<TSetup = unknown> {
141
141
  sessionBundle?: AuthSessionBundle<TSetup> | null;
142
142
  }
143
143
 
144
- export interface ActorRequestClient {
144
+ export interface MultipartFileInput {
145
+ contentType?: string;
146
+ data: unknown;
147
+ field: string;
148
+ filename?: string;
149
+ }
150
+
151
+ export interface MultipartPayload {
152
+ fields?: Record<string, unknown>;
153
+ files?: MultipartFileInput[];
154
+ }
155
+
156
+ export interface MultipartRequestClient {
157
+ patch(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
158
+ post(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
159
+ put(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
160
+ }
161
+
162
+ export interface RawRequestClient {
163
+ (
164
+ method: RuntimeMethod,
165
+ path: string,
166
+ body?: unknown,
167
+ extraHeaders?: RuntimeHeaders
168
+ ): RuntimeResponse;
169
+ as(actorName: string): RawRequestClient;
145
170
  delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
146
171
  get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
172
+ headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
173
+ multipart: MultipartRequestClient;
147
174
  patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
148
175
  post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
149
176
  put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
@@ -155,24 +182,53 @@ export interface ActorRequestClient {
155
182
  ): RuntimeResponse;
156
183
  }
157
184
 
158
- export interface HttpClient<TSetup = unknown> {
159
- as(actorName: string): ActorRequestClient;
185
+ export interface ActorRequestClient {
186
+ headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
187
+ multipart: MultipartRequestClient;
188
+ raw(
189
+ method: RuntimeMethod,
190
+ path: string,
191
+ body?: unknown,
192
+ extraHeaders?: RuntimeHeaders
193
+ ): RuntimeResponse;
194
+ rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
195
+ rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
196
+ rawHeaders(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
197
+ rawMultipart: MultipartRequestClient;
198
+ rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
199
+ rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
200
+ rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
201
+ rawReq: RawRequestClient;
160
202
  delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
161
203
  get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
162
204
  patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
163
205
  post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
164
206
  put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
165
- raw(
207
+ request(
166
208
  method: RuntimeMethod,
167
209
  path: string,
168
210
  body?: unknown,
169
211
  extraHeaders?: RuntimeHeaders
170
212
  ): RuntimeResponse;
213
+ }
214
+
215
+ export interface HttpClient<TSetup = unknown> {
216
+ as(actorName: string): ActorRequestClient;
217
+ headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
218
+ multipart: MultipartRequestClient;
219
+ delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
220
+ get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
221
+ patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
222
+ post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
223
+ put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
171
224
  rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
172
225
  rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
226
+ rawHeaders(actorName?: string | null, extraHeaders?: RuntimeHeaders): RuntimeHeaders;
227
+ rawMultipart: MultipartRequestClient;
173
228
  rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
174
229
  rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
175
230
  rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
231
+ raw: RawRequestClient;
176
232
  request(
177
233
  method: RuntimeMethod,
178
234
  path: string,
@@ -255,7 +311,7 @@ export declare function makeRawReq(
255
311
  baseUrl: string,
256
312
  routeHeaders?: RuntimeHeaders,
257
313
  getRawHeaders?: (context: HttpHeaderBuildContext<never>) => RuntimeHeaders | void
258
- ): HttpClient["raw"];
314
+ ): RawRequestClient;
259
315
 
260
316
  export declare function expectStatus(
261
317
  response: RuntimeResponse,
@@ -283,6 +339,83 @@ export declare function expectJsonPath(
283
339
  predicate: (value: unknown) => boolean,
284
340
  label?: string | null
285
341
  ): boolean;
342
+ export declare function expectErrorShape(
343
+ response: RuntimeResponse,
344
+ label?: string | null
345
+ ): boolean;
346
+ export declare function expectErrorMessage(
347
+ response: RuntimeResponse,
348
+ label?: string | null
349
+ ): boolean;
350
+ export declare function expectResponse(
351
+ response: RuntimeResponse,
352
+ predicate: (response: RuntimeResponse) => boolean,
353
+ label: string
354
+ ): boolean;
355
+ export declare function expectValue<T>(
356
+ value: T,
357
+ predicate: (value: T) => boolean,
358
+ label: string
359
+ ): boolean;
360
+ export declare function expectCondition(
361
+ predicate: () => boolean,
362
+ label: string
363
+ ): boolean;
364
+
365
+ export declare function runAuthGateChecks(
366
+ rawReq: RawRequestClient,
367
+ scope: string,
368
+ descriptors: {
369
+ delete?: Array<string>;
370
+ get?: Array<string>;
371
+ patch?: Array<string | [string, unknown]>;
372
+ post?: Array<string | [string, unknown]>;
373
+ put?: Array<string | [string, unknown]>;
374
+ validateGetErrorShape?: boolean;
375
+ }
376
+ ): void;
377
+ export declare function runPaginationChecks(
378
+ req: Pick<HttpClient, "get">,
379
+ endpoint: string,
380
+ options?: { auditLogsExtra?: boolean }
381
+ ): void;
382
+
383
+ export interface RuntimeExpectNamespace {
384
+ condition: typeof expectCondition;
385
+ error: {
386
+ message: typeof expectErrorMessage;
387
+ shape: typeof expectErrorShape;
388
+ };
389
+ json: typeof expectJson;
390
+ jsonPath: typeof expectJsonPath;
391
+ notStatus: typeof expectNotStatus;
392
+ response: typeof expectResponse;
393
+ status: typeof expectStatus;
394
+ statusOneOf: typeof expectStatusOneOf;
395
+ value: typeof expectValue;
396
+ }
397
+
398
+ export interface RuntimeParseNamespace {
399
+ cookie(response: Pick<RuntimeResponse, "headers">, cookieName: string): string | null;
400
+ json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
401
+ safeJson: typeof safeJson;
402
+ sse(body: string): Array<{ event: string | null; data: unknown }>;
403
+ sseEvent<T = unknown>(body: string, eventName: string): T | null;
404
+ }
405
+
406
+ export interface RuntimeChecksNamespace {
407
+ authGate: typeof runAuthGateChecks;
408
+ pagination: typeof runPaginationChecks;
409
+ }
410
+
411
+ export interface RuntimeNetworkNamespace {
412
+ deterministicIp(seed: string | number, offset?: number): string;
413
+ }
414
+
415
+ export declare const expect: RuntimeExpectNamespace;
416
+ export declare const parse: RuntimeParseNamespace;
417
+ export declare const checks: RuntimeChecksNamespace;
418
+ export declare const network: RuntimeNetworkNamespace;
286
419
 
287
420
  declare global {
288
421
  const __ENV: Record<string, string | undefined>;