@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
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
+ auth,
3
4
  app,
4
5
  database,
5
6
  defineConfig,
6
7
  defineFile,
7
- profiles,
8
8
  toolchain,
9
9
  } from "./index.mjs";
10
10
  import { clearRuntimeContext, registerRuntimeContext } from "./runtime.mjs";
@@ -16,27 +16,7 @@ describe("config api", () => {
16
16
  });
17
17
  });
18
18
 
19
- it("builds raw HTTP profiles with deterministic forwarded headers", () => {
20
- const profile = profiles.raw({
21
- headers: {
22
- contentTypeJson: true,
23
- forwardedFor: "deterministic",
24
- values: { "X-Testkit-Mode": "raw" },
25
- },
26
- });
27
-
28
- const headers = profile.headers?.(null, { env: { BASE: "http://api.test" } });
29
- const rawHeaders = profile.rawHeaders?.(null, { env: { BASE: "http://api.test" } });
30
-
31
- expect(headers).toMatchObject({
32
- "Content-Type": "application/json",
33
- "X-Testkit-Mode": "raw",
34
- });
35
- expect(headers["X-Forwarded-For"]).toMatch(/^10\.\d+\.\d+\.\d+$/);
36
- expect(rawHeaders).toEqual(headers);
37
- });
38
-
39
- it("builds session profiles that perform bootstrap/login and derive auth/session headers", () => {
19
+ it("builds generated auth fixture profiles with actor-aware sessions and headers", () => {
40
20
  const requests = [];
41
21
  registerRuntimeContext({
42
22
  env: {
@@ -45,100 +25,13 @@ describe("config api", () => {
45
25
  },
46
26
  http: {
47
27
  post(url, body, params) {
48
- requests.push({ url, body, params });
49
- if (url.endsWith("/signup")) {
50
- return {
51
- status: 201,
52
- body: JSON.stringify({ data: { organizations: [{ id: "org-signup" }] } }),
53
- headers: {
54
- "set-cookie": ["fixture_session=signup-token; Path=/", "fixture_refresh=signup-refresh; Path=/"],
55
- },
56
- };
57
- }
58
- return {
59
- status: 200,
60
- body: JSON.stringify({ data: { organizations: [{ id: "org-123" }] } }),
61
- headers: {
62
- "set-cookie": ["fixture_session=jwt-123; Path=/", "fixture_refresh=refresh-123; Path=/"],
63
- },
64
- };
65
- },
66
- },
67
- });
68
-
69
- const profile = profiles.session({
70
- actor: {
71
- bootstrap: {
72
- path: "/signup",
73
- expect: [201, 409],
74
- body: ({ actor }) => ({ email: `${actor}@example.com` }),
75
- },
76
- login: {
77
- path: "/login",
78
- expect: 200,
79
- body: ({ actor }) => ({ email: `${actor}@example.com` }),
80
- },
81
- session: {
82
- cookies: {
83
- jwt: "fixture_session",
84
- refreshToken: "fixture_refresh",
85
- },
86
- fields: {
87
- organizationId: "data.organizations[0].id",
88
- },
89
- auth: {
90
- source: { key: "jwt" },
91
- },
92
- },
93
- },
94
- headers: {
95
- contentTypeJson: true,
96
- forwardedFor: "deterministic",
97
- fromSession: [{ header: "X-Organization-Id", field: "organizationId" }],
98
- values: ({ actor }) => ({ "X-Testkit-Actor": actor || "primary" }),
99
- },
100
- });
101
-
102
- const session = profile.auth.setup({
103
- env: { BASE: "http://api.test", routeParams: { "x-route": "route-a" } },
104
- });
105
- const authHeaders = profile.auth.headers(session, { env: { BASE: "http://api.test" } });
106
- const requestHeaders = profile.headers(session, { env: { BASE: "http://api.test" } });
107
-
108
- expect(requests).toHaveLength(2);
109
- expect(requests[0].params.headers["x-route"]).toBe("route-a");
110
- expect(session).toEqual({
111
- jwt: "jwt-123",
112
- refreshToken: "refresh-123",
113
- organizationId: "org-123",
114
- });
115
- expect(authHeaders).toEqual({
116
- Authorization: "Bearer jwt-123",
117
- });
118
- expect(requestHeaders).toMatchObject({
119
- "Content-Type": "application/json",
120
- "X-Organization-Id": "org-123",
121
- "X-Testkit-Actor": "primary",
122
- });
123
- expect(requestHeaders["X-Forwarded-For"]).toMatch(/^10\.\d+\.\d+\.\d+$/);
124
- clearRuntimeContext();
125
- });
126
-
127
- it("builds local-json profile presets that derive session, multi-actor, and raw variants", () => {
128
- const requests = [];
129
- registerRuntimeContext({
130
- env: {
131
- BASE: "http://api.test",
132
- },
133
- http: {
134
- post(url, body) {
135
28
  const payload = JSON.parse(body);
136
- requests.push({ url, payload });
137
- const actor = payload.email.startsWith("primary")
138
- ? "primary"
139
- : payload.email.startsWith("user-a")
140
- ? "userA"
141
- : "userB";
29
+ requests.push({ url, payload, params });
30
+ const actor = payload.email.includes(".user-a@")
31
+ ? "userA"
32
+ : payload.email.includes(".user-b@")
33
+ ? "userB"
34
+ : "primary";
142
35
  return {
143
36
  status: url.endsWith("/signup") ? 201 : 200,
144
37
  body: JSON.stringify({
@@ -157,68 +50,71 @@ describe("config api", () => {
157
50
  },
158
51
  });
159
52
 
160
- const auth = profiles.localJson({
161
- password: "TestkitPass2026",
162
- identities: {
163
- primary: {
164
- email: "primary@example.com",
165
- name: "Primary User",
166
- organizationName: "Primary Org",
167
- },
168
- userA: {
169
- email: "user-a@example.com",
170
- name: "User A",
171
- organizationName: "Org A",
172
- },
173
- userB: {
174
- email: "user-b@example.com",
175
- name: "User B",
176
- organizationName: "Org B",
177
- },
178
- },
179
- session: {
53
+ const fixture = auth.fixture({
54
+ contract: auth.contracts.jsonSession({
180
55
  authCookie: "fixture_session",
181
56
  refreshCookie: "fixture_refresh",
182
57
  organizationIdPath: "data.organizations[0].id",
183
- },
184
- headers: {
185
- contentTypeJson: true,
186
58
  forwardedFor: "deterministic",
187
- organization: "X-Organization-Id",
188
- },
59
+ organizationHeader: "X-Organization-Id",
60
+ }),
61
+ topology: auth.topologies.crossOrg({
62
+ namespace: "fixture",
63
+ actors: {
64
+ primary: { org: "primary" },
65
+ userA: { org: "primary" },
66
+ userB: { org: "secondary" },
67
+ },
68
+ }),
189
69
  });
190
70
 
191
- const defaultProfile = auth.session();
192
- const defaultSetup = defaultProfile.auth.setup({ env: { BASE: "http://api.test" } });
193
- expect(defaultSetup).toEqual({
194
- jwt: "jwt-primary",
195
- refreshToken: "refresh-primary",
196
- organizationId: "org-primary",
71
+ const builtProfiles = fixture.profiles({
72
+ default: auth.profile.actor("primary", {
73
+ headers: {
74
+ values: ({ actor }) => ({ "X-Testkit-Actor": actor || "primary" }),
75
+ },
76
+ }),
77
+ dual: auth.profile.actors({
78
+ primaryActor: "userA",
79
+ actors: ["userA", "userB"],
80
+ }),
81
+ raw: auth.profile.raw({
82
+ headers: {
83
+ values: {
84
+ "X-Testkit-Mode": "raw",
85
+ },
86
+ },
87
+ }),
197
88
  });
198
- expect(defaultProfile.auth.headers(defaultSetup)).toEqual({
89
+
90
+ expect(fixture.topology.actors.primary.email).toBe("testkit+fixture.primary@example.test");
91
+ expect(fixture.topology.actors.userA.name).toBe("Testkit Fixture User A");
92
+ expect(fixture.topology.actors.userB.organizationName).toBe("Testkit Fixture Secondary Org");
93
+
94
+ const defaultSetup = builtProfiles.default.auth.setup({ env: { BASE: "http://api.test", routeParams: { "x-route": "route-a" } } });
95
+ expect(defaultSetup.actors.primary.organizationId).toBe("org-primary");
96
+ expect(builtProfiles.default.auth.headers(defaultSetup, { env: { BASE: "http://api.test" } })).toEqual({
199
97
  Authorization: "Bearer jwt-primary",
200
98
  });
201
- expect(defaultProfile.headers(defaultSetup, { env: { BASE: "http://api.test" } })).toMatchObject({
99
+ expect(builtProfiles.default.headers(defaultSetup, { env: { BASE: "http://api.test" } })).toMatchObject({
202
100
  "Content-Type": "application/json",
203
101
  "X-Organization-Id": "org-primary",
102
+ "X-Testkit-Actor": "primary",
204
103
  });
205
104
 
206
- const dualProfile = auth.multiActor({
207
- primaryActor: "userA",
208
- actors: ["userA", "userB"],
209
- });
210
- const dualSetup = dualProfile.auth.setup({ env: { BASE: "http://api.test" } });
211
- expect(dualSetup.userA.organizationId).toBe("org-userA");
212
- expect(dualSetup.userB.organizationId).toBe("org-userB");
213
- expect(dualProfile.auth.headers(dualSetup)).toEqual({
105
+ const dualSetup = builtProfiles.dual.auth.setup({ env: { BASE: "http://api.test" } });
106
+ expect(dualSetup.actors.userA.organizationId).toBe("org-userA");
107
+ expect(dualSetup.actors.userB.organizationId).toBe("org-userB");
108
+ expect(builtProfiles.dual.auth.headers(dualSetup)).toEqual({
214
109
  Authorization: "Bearer jwt-userA",
215
110
  });
216
111
 
217
- const rawProfile = auth.raw();
218
- expect(rawProfile.rawHeaders(null, { env: { BASE: "http://api.test" } })).toMatchObject({
112
+ expect(builtProfiles.raw.rawHeaders(null, { env: { BASE: "http://api.test" } })).toMatchObject({
219
113
  "Content-Type": "application/json",
114
+ "X-Testkit-Mode": "raw",
220
115
  });
221
116
 
117
+ expect(requests[0].params.headers["x-route"]).toBe("route-a");
222
118
  expect(requests.map((entry) => entry.url)).toEqual([
223
119
  "http://api.test/api/v1/auth/signup",
224
120
  "http://api.test/api/v1/auth/login",
@@ -230,71 +126,68 @@ describe("config api", () => {
230
126
  clearRuntimeContext();
231
127
  });
232
128
 
233
- it("builds multi-actor profiles and derives headers from the primary actor by default", () => {
234
- const responses = {
235
- alpha: {
236
- status: 200,
237
- body: JSON.stringify({ data: { organizations: [{ id: "org-alpha" }] } }),
238
- headers: { "set-cookie": "fixture_session=token-alpha; Path=/" },
239
- },
240
- beta: {
241
- status: 200,
242
- body: JSON.stringify({ data: { organizations: [{ id: "org-beta" }] } }),
243
- headers: { "set-cookie": "fixture_session=token-beta; Path=/" },
244
- },
245
- };
129
+ it("builds a single-org topology with deterministic defaults", () => {
130
+ const topology = auth.topologies.singleOrg({
131
+ namespace: "sample-app",
132
+ actors: ["primary", "reviewer"],
133
+ });
134
+
135
+ expect(topology.actors.primary.email).toBe("testkit+sample-app.primary@example.test");
136
+ expect(topology.actors.reviewer.name).toBe("Testkit Sample App Reviewer");
137
+ expect(topology.actors.primary.organizationName).toBe("Testkit Sample App Primary Org");
138
+ });
139
+
140
+ it("treats signup as best-effort when login still succeeds", () => {
246
141
  registerRuntimeContext({
247
142
  env: {
248
143
  BASE: "http://api.test",
144
+ routeParams: {},
249
145
  },
250
146
  http: {
251
- post(_url, body) {
147
+ post(url, body) {
252
148
  const payload = JSON.parse(body);
253
- return payload.email.startsWith("alpha") ? responses.alpha : responses.beta;
254
- },
255
- },
256
- });
149
+ if (url.endsWith("/signup")) {
150
+ return {
151
+ status: 500,
152
+ body: JSON.stringify({ error: "Failed to create account" }),
153
+ headers: {},
154
+ };
155
+ }
257
156
 
258
- const profile = profiles.multiActor({
259
- primaryActor: "userA",
260
- actors: {
261
- userA: {
262
- login: {
263
- path: "/login",
264
- body: () => ({ email: "alpha@example.com" }),
265
- },
266
- session: {
267
- cookies: { jwt: "fixture_session" },
268
- fields: { organizationId: "data.organizations[0].id" },
269
- auth: { source: { key: "jwt" } },
270
- },
271
- },
272
- userB: {
273
- login: {
274
- path: "/login",
275
- body: () => ({ email: "beta@example.com" }),
276
- },
277
- session: {
278
- cookies: { jwt: "fixture_session" },
279
- fields: { organizationId: "data.organizations[0].id" },
280
- auth: { source: { key: "jwt" } },
281
- },
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
+ };
282
168
  },
283
169
  },
284
- headers: {
285
- fromSession: [{ header: "X-Organization-Id", field: "organizationId" }],
286
- },
287
170
  });
288
171
 
289
- const setupData = profile.auth.setup({ env: { BASE: "http://api.test" } });
290
- expect(setupData.userA.organizationId).toBe("org-alpha");
291
- expect(setupData.userB.organizationId).toBe("org-beta");
292
- expect(profile.auth.headers(setupData)).toEqual({
293
- Authorization: "Bearer token-alpha",
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
+ }),
294
181
  });
295
- expect(profile.headers(setupData, { env: { BASE: "http://api.test" } })).toMatchObject({
296
- "X-Organization-Id": "org-alpha",
182
+
183
+ const profiles = fixture.profiles({
184
+ default: auth.profile.actor("primary"),
297
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
+
298
191
  clearRuntimeContext();
299
192
  });
300
193
 
@@ -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
@@ -1,6 +1,8 @@
1
1
  import type {
2
+ ActorRequestClient,
2
3
  HttpClient,
3
4
  HttpClientConfig,
5
+ RawRequestClient,
4
6
  RuntimeDb,
5
7
  RuntimeDalContext,
6
8
  RuntimeEnv,
@@ -16,6 +18,7 @@ export interface TestkitSuite<TSetup = unknown> {
16
18
  }
17
19
 
18
20
  export interface HeaderBuilderContext {
21
+ actor?: string | null;
19
22
  env: RuntimeEnv;
20
23
  }
21
24
 
@@ -29,13 +32,34 @@ export interface AuthAdapter<TSetup = unknown> {
29
32
  headers?: HeaderBuilder<TSetup>;
30
33
  }
31
34
 
32
- export interface HttpSuiteContext<TSetup = unknown> {
35
+ export interface SuiteActor<TSession = Record<string, unknown>> {
36
+ email: string | null;
37
+ headers: RuntimeHeaders;
38
+ index: number;
39
+ key: string;
40
+ name: string | null;
41
+ organizationKey: string | null;
42
+ organizationName: string | null;
43
+ rawHeaders: RuntimeHeaders;
44
+ rawReq: RawRequestClient;
45
+ req: ActorRequestClient;
46
+ session: TSession | null;
47
+ }
48
+
49
+ export interface SuiteActors<TSession = Record<string, unknown>> {
50
+ readonly names: string[];
51
+ readonly primary: SuiteActor<TSession> | null;
52
+ get(name: string): SuiteActor<TSession>;
53
+ has(name: string): boolean;
54
+ list(): SuiteActor<TSession>[];
55
+ }
56
+
57
+ export interface HttpSuiteContext<TSession = Record<string, unknown>> {
58
+ actor: SuiteActor<TSession> | null;
59
+ actors: SuiteActors<TSession>;
33
60
  env: RuntimeEnv;
34
- req: HttpClient<TSetup>["request"];
35
- rawReq: HttpClient["raw"];
36
- getWithHeaders: HttpClient<TSetup>["getWithHeaders"];
37
- setupData: TSetup | null;
38
- session: TSetup | null;
61
+ req: HttpClient<TSession>;
62
+ rawReq: RawRequestClient;
39
63
  }
40
64
 
41
65
  export interface ScenarioStepResult {
@@ -83,14 +107,14 @@ export interface ScenarioRuntime {
83
107
  };
84
108
  }
85
109
 
86
- export interface ScenarioSuiteContext<TSetup = unknown> extends HttpSuiteContext<TSetup> {
110
+ export interface ScenarioSuiteContext<TSession = Record<string, unknown>> extends HttpSuiteContext<TSession> {
87
111
  scenario: ScenarioRuntime;
88
112
  }
89
113
 
90
- export interface HttpSuiteConfig<TSetup = unknown> {
91
- auth?: AuthAdapter<TSetup> | null;
114
+ export interface HttpSuiteConfig<TSession = unknown> {
115
+ auth?: AuthAdapter<TSession> | null;
92
116
  env?: RuntimeEnv;
93
- headers?: HeaderBuilder<TSetup>;
117
+ headers?: HeaderBuilder<TSession>;
94
118
  profile?: string;
95
119
  rawHeaders?: HeaderBuilder<never>;
96
120
  options?: RuntimeOptions;
@@ -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() });