@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.
- package/README.md +37 -7
- package/lib/cli/agents/index.mjs +64 -0
- package/lib/cli/agents/investigate.mjs +75 -0
- package/lib/cli/agents/investigation-context.mjs +102 -0
- package/lib/cli/agents/investigation-context.test.mjs +144 -0
- package/lib/cli/agents/prompt-builder.mjs +25 -0
- package/lib/cli/agents/providers/claude.mjs +74 -0
- package/lib/cli/agents/providers/claude.test.mjs +95 -0
- package/lib/cli/agents/providers/codex.mjs +83 -0
- package/lib/cli/agents/providers/codex.test.mjs +93 -0
- package/lib/cli/agents/providers/shared.mjs +134 -0
- package/lib/cli/command-helpers.mjs +53 -25
- package/lib/cli/command-helpers.test.mjs +122 -0
- package/lib/cli/commands/investigate.mjs +87 -0
- package/lib/cli/commands/investigate.test.mjs +83 -0
- package/lib/cli/entrypoint.mjs +3 -0
- package/lib/cli/presentation/colors.mjs +12 -0
- package/lib/cli/presentation/events-reporter.mjs +135 -0
- package/lib/cli/presentation/events-reporter.test.mjs +73 -0
- package/lib/cli/presentation/summary-box.mjs +11 -11
- package/lib/cli/presentation/summary-box.test.mjs +17 -0
- package/lib/cli/presentation/tree-reporter.mjs +159 -0
- package/lib/cli/presentation/tree-reporter.test.mjs +166 -0
- package/lib/cli/tui/run-app.mjs +1 -0
- package/lib/cli/tui/run-session-app.mjs +370 -0
- package/lib/cli/tui/run-session-app.test.mjs +50 -0
- package/lib/cli/tui/run-session-state.mjs +481 -0
- package/lib/cli/tui/run-tree-state.mjs +1 -0
- package/lib/cli/tui/run-tree-state.test.mjs +324 -0
- package/lib/config-api/auth-fixtures.mjs +15 -10
- package/lib/config-api/index.test.mjs +54 -0
- package/lib/discovery/index.mjs +1 -1
- package/lib/index.d.ts +5 -1
- package/lib/runner/orchestrator.mjs +1 -0
- package/lib/runtime/index.d.ts +138 -5
- package/lib/runtime/index.mjs +68 -2
- package/lib/runtime-src/k6/http-assertions.js +31 -1
- package/lib/runtime-src/k6/http-checks.js +120 -0
- package/lib/runtime-src/k6/http-checks.test.mjs +120 -0
- package/lib/runtime-src/k6/http-suite-runtime.js +5 -1
- package/lib/runtime-src/k6/http.js +213 -23
- package/lib/runtime-src/k6/http.test.mjs +205 -0
- package/lib/runtime-src/shared/error-body.mjs +42 -0
- package/lib/runtime-src/shared/http-parsing.mjs +68 -0
- package/lib/runtime-src/shared/http-parsing.test.mjs +69 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- 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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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",
|
package/lib/discovery/index.mjs
CHANGED
|
@@ -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:
|
|
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() });
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -141,9 +141,36 @@ export interface HttpClientConfig<TSetup = unknown> {
|
|
|
141
141
|
sessionBundle?: AuthSessionBundle<TSetup> | null;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export interface
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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>;
|