@elench/testkit 0.1.81 → 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 (55) hide show
  1. package/README.md +64 -27
  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 +767 -0
  31. package/lib/config-api/index.d.ts +92 -108
  32. package/lib/config-api/index.mjs +22 -12
  33. package/lib/config-api/index.test.mjs +103 -210
  34. package/lib/discovery/index.mjs +1 -1
  35. package/lib/index.d.ts +34 -10
  36. package/lib/runner/orchestrator.mjs +1 -0
  37. package/lib/runtime/index.d.ts +177 -27
  38. package/lib/runtime/index.mjs +68 -3
  39. package/lib/runtime-src/k6/http-assertions.js +31 -1
  40. package/lib/runtime-src/k6/http-checks.js +120 -0
  41. package/lib/runtime-src/k6/http-checks.test.mjs +120 -0
  42. package/lib/runtime-src/k6/http-suite-runtime.js +151 -0
  43. package/lib/runtime-src/k6/http.js +285 -56
  44. package/lib/runtime-src/k6/http.test.mjs +205 -0
  45. package/lib/runtime-src/k6/scenario-suite.js +13 -110
  46. package/lib/runtime-src/k6/suite.js +13 -107
  47. package/lib/runtime-src/shared/error-body.mjs +42 -0
  48. package/lib/runtime-src/shared/http-parsing.mjs +68 -0
  49. package/lib/runtime-src/shared/http-parsing.test.mjs +69 -0
  50. package/node_modules/@elench/next-analysis/package.json +1 -1
  51. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  52. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  53. package/node_modules/@elench/ts-analysis/package.json +1 -1
  54. package/package.json +5 -5
  55. package/lib/config-api/profiles.mjs +0 -640
@@ -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
+ });