@elench/testkit 0.1.83 → 0.1.85

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 (87) hide show
  1. package/lib/cli/agents/investigation-interpreter.mjs +320 -0
  2. package/lib/cli/agents/investigation-log.mjs +37 -0
  3. package/lib/cli/agents/providers/codex.mjs +1 -1
  4. package/lib/cli/presentation/tree-reporter.mjs +33 -1
  5. package/lib/cli/tui/run-session-app.mjs +73 -11
  6. package/lib/cli/tui/run-session-state.mjs +29 -5
  7. package/node_modules/@elench/next-analysis/package.json +1 -1
  8. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  9. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  10. package/node_modules/@elench/ts-analysis/package.json +1 -1
  11. package/package.json +7 -6
  12. package/lib/app/configs.test.mjs +0 -34
  13. package/lib/app/typecheck.test.mjs +0 -24
  14. package/lib/bundler/index.test.mjs +0 -164
  15. package/lib/cli/agents/investigation-context.test.mjs +0 -144
  16. package/lib/cli/agents/providers/claude.test.mjs +0 -95
  17. package/lib/cli/agents/providers/codex.test.mjs +0 -93
  18. package/lib/cli/args.test.mjs +0 -110
  19. package/lib/cli/command-helpers.test.mjs +0 -122
  20. package/lib/cli/commands/investigate.test.mjs +0 -83
  21. package/lib/cli/presentation/code-frames.test.mjs +0 -71
  22. package/lib/cli/presentation/events-reporter.test.mjs +0 -73
  23. package/lib/cli/presentation/run-reporter.test.mjs +0 -192
  24. package/lib/cli/presentation/summary-box.test.mjs +0 -60
  25. package/lib/cli/presentation/terminal-layout.test.mjs +0 -23
  26. package/lib/cli/presentation/tree-reporter.test.mjs +0 -166
  27. package/lib/cli/tui/run-session-app.test.mjs +0 -50
  28. package/lib/cli/tui/run-tree-state.test.mjs +0 -324
  29. package/lib/config/database.test.mjs +0 -29
  30. package/lib/config/discovery.test.mjs +0 -276
  31. package/lib/config/env.test.mjs +0 -40
  32. package/lib/config/index.test.mjs +0 -44
  33. package/lib/config/paths.test.mjs +0 -27
  34. package/lib/config/runtime.test.mjs +0 -82
  35. package/lib/config/skip-config.test.mjs +0 -63
  36. package/lib/config-api/index.test.mjs +0 -398
  37. package/lib/config-api/next-runtime-tsconfig.test.mjs +0 -58
  38. package/lib/coverage/backend-discovery.test.mjs +0 -61
  39. package/lib/coverage/evidence.test.mjs +0 -87
  40. package/lib/coverage/index.test.mjs +0 -715
  41. package/lib/coverage/routing.test.mjs +0 -36
  42. package/lib/coverage/shared.test.mjs +0 -72
  43. package/lib/database/fingerprint.test.mjs +0 -99
  44. package/lib/database/index.test.mjs +0 -95
  45. package/lib/database/naming.test.mjs +0 -39
  46. package/lib/database/state.test.mjs +0 -66
  47. package/lib/database/template-steps.test.mjs +0 -43
  48. package/lib/discovery/file-metadata.test.mjs +0 -51
  49. package/lib/discovery/index.test.mjs +0 -182
  50. package/lib/discovery/path-policy.test.mjs +0 -65
  51. package/lib/drizzle/index.test.mjs +0 -33
  52. package/lib/env/index.test.mjs +0 -82
  53. package/lib/history/index.test.mjs +0 -115
  54. package/lib/package.test.mjs +0 -59
  55. package/lib/playwright/index.test.mjs +0 -43
  56. package/lib/regressions/github.test.mjs +0 -324
  57. package/lib/regressions/index.test.mjs +0 -187
  58. package/lib/reporters/playwright.test.mjs +0 -167
  59. package/lib/runner/default-runtime-errors.test.mjs +0 -49
  60. package/lib/runner/execution-config.test.mjs +0 -67
  61. package/lib/runner/failure-details.test.mjs +0 -114
  62. package/lib/runner/formatting.test.mjs +0 -205
  63. package/lib/runner/metadata.test.mjs +0 -52
  64. package/lib/runner/planning.test.mjs +0 -371
  65. package/lib/runner/playwright-config.test.mjs +0 -78
  66. package/lib/runner/processes.test.mjs +0 -21
  67. package/lib/runner/regressions.test.mjs +0 -168
  68. package/lib/runner/reporting.test.mjs +0 -310
  69. package/lib/runner/results.test.mjs +0 -376
  70. package/lib/runner/runtime-manager.test.mjs +0 -252
  71. package/lib/runner/runtime-preparation.test.mjs +0 -141
  72. package/lib/runner/selection.test.mjs +0 -24
  73. package/lib/runner/setup-operations.test.mjs +0 -94
  74. package/lib/runner/state.test.mjs +0 -62
  75. package/lib/runner/suite-selection.test.mjs +0 -49
  76. package/lib/runner/template.test.mjs +0 -272
  77. package/lib/runtime-src/k6/http-checks.test.mjs +0 -120
  78. package/lib/runtime-src/k6/http.test.mjs +0 -205
  79. package/lib/runtime-src/shared/http-parsing.test.mjs +0 -69
  80. package/lib/shared/build-config.test.mjs +0 -132
  81. package/lib/shared/configured-steps.test.mjs +0 -102
  82. package/lib/shared/execution-schema.test.mjs +0 -26
  83. package/lib/shared/file-timeout.test.mjs +0 -64
  84. package/lib/shared/test-context.test.mjs +0 -43
  85. package/lib/timing/index.test.mjs +0 -64
  86. package/lib/toolchains/index.test.mjs +0 -168
  87. package/lib/vitest/index.test.mjs +0 -20
@@ -1,272 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import { describe, expect, it } from "vitest";
5
- import {
6
- buildExecutionEnv,
7
- buildPlaywrightEnv,
8
- buildPortMap,
9
- buildTaskExecutionEnv,
10
- finalizeString,
11
- normalizeSocketHost,
12
- numericPortFromUrl,
13
- resolveRuntimeInstanceConfigs,
14
- resolveServiceStateDir,
15
- resolveTemplateString,
16
- rewriteUrlPort,
17
- socketFromUrl,
18
- } from "./template.mjs";
19
-
20
- function makeRuntimeConfig(name, local, extras = {}) {
21
- return {
22
- name,
23
- stateDir: extras.stateDir,
24
- runtimeId: extras.runtimeId,
25
- productDir: extras.productDir || process.cwd(),
26
- testkit: {
27
- local,
28
- runtime: extras.runtime || {
29
- instances: 1,
30
- maxConcurrentTasks: Infinity,
31
- prepare: {
32
- inputs: [],
33
- steps: [],
34
- },
35
- },
36
- envFiles: extras.envFiles || [],
37
- serviceEnv: extras.serviceEnv || {},
38
- databaseFrom: extras.databaseFrom,
39
- database: extras.database,
40
- templateContext: extras.templateContext,
41
- },
42
- };
43
- }
44
-
45
- describe("runner-template", () => {
46
- it("builds port maps and detects collisions", () => {
47
- const configs = [
48
- makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
49
- makeRuntimeConfig("frontend", { port: 3001, baseUrl: "http://127.0.0.1:{port}" }),
50
- ];
51
-
52
- expect([...buildPortMap(configs, "runtime-2").entries()]).toEqual([
53
- ["api", 3100],
54
- ["frontend", 3101],
55
- ]);
56
- expect([...buildPortMap(configs, "runtime-1", { index: 1, stride: 2 }).entries()]).toEqual([
57
- ["api", 3200],
58
- ["frontend", 3201],
59
- ]);
60
-
61
- expect(() =>
62
- buildPortMap(
63
- [
64
- makeRuntimeConfig("api", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
65
- makeRuntimeConfig("other", { port: 3000, baseUrl: "http://127.0.0.1:{port}" }),
66
- ],
67
- "runtime-1",
68
- { index: 0, stride: 2 }
69
- )
70
- ).toThrow("Runtime port collision");
71
- });
72
-
73
- it("resolves template strings and URL rewrites", () => {
74
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-template-"));
75
- const apiStateDir = path.join(tmpDir, "api");
76
- fs.mkdirSync(apiStateDir, { recursive: true });
77
- fs.writeFileSync(
78
- path.join(apiStateDir, "database_url"),
79
- "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
80
- );
81
-
82
- const context = {
83
- runtimeId: "runtime-2",
84
- serviceName: "frontend",
85
- serviceStateDir: "/tmp/state",
86
- portMap: new Map([
87
- ["frontend", 3200],
88
- ["api", 3100],
89
- ]),
90
- baseUrlByService: new Map([["api", "http://127.0.0.1:3100"]]),
91
- readyUrlByService: new Map([["api", "http://127.0.0.1:3100/health"]]),
92
- stateDirByService: new Map([["api", apiStateDir]]),
93
- urlMappings: [["http://api:3000", "http://127.0.0.1:3100"]],
94
- leaseId: "lease-1",
95
- leaseDir: "/tmp/lease-1",
96
- };
97
-
98
- expect(resolveTemplateString("{runtime}:{service}:{lease}", context)).toBe(
99
- "runtime-2:frontend:lease-1"
100
- );
101
- expect(resolveTemplateString("{prepareDir}", { ...context, prepareDir: "/tmp/prepare-1" })).toBe(
102
- "/tmp/prepare-1"
103
- );
104
- expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
105
- expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
106
- "127.0.0.1:55432/runtime_db"
107
- );
108
- expect(finalizeString("API={baseUrl:api} OLD=http://api:3000", context)).toBe(
109
- "API=http://127.0.0.1:3100 OLD=http://127.0.0.1:3100"
110
- );
111
- expect(rewriteUrlPort("http://127.0.0.1:3000/health", 3200)).toBe(
112
- "http://127.0.0.1:3200/health"
113
- );
114
- });
115
-
116
- it("builds runtime configs and execution env", () => {
117
- const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-"));
118
- const api = makeRuntimeConfig(
119
- "api",
120
- {
121
- cwd: ".",
122
- start: "npm run api",
123
- port: 3000,
124
- baseUrl: "http://127.0.0.1:{port}",
125
- readyUrl: "http://127.0.0.1:{port}/health",
126
- env: {
127
- PORT: "{port}",
128
- },
129
- },
130
- {
131
- serviceEnv: {
132
- API_KEY: "secret",
133
- },
134
- }
135
- );
136
- const frontend = makeRuntimeConfig("frontend", {
137
- cwd: "frontend",
138
- start: "npm run web",
139
- port: 3001,
140
- baseUrl: "http://127.0.0.1:{port}",
141
- readyUrl: "http://127.0.0.1:{port}",
142
- env: {
143
- NEXT_PUBLIC_API_URL: "{baseUrl:api}",
144
- API_DB_HOST: "{dbHost:api}",
145
- },
146
- });
147
-
148
- const resolved = resolveRuntimeInstanceConfigs([api, frontend], "runtime-2", runtimeDir, {
149
- graphDirName: "api__frontend",
150
- portNamespaceIndex: 0,
151
- portNamespaceStride: 2,
152
- });
153
- expect(resolved[0].testkit.local.port).toBe(3100);
154
- expect(resolved[0].runtimeLabel).toBe("api__frontend/runtime-2");
155
- expect(resolveServiceStateDir(runtimeDir, api)).toBe(`${runtimeDir}/services/api`);
156
- expect(resolved[0].testkit.prepareDir).toBe(`${runtimeDir}/services/api/prepared`);
157
-
158
- fs.mkdirSync(path.join(runtimeDir, "services", "api"), { recursive: true });
159
- fs.writeFileSync(
160
- path.join(runtimeDir, "services", "api", "database_url"),
161
- "postgres://testkit:testkit@127.0.0.1:55432/runtime_db"
162
- );
163
-
164
- expect(
165
- buildExecutionEnv(
166
- resolved[1],
167
- { DATABASE_URL: "gone" },
168
- {
169
- PATH: "/usr/bin",
170
- }
171
- )
172
- ).toEqual({
173
- PATH: "/usr/bin",
174
- NEXT_PUBLIC_API_URL: "http://127.0.0.1:3100",
175
- API_DB_HOST: "127.0.0.1",
176
- TESTKIT_ACTIVE: "1",
177
- TESTKIT_RUNTIME_ID: "runtime-2",
178
- });
179
-
180
- expect(
181
- buildTaskExecutionEnv(
182
- resolved[1],
183
- {
184
- leaseId: "lease-1",
185
- leaseDir: "/tmp/lease-1",
186
- },
187
- {},
188
- {}
189
- )
190
- ).toMatchObject({
191
- TESTKIT_RUNTIME_ID: "runtime-2",
192
- TESTKIT_LEASE_ID: "lease-1",
193
- TESTKIT_LEASE_DIR: "/tmp/lease-1",
194
- });
195
- expect(finalizeString(".next-testkit/{runtimeId}/dist", resolved[1].testkit.templateContext)).toBe(
196
- ".next-testkit/runtime-2/dist"
197
- );
198
-
199
- expect(
200
- buildPlaywrightEnv(
201
- { runtimeId: "runtime-1", testkit: { serviceEnv: {}, templateContext: {} } },
202
- "http://localhost:3000",
203
- { leaseId: "lease-2", leaseDir: "/tmp/lease-2" },
204
- {}
205
- )
206
- ).toMatchObject({
207
- BASE_URL: "http://localhost:3000",
208
- TESTKIT_RUNTIME_ID: "runtime-1",
209
- TESTKIT_LEASE_ID: "lease-2",
210
- PLAYWRIGHT_HTML_OPEN: "never",
211
- });
212
- });
213
-
214
- it("finalizes runtime.prepare templates with prepareDir", () => {
215
- const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runtime-prepare-"));
216
- const api = makeRuntimeConfig(
217
- "api",
218
- {
219
- cwd: ".",
220
- start: "npm run api",
221
- port: 3000,
222
- baseUrl: "http://127.0.0.1:{port}",
223
- readyUrl: "http://127.0.0.1:{port}/health",
224
- env: {},
225
- },
226
- {
227
- runtime: {
228
- instances: 1,
229
- maxConcurrentTasks: 2,
230
- prepare: {
231
- inputs: ["src/{service}.ts"],
232
- steps: [
233
- {
234
- kind: "command",
235
- cmd: "node scripts/prepare.mjs {prepareDir}",
236
- cwd: ".",
237
- inputs: ["src/{service}.ts"],
238
- },
239
- ],
240
- },
241
- },
242
- }
243
- );
244
-
245
- const [resolved] = resolveRuntimeInstanceConfigs([api], "runtime-1", runtimeDir, {
246
- graphDirName: "api",
247
- portNamespaceIndex: 0,
248
- portNamespaceStride: 1,
249
- });
250
-
251
- expect(resolved.testkit.runtime.prepare).toEqual({
252
- inputs: ["src/api.ts"],
253
- steps: [
254
- {
255
- kind: "command",
256
- cmd: `node scripts/prepare.mjs ${runtimeDir}/services/api/prepared`,
257
- cwd: ".",
258
- inputs: ["src/api.ts"],
259
- },
260
- ],
261
- });
262
- });
263
-
264
- it("parses runtime sockets", () => {
265
- expect(numericPortFromUrl("http://localhost:3000")).toBe(3000);
266
- expect(socketFromUrl("http://localhost:3000")).toEqual({
267
- host: "127.0.0.1",
268
- port: 3000,
269
- });
270
- expect(normalizeSocketHost("[::1]")).toBe("::1");
271
- });
272
- });
@@ -1,120 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const mockCheck = vi.fn((value, checks) =>
4
- Object.values(checks).every((predicate) => predicate(value))
5
- );
6
- const mockGroup = vi.fn((name, fn) => fn());
7
-
8
- vi.mock("k6", () => ({
9
- check: mockCheck,
10
- group: mockGroup,
11
- }));
12
-
13
- vi.mock("k6/http", () => ({
14
- default: {
15
- del: vi.fn(),
16
- file: vi.fn(),
17
- get: vi.fn(),
18
- patch: vi.fn(),
19
- post: vi.fn(),
20
- put: vi.fn(),
21
- },
22
- }));
23
-
24
- vi.mock("k6/metrics", () => ({
25
- Rate: class {
26
- add() {}
27
- },
28
- }));
29
-
30
- const { runAuthGateChecks, runPaginationChecks } = await import("./http-checks.js");
31
-
32
- function makeJsonResponse(status, body) {
33
- return {
34
- status,
35
- body: JSON.stringify(body),
36
- headers: { "content-type": "application/json" },
37
- };
38
- }
39
-
40
- describe("runtime http checks", () => {
41
- beforeEach(() => {
42
- mockCheck.mockClear();
43
- mockGroup.mockClear();
44
- });
45
-
46
- it("executes auth-gate checks across configured methods", () => {
47
- const rawReq = vi.fn((method, path, body) => {
48
- return makeJsonResponse(401, {
49
- error: {
50
- code: "UNAUTHORIZED",
51
- message: `${method} ${path} requires auth`,
52
- },
53
- body,
54
- });
55
- });
56
-
57
- runAuthGateChecks(rawReq, "sessions", {
58
- get: ["/api/v1/sessions"],
59
- post: [["/api/v1/sessions", { name: "draft" }]],
60
- validateGetErrorShape: true,
61
- });
62
-
63
- expect(rawReq.mock.calls).toEqual([
64
- ["GET", "/api/v1/sessions", undefined],
65
- ["POST", "/api/v1/sessions", { name: "draft" }],
66
- ]);
67
- expect(mockGroup).toHaveBeenCalledWith(
68
- "sessions GET endpoints return 401 without auth",
69
- expect.any(Function)
70
- );
71
- expect(mockGroup).toHaveBeenCalledWith(
72
- "sessions POST endpoints return 401 without auth",
73
- expect.any(Function)
74
- );
75
- });
76
-
77
- it("executes default and audit-log pagination abuse cases", () => {
78
- const requested = [];
79
- const req = {
80
- get: vi.fn((url) => {
81
- requested.push(url);
82
- const expects400 =
83
- url.includes("limit=-1") ||
84
- url.includes("limit=abc") ||
85
- url.includes("offset=-1") ||
86
- url.includes("offset=1.5") ||
87
- url.includes("limit=Infinity") ||
88
- url.includes("limit=NaN") ||
89
- url.includes("offset=NaN") ||
90
- url.endsWith("limit=") ||
91
- url.endsWith("offset=");
92
-
93
- if (expects400) {
94
- return makeJsonResponse(400, {
95
- error: {
96
- code: "VALIDATION_ERROR",
97
- message: "invalid pagination",
98
- },
99
- });
100
- }
101
-
102
- return makeJsonResponse(200, {
103
- data: [],
104
- pagination: {
105
- limit: 25,
106
- offset: 0,
107
- },
108
- });
109
- }),
110
- };
111
-
112
- runPaginationChecks(req, "/api/v1/audit-logs", { auditLogsExtra: true });
113
-
114
- expect(requested).toHaveLength(13);
115
- expect(requested).toContain("/api/v1/audit-logs?limit=-1");
116
- expect(requested).toContain("/api/v1/audit-logs?offset=1.5");
117
- expect(requested).toContain("/api/v1/audit-logs?limit=Infinity");
118
- expect(requested).toContain("/api/v1/audit-logs?limit=0x10");
119
- });
120
- });
@@ -1,205 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const mockHttp = {
4
- del: vi.fn(),
5
- file: vi.fn((data, filename, contentType) => ({
6
- __testkitFile: true,
7
- contentType,
8
- data,
9
- filename,
10
- })),
11
- get: vi.fn(),
12
- patch: vi.fn(),
13
- post: vi.fn(),
14
- put: vi.fn(),
15
- };
16
-
17
- vi.mock("k6/http", () => ({
18
- default: mockHttp,
19
- }));
20
-
21
- vi.mock("k6", () => ({
22
- check: vi.fn((value, checks) =>
23
- Object.values(checks).every((predicate) => predicate(value))
24
- ),
25
- group: vi.fn((name, fn) => fn()),
26
- }));
27
-
28
- vi.mock("k6/metrics", () => ({
29
- Rate: class {
30
- add() {}
31
- },
32
- }));
33
-
34
- const { createHttpClient } = await import("./http.js");
35
-
36
- function makeResponse(status = 200, body = { ok: true }) {
37
- return {
38
- status,
39
- body: JSON.stringify(body),
40
- headers: { "content-type": "application/json" },
41
- };
42
- }
43
-
44
- function makeSessionBundle() {
45
- return {
46
- primaryActor: "userA",
47
- actors: {
48
- userA: {
49
- actorIndex: 0,
50
- actorName: "userA",
51
- email: "testkit+fixture.user-a@example.test",
52
- jwt: "jwt-user-a",
53
- organizationId: "org-alpha",
54
- organizationKey: "primary",
55
- organizationName: "Primary Org",
56
- session: {
57
- jwt: "jwt-user-a",
58
- organizationId: "org-alpha",
59
- },
60
- },
61
- userB: {
62
- actorIndex: 1,
63
- actorName: "userB",
64
- email: "testkit+fixture.user-b@example.test",
65
- jwt: "jwt-user-b",
66
- organizationId: "org-beta",
67
- organizationKey: "secondary",
68
- organizationName: "Secondary Org",
69
- session: {
70
- jwt: "jwt-user-b",
71
- organizationId: "org-beta",
72
- },
73
- },
74
- },
75
- };
76
- }
77
-
78
- describe("runtime http client", () => {
79
- beforeEach(() => {
80
- globalThis.__ENV = { TESTKIT_RUNTIME_ID: "unit" };
81
- mockHttp.del.mockReset();
82
- mockHttp.file.mockClear();
83
- mockHttp.get.mockReset();
84
- mockHttp.patch.mockReset();
85
- mockHttp.post.mockReset();
86
- mockHttp.put.mockReset();
87
-
88
- mockHttp.get.mockImplementation(() => makeResponse());
89
- mockHttp.post.mockImplementation(() => makeResponse(201));
90
- mockHttp.put.mockImplementation(() => makeResponse());
91
- mockHttp.patch.mockImplementation(() => makeResponse());
92
- mockHttp.del.mockImplementation(() => makeResponse(204));
93
- });
94
-
95
- it("resolves actor-aware auth headers and raw headers", () => {
96
- const client = createHttpClient({
97
- baseUrl: "http://api.test",
98
- defaultActor: "userA",
99
- routeHeaders: { "x-route": "route-a" },
100
- sessionBundle: makeSessionBundle(),
101
- getHeaders({ actor }) {
102
- return actor?.jwt
103
- ? {
104
- Authorization: `Bearer ${actor.jwt}`,
105
- "Content-Type": "application/json",
106
- "X-Organization-Id": actor.organizationId,
107
- }
108
- : {};
109
- },
110
- getRawHeaders({ actor }) {
111
- return actor?.organizationId
112
- ? {
113
- "X-Organization-Id": actor.organizationId,
114
- }
115
- : {
116
- "X-Testkit-Mode": "raw",
117
- };
118
- },
119
- });
120
-
121
- expect(client.headers({ "X-Test": "1" })).toEqual({
122
- "Content-Type": "application/json",
123
- Authorization: "Bearer jwt-user-a",
124
- "X-Organization-Id": "org-alpha",
125
- "x-route": "route-a",
126
- "X-Test": "1",
127
- });
128
-
129
- expect(client.raw.headers()).toEqual({
130
- "Content-Type": "application/json",
131
- "X-Testkit-Mode": "raw",
132
- "x-route": "route-a",
133
- });
134
-
135
- expect(client.raw.as("userB").headers({ "X-Trace": "secondary" })).toEqual({
136
- "Content-Type": "application/json",
137
- "X-Organization-Id": "org-beta",
138
- "x-route": "route-a",
139
- "X-Trace": "secondary",
140
- });
141
-
142
- client.as("userB").rawReq.get("/api/v1/context", { "X-Test": "1" });
143
-
144
- expect(mockHttp.get).toHaveBeenCalledWith("http://api.test/api/v1/context", {
145
- headers: expect.objectContaining({
146
- "Content-Type": "application/json",
147
- "X-Organization-Id": "org-beta",
148
- "x-route": "route-a",
149
- "X-Test": "1",
150
- "x-request-id": expect.stringContaining("tk_unit_exec_"),
151
- }),
152
- });
153
- });
154
-
155
- it("builds multipart requests without forcing json content type", () => {
156
- const client = createHttpClient({
157
- baseUrl: "http://api.test",
158
- defaultActor: "userA",
159
- sessionBundle: makeSessionBundle(),
160
- getHeaders({ actor }) {
161
- return actor?.jwt
162
- ? {
163
- Authorization: `Bearer ${actor.jwt}`,
164
- "Content-Type": "application/json",
165
- "X-Organization-Id": actor.organizationId,
166
- }
167
- : {};
168
- },
169
- });
170
-
171
- client.multipart.post("/api/v1/uploads", {
172
- fields: { name: "fixture-upload" },
173
- files: [
174
- {
175
- field: "file",
176
- data: "hello world",
177
- filename: "fixture.txt",
178
- contentType: "text/plain",
179
- },
180
- ],
181
- });
182
-
183
- expect(mockHttp.file).toHaveBeenCalledWith("hello world", "fixture.txt", "text/plain");
184
- expect(mockHttp.post).toHaveBeenCalledWith(
185
- "http://api.test/api/v1/uploads",
186
- {
187
- file: {
188
- __testkitFile: true,
189
- contentType: "text/plain",
190
- data: "hello world",
191
- filename: "fixture.txt",
192
- },
193
- name: "fixture-upload",
194
- },
195
- {
196
- headers: expect.objectContaining({
197
- Authorization: "Bearer jwt-user-a",
198
- "X-Organization-Id": "org-alpha",
199
- "x-request-id": expect.stringContaining("tk_unit_exec_"),
200
- }),
201
- }
202
- );
203
- expect(mockHttp.post.mock.calls[0][2].headers["Content-Type"]).toBeUndefined();
204
- });
205
- });
@@ -1,69 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- extractCookie,
4
- getSseEventData,
5
- parseJsonBody,
6
- parseSseEvents,
7
- } from "./http-parsing.mjs";
8
- import {
9
- deriveDeterministicIp,
10
- extractErrorMessageFromBody,
11
- hasStandardErrorShape,
12
- } from "./error-body.mjs";
13
-
14
- describe("runtime shared parsing", () => {
15
- it("extracts cookies from set-cookie headers", () => {
16
- const response = {
17
- headers: {
18
- "set-cookie": [
19
- "fixture_session=jwt-primary; Path=/",
20
- "fixture_refresh=refresh-primary; Path=/",
21
- ],
22
- },
23
- };
24
-
25
- expect(extractCookie(response, "fixture_session")).toBe("jwt-primary");
26
- expect(extractCookie(response, "fixture_refresh")).toBe("refresh-primary");
27
- expect(extractCookie(response, "missing")).toBeNull();
28
- });
29
-
30
- it("parses JSON response bodies", () => {
31
- expect(parseJsonBody({ body: "{\"ok\":true}" })).toEqual({ ok: true });
32
- });
33
-
34
- it("parses SSE events and extracts typed event data", () => {
35
- const body = [
36
- "event: progress",
37
- "data: {\"step\":1}",
38
- "",
39
- "event: complete",
40
- "data: {\"success\":true}",
41
- "",
42
- ].join("\n");
43
-
44
- expect(parseSseEvents(body)).toEqual([
45
- { event: "progress", data: { step: 1 } },
46
- { event: "complete", data: { success: true } },
47
- ]);
48
- expect(getSseEventData(body, "complete")).toEqual({ success: true });
49
- });
50
- });
51
-
52
- describe("runtime shared error helpers", () => {
53
- it("extracts human-readable error messages", () => {
54
- expect(extractErrorMessageFromBody({ error: "bad request" })).toBe("bad request");
55
- expect(extractErrorMessageFromBody({ error: { message: "nested" } })).toBe("nested");
56
- expect(extractErrorMessageFromBody({ message: "top-level" })).toBe("top-level");
57
- expect(extractErrorMessageFromBody({ ok: true })).toBeNull();
58
- });
59
-
60
- it("detects the standard structured error shape", () => {
61
- expect(hasStandardErrorShape({ error: { code: "BAD", message: "boom" } })).toBe(true);
62
- expect(hasStandardErrorShape({ error: { message: "boom" } })).toBe(false);
63
- });
64
-
65
- it("derives deterministic test-network IPs", () => {
66
- expect(deriveDeterministicIp("bourne", 1)).toBe(deriveDeterministicIp("bourne", 1));
67
- expect(deriveDeterministicIp("bourne", 1)).not.toBe(deriveDeterministicIp("bourne", 2));
68
- });
69
- });