@elench/testkit 0.1.83 → 0.1.84

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 (83) hide show
  1. package/lib/cli/agents/providers/codex.mjs +1 -1
  2. package/lib/cli/tui/run-session-app.mjs +1 -1
  3. package/node_modules/@elench/next-analysis/package.json +1 -1
  4. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  5. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  6. package/node_modules/@elench/ts-analysis/package.json +1 -1
  7. package/package.json +7 -6
  8. package/lib/app/configs.test.mjs +0 -34
  9. package/lib/app/typecheck.test.mjs +0 -24
  10. package/lib/bundler/index.test.mjs +0 -164
  11. package/lib/cli/agents/investigation-context.test.mjs +0 -144
  12. package/lib/cli/agents/providers/claude.test.mjs +0 -95
  13. package/lib/cli/agents/providers/codex.test.mjs +0 -93
  14. package/lib/cli/args.test.mjs +0 -110
  15. package/lib/cli/command-helpers.test.mjs +0 -122
  16. package/lib/cli/commands/investigate.test.mjs +0 -83
  17. package/lib/cli/presentation/code-frames.test.mjs +0 -71
  18. package/lib/cli/presentation/events-reporter.test.mjs +0 -73
  19. package/lib/cli/presentation/run-reporter.test.mjs +0 -192
  20. package/lib/cli/presentation/summary-box.test.mjs +0 -60
  21. package/lib/cli/presentation/terminal-layout.test.mjs +0 -23
  22. package/lib/cli/presentation/tree-reporter.test.mjs +0 -166
  23. package/lib/cli/tui/run-session-app.test.mjs +0 -50
  24. package/lib/cli/tui/run-tree-state.test.mjs +0 -324
  25. package/lib/config/database.test.mjs +0 -29
  26. package/lib/config/discovery.test.mjs +0 -276
  27. package/lib/config/env.test.mjs +0 -40
  28. package/lib/config/index.test.mjs +0 -44
  29. package/lib/config/paths.test.mjs +0 -27
  30. package/lib/config/runtime.test.mjs +0 -82
  31. package/lib/config/skip-config.test.mjs +0 -63
  32. package/lib/config-api/index.test.mjs +0 -398
  33. package/lib/config-api/next-runtime-tsconfig.test.mjs +0 -58
  34. package/lib/coverage/backend-discovery.test.mjs +0 -61
  35. package/lib/coverage/evidence.test.mjs +0 -87
  36. package/lib/coverage/index.test.mjs +0 -715
  37. package/lib/coverage/routing.test.mjs +0 -36
  38. package/lib/coverage/shared.test.mjs +0 -72
  39. package/lib/database/fingerprint.test.mjs +0 -99
  40. package/lib/database/index.test.mjs +0 -95
  41. package/lib/database/naming.test.mjs +0 -39
  42. package/lib/database/state.test.mjs +0 -66
  43. package/lib/database/template-steps.test.mjs +0 -43
  44. package/lib/discovery/file-metadata.test.mjs +0 -51
  45. package/lib/discovery/index.test.mjs +0 -182
  46. package/lib/discovery/path-policy.test.mjs +0 -65
  47. package/lib/drizzle/index.test.mjs +0 -33
  48. package/lib/env/index.test.mjs +0 -82
  49. package/lib/history/index.test.mjs +0 -115
  50. package/lib/package.test.mjs +0 -59
  51. package/lib/playwright/index.test.mjs +0 -43
  52. package/lib/regressions/github.test.mjs +0 -324
  53. package/lib/regressions/index.test.mjs +0 -187
  54. package/lib/reporters/playwright.test.mjs +0 -167
  55. package/lib/runner/default-runtime-errors.test.mjs +0 -49
  56. package/lib/runner/execution-config.test.mjs +0 -67
  57. package/lib/runner/failure-details.test.mjs +0 -114
  58. package/lib/runner/formatting.test.mjs +0 -205
  59. package/lib/runner/metadata.test.mjs +0 -52
  60. package/lib/runner/planning.test.mjs +0 -371
  61. package/lib/runner/playwright-config.test.mjs +0 -78
  62. package/lib/runner/processes.test.mjs +0 -21
  63. package/lib/runner/regressions.test.mjs +0 -168
  64. package/lib/runner/reporting.test.mjs +0 -310
  65. package/lib/runner/results.test.mjs +0 -376
  66. package/lib/runner/runtime-manager.test.mjs +0 -252
  67. package/lib/runner/runtime-preparation.test.mjs +0 -141
  68. package/lib/runner/selection.test.mjs +0 -24
  69. package/lib/runner/setup-operations.test.mjs +0 -94
  70. package/lib/runner/state.test.mjs +0 -62
  71. package/lib/runner/suite-selection.test.mjs +0 -49
  72. package/lib/runner/template.test.mjs +0 -272
  73. package/lib/runtime-src/k6/http-checks.test.mjs +0 -120
  74. package/lib/runtime-src/k6/http.test.mjs +0 -205
  75. package/lib/runtime-src/shared/http-parsing.test.mjs +0 -69
  76. package/lib/shared/build-config.test.mjs +0 -132
  77. package/lib/shared/configured-steps.test.mjs +0 -102
  78. package/lib/shared/execution-schema.test.mjs +0 -26
  79. package/lib/shared/file-timeout.test.mjs +0 -64
  80. package/lib/shared/test-context.test.mjs +0 -43
  81. package/lib/timing/index.test.mjs +0 -64
  82. package/lib/toolchains/index.test.mjs +0 -168
  83. package/lib/vitest/index.test.mjs +0 -20
@@ -17,7 +17,7 @@ export function startCodexHostedSession({ cwd, prompt, onEvent, purpose = "inves
17
17
  const args = ["exec", "--json", "-o", outputFile];
18
18
 
19
19
  if (purpose === "investigate") {
20
- args.push("-s", "read-only", "-a", "never");
20
+ args.push("-s", "read-only");
21
21
  }
22
22
 
23
23
  args.push(prompt);
@@ -80,7 +80,7 @@ export function RunSessionApp({
80
80
  sessionState.selectPreviousFailure();
81
81
  return;
82
82
  }
83
- if (key.return || input === "y" || input === "i") {
83
+ if (input === "y" || input === "i") {
84
84
  if (snapshot.selectedFailure) {
85
85
  onInvestigate?.({
86
86
  provider: "auto",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.83"
25
+ "@elench/testkit-protocol": "0.1.84"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -66,7 +66,8 @@
66
66
  "files": [
67
67
  "bin/",
68
68
  "lib/",
69
- "vendor/"
69
+ "vendor/",
70
+ "!lib/**/*.test.*"
70
71
  ],
71
72
  "bundleDependencies": [
72
73
  "@elench/next-analysis",
@@ -81,10 +82,10 @@
81
82
  },
82
83
  "dependencies": {
83
84
  "@babel/code-frame": "^7.29.0",
84
- "@elench/next-analysis": "0.1.83",
85
- "@elench/testkit-bridge": "0.1.83",
86
- "@elench/testkit-protocol": "0.1.83",
87
- "@elench/ts-analysis": "0.1.83",
85
+ "@elench/next-analysis": "0.1.84",
86
+ "@elench/testkit-bridge": "0.1.84",
87
+ "@elench/testkit-protocol": "0.1.84",
88
+ "@elench/ts-analysis": "0.1.84",
88
89
  "@oclif/core": "^4.10.6",
89
90
  "esbuild": "^0.25.11",
90
91
  "execa": "^9.5.0",
@@ -1,34 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- collectRequiredConfigs,
4
- filterConfigsByService,
5
- resolveTargetConfig,
6
- topologicallySortConfigs,
7
- } from "./configs.mjs";
8
-
9
- function makeConfig(name, dependsOn = [], provider = null) {
10
- return {
11
- name,
12
- testkit: {
13
- dependsOn,
14
- database: provider ? { provider } : undefined,
15
- },
16
- };
17
- }
18
-
19
- describe("app config helpers", () => {
20
- it("filters and resolves target configs", () => {
21
- const configs = [makeConfig("api"), makeConfig("frontend", ["api"], "local")];
22
-
23
- expect(filterConfigsByService(configs, "api")).toEqual([configs[0]]);
24
- expect(resolveTargetConfig(configs)).toBe(configs[1]);
25
- expect(() => filterConfigsByService(configs, "missing")).toThrow('Service "missing" not found');
26
- });
27
-
28
- it("collects dependencies and sorts them topologically", () => {
29
- const configs = [makeConfig("frontend", ["api"]), makeConfig("api"), makeConfig("worker", ["api"])];
30
-
31
- expect(collectRequiredConfigs(configs, "frontend").map((config) => config.name).sort()).toEqual(["api", "frontend"]);
32
- expect(topologicallySortConfigs(configs).map((config) => config.name)).toEqual(["api", "frontend", "worker"]);
33
- });
34
- });
@@ -1,24 +0,0 @@
1
- import path from "path";
2
- import { describe, expect, it } from "vitest";
3
- import { buildTypecheckCompilerOptions } from "./typecheck.mjs";
4
-
5
- describe("typecheck app helpers", () => {
6
- it("preserves inherited service alias paths when generating next-service configs", () => {
7
- const fixtureRoot = "/home/georgedlr/workspace/elench/bourne";
8
- const extendsPath = path.join(fixtureRoot, "frontend", "tsconfig.json");
9
- const generatedTsconfig = path.join(fixtureRoot, ".testkit", "_typecheck", "frontend.tsconfig.json");
10
-
11
- const compilerOptions = buildTypecheckCompilerOptions({
12
- tsconfigPath: generatedTsconfig,
13
- extendsPath,
14
- });
15
-
16
- expect(compilerOptions.paths?.["@/*"]).toEqual(["../../frontend/src/*"]);
17
- expect(compilerOptions.paths?.["@playwright-fixtures/*"]).toEqual([
18
- "../../frontend/tests/playwright-fixtures/*",
19
- ]);
20
- expect(compilerOptions.paths?.["@elench/testkit/config"]?.[0]).toMatch(
21
- /(?:\.\.\/)+(?:node_modules\/@elench\/testkit|testkit)\/lib\/config-api\/index\.d\.ts$/
22
- );
23
- });
24
- });
@@ -1,164 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import { pathToFileURL } from "url";
5
- import { afterEach, describe, expect, it } from "vitest";
6
- import { bundleK6File } from "./index.mjs";
7
-
8
- const cleanups = [];
9
-
10
- afterEach(() => {
11
- while (cleanups.length > 0) {
12
- cleanups.pop()();
13
- }
14
- });
15
-
16
- describe("runtime bundler", () => {
17
- it("bundles root and runtime package imports for execution", async () => {
18
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
19
- cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
20
-
21
- const sourceFile = path.join(tmpDir, "health.js");
22
- fs.writeFileSync(
23
- sourceFile,
24
- [
25
- 'import { defineHttpSuite } from "@elench/testkit";',
26
- 'import { check, json } from "@elench/testkit/runtime";',
27
- "const suite = defineHttpSuite(({ rawReq }) => {",
28
- ' const res = rawReq("GET", "/health");',
29
- " check(json(res), {",
30
- ' "has status": (body) => typeof body.status === "string",',
31
- " });",
32
- "});",
33
- "export default suite;",
34
- "",
35
- ].join("\n")
36
- );
37
-
38
- const bundledFile = await bundleK6File({
39
- productDir: tmpDir,
40
- serviceName: "api",
41
- sourceFile,
42
- });
43
-
44
- const bundled = fs.readFileSync(bundledFile, "utf8");
45
- expect(bundled).toContain("defineHttpSuite");
46
- expect(bundled).toContain('import { check');
47
- expect(bundled).toContain('from "k6"');
48
- });
49
-
50
- it("bundles DAL execution through the public package surface", async () => {
51
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
52
- cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
53
-
54
- const sourceFile = path.join(tmpDir, "dal.js");
55
- fs.writeFileSync(
56
- sourceFile,
57
- [
58
- 'import { defineDalSuite } from "@elench/testkit";',
59
- "const suite = defineDalSuite(({ db }) => {",
60
- ' db.query("SELECT 1");',
61
- "});",
62
- "export default suite;",
63
- "",
64
- ].join("\n")
65
- );
66
-
67
- const bundledFile = await bundleK6File({
68
- productDir: tmpDir,
69
- serviceName: "api",
70
- sourceFile,
71
- });
72
-
73
- const bundled = fs.readFileSync(bundledFile, "utf8");
74
- expect(bundled).toContain("defineDalSuite");
75
- expect(bundled).toContain('import sql from "k6/x/sql"');
76
- });
77
-
78
- it("bundles scenario execution through the public package surface", async () => {
79
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
80
- cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
81
-
82
- const sourceFile = path.join(tmpDir, "scenario.js");
83
- fs.writeFileSync(
84
- sourceFile,
85
- [
86
- 'import { defineScenarioSuite } from "@elench/testkit";',
87
- "const suite = defineScenarioSuite(({ scenario }) => {",
88
- " const plan = scenario.choose('journey', { endpoint: scenario.pick('endpoint', ['/a', '/b']) });",
89
- " scenario.step('record choice', () => plan.endpoint);",
90
- "});",
91
- "export default suite;",
92
- "",
93
- ].join("\n")
94
- );
95
-
96
- const bundledFile = await bundleK6File({
97
- productDir: tmpDir,
98
- serviceName: "api",
99
- sourceFile,
100
- });
101
-
102
- const bundled = fs.readFileSync(bundledFile, "utf8");
103
- expect(bundled).toContain("defineScenarioSuite");
104
- expect(bundled).toContain("createScenarioRuntime");
105
- });
106
-
107
- it("normalizes a default-exported suite object with no setup override", async () => {
108
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
109
- cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
110
-
111
- const sourceFile = path.join(tmpDir, "no-setup.js");
112
- fs.writeFileSync(
113
- sourceFile,
114
- [
115
- "const suite = {",
116
- " options: { vus: 1, iterations: 1 },",
117
- " exec() {",
118
- " return 'ok';",
119
- " },",
120
- "};",
121
- "export default suite;",
122
- "",
123
- ].join("\n")
124
- );
125
-
126
- const bundledFile = await bundleK6File({
127
- productDir: tmpDir,
128
- serviceName: "api",
129
- sourceFile,
130
- });
131
-
132
- const bundled = await import(`${pathToFileURL(bundledFile).href}?v=${Date.now()}`);
133
- expect(typeof bundled.setup).toBe("function");
134
- expect(bundled.setup()).toBeNull();
135
- expect(typeof bundled.default).toBe("function");
136
- expect(bundled.default()).toBe("ok");
137
- });
138
-
139
- it("throws a clear error when the default export is not a suite object", async () => {
140
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
141
- cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
142
-
143
- const sourceFile = path.join(tmpDir, "legacy-shape.js");
144
- fs.writeFileSync(
145
- sourceFile,
146
- [
147
- "export default function exec() {",
148
- " return 'legacy';",
149
- "}",
150
- "",
151
- ].join("\n")
152
- );
153
-
154
- const bundledFile = await bundleK6File({
155
- productDir: tmpDir,
156
- serviceName: "api",
157
- sourceFile,
158
- });
159
-
160
- await expect(
161
- import(`${pathToFileURL(bundledFile).href}?v=${Date.now()}`)
162
- ).rejects.toThrow(/default-export the suite object/);
163
- });
164
- });
@@ -1,144 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
- import {
6
- formatInvestigationTranscriptContext,
7
- loadInvestigationContext,
8
- } from "./investigation-context.mjs";
9
- import { buildInvestigationPrompt, defaultInvestigationMessage } from "./prompt-builder.mjs";
10
-
11
- const tempDirs = [];
12
-
13
- afterEach(() => {
14
- while (tempDirs.length > 0) {
15
- fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
16
- }
17
- });
18
-
19
- function makeTempProduct() {
20
- const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-investigation-"));
21
- tempDirs.push(productDir);
22
- fs.mkdirSync(path.join(productDir, ".testkit", "results"), { recursive: true });
23
- return productDir;
24
- }
25
-
26
- function writeJson(filePath, value) {
27
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
- fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
29
- }
30
-
31
- describe("investigation context", () => {
32
- it("loads the selected failure, artifacts, logs, and formatted detail lines", () => {
33
- const productDir = makeTempProduct();
34
- writeJson(path.join(productDir, "artifacts", "failure.json"), {
35
- source: "testkit-runtime-artifact",
36
- data: { message: "artifact payload" },
37
- });
38
- fs.mkdirSync(path.join(productDir, "logs"), { recursive: true });
39
- fs.writeFileSync(path.join(productDir, "logs", "api.log"), "first line\nsecond line\n");
40
-
41
- writeJson(path.join(productDir, ".testkit", "results", "latest.json"), {
42
- services: [
43
- {
44
- name: "api",
45
- suites: [
46
- {
47
- name: "users",
48
- type: "integration",
49
- files: [
50
- {
51
- path: "tests/api/users.int.testkit.ts",
52
- status: "failed",
53
- durationMs: 1000,
54
- error: "status 500",
55
- failureDetails: [
56
- {
57
- kind: "http-assertion",
58
- title: "status is 200",
59
- message: "GET /users expected 200, got 500",
60
- response: { status: 500, bodyPreview: "{\"error\":\"boom\"}" },
61
- },
62
- ],
63
- artifacts: [
64
- {
65
- name: "failure-artifact",
66
- kind: "example",
67
- summary: "captured payload",
68
- path: "artifacts/failure.json",
69
- },
70
- ],
71
- },
72
- ],
73
- },
74
- ],
75
- },
76
- ],
77
- logs: {
78
- services: [
79
- {
80
- serviceName: "api",
81
- runtimeLabel: "api",
82
- path: "logs/api.log",
83
- },
84
- ],
85
- },
86
- setup: {
87
- operations: [
88
- {
89
- serviceName: "api",
90
- stage: "runtime:prepare",
91
- status: "passed",
92
- summary: "prepared runtime",
93
- durationMs: 1200,
94
- logRef: { path: "logs/api.log" },
95
- },
96
- ],
97
- },
98
- });
99
-
100
- const context = loadInvestigationContext({
101
- productDir,
102
- serviceName: "api",
103
- filePath: "tests/api/users.int.testkit.ts",
104
- });
105
-
106
- expect(context.summary).toMatchObject({
107
- service: "api",
108
- suite: "integration:users",
109
- file: "tests/api/users.int.testkit.ts",
110
- status: "failed",
111
- error: "status 500",
112
- });
113
- expect(context.artifacts[0]).toMatchObject({
114
- name: "failure-artifact",
115
- kind: "example",
116
- path: "artifacts/failure.json",
117
- });
118
- expect(context.backendLogs[0].lines).toContain("second line");
119
- expect(context.detailLines.join("\n")).toContain("GET /users expected 200, got 500");
120
- expect(formatInvestigationTranscriptContext(context)).toContain("Artifacts:");
121
- });
122
-
123
- it("builds the default investigation prompt with contextual evidence", () => {
124
- const context = {
125
- summary: {
126
- service: "api",
127
- suite: "integration:users",
128
- file: "tests/api/users.int.testkit.ts",
129
- status: "failed",
130
- error: "status 500",
131
- },
132
- artifacts: [],
133
- backendLogs: [],
134
- detailLines: ["File: tests/api/users.int.testkit.ts", "Error: status 500"],
135
- };
136
-
137
- const prompt = buildInvestigationPrompt({ context });
138
-
139
- expect(defaultInvestigationMessage()).toContain("Investigate");
140
- expect(prompt).toContain("User request:");
141
- expect(prompt).toContain("Service: api");
142
- expect(prompt).toContain("Detailed Failure View:");
143
- });
144
- });
@@ -1,95 +0,0 @@
1
- import { PassThrough } from "stream";
2
- import { afterEach, describe, expect, it, vi } from "vitest";
3
-
4
- const execaMock = vi.fn();
5
-
6
- vi.mock("execa", () => ({
7
- execa: execaMock,
8
- }));
9
-
10
- function createChildHandle() {
11
- const stdout = new PassThrough();
12
- const stderr = new PassThrough();
13
- let resolveChild;
14
- const promise = new Promise((resolve) => {
15
- resolveChild = resolve;
16
- });
17
- promise.stdout = stdout;
18
- promise.stderr = stderr;
19
- promise.kill = vi.fn(() => {
20
- stdout.end();
21
- stderr.end();
22
- });
23
- return {
24
- child: promise,
25
- finish(result = {}) {
26
- stdout.end();
27
- stderr.end();
28
- resolveChild({
29
- exitCode: 0,
30
- stdout: "",
31
- stderr: "",
32
- ...result,
33
- });
34
- },
35
- };
36
- }
37
-
38
- afterEach(() => {
39
- execaMock.mockReset();
40
- });
41
-
42
- describe("Claude hosted session", () => {
43
- it("normalizes streamed tool, delta, status, and final events", async () => {
44
- const { startClaudeHostedSession } = await import("./claude.mjs");
45
- const handle = createChildHandle();
46
- execaMock.mockReturnValue(handle.child);
47
- const events = [];
48
-
49
- const session = startClaudeHostedSession({
50
- cwd: "/tmp/project",
51
- prompt: "Investigate this failure",
52
- onEvent(event) {
53
- events.push(event);
54
- },
55
- });
56
-
57
- handle.child.stdout.write(`${JSON.stringify({ type: "tool_use", name: "Bash", detail: "rg failure" })}\n`);
58
- handle.child.stdout.write(`${JSON.stringify({ type: "message_delta", delta: "Likely root cause." })}\n`);
59
- handle.child.stderr.write("stderr note\n");
60
- handle.finish({ exitCode: 0 });
61
-
62
- const result = await session.completion;
63
-
64
- expect(execaMock).toHaveBeenCalledWith(
65
- "claude",
66
- expect.arrayContaining(["-p", "--output-format", "stream-json", "--include-partial-messages"]),
67
- expect.objectContaining({ cwd: "/tmp/project", reject: false })
68
- );
69
- expect(events[0]).toMatchObject({ provider: "claude", type: "start" });
70
- expect(events).toContainEqual(expect.objectContaining({ provider: "claude", type: "tool", name: "Bash" }));
71
- expect(events).toContainEqual(expect.objectContaining({ provider: "claude", type: "delta", text: "Likely root cause." }));
72
- expect(events).toContainEqual(expect.objectContaining({ provider: "claude", type: "status", message: "stderr note" }));
73
- expect(events.at(-2)).toMatchObject({ provider: "claude", type: "final", text: "Likely root cause." });
74
- expect(events.at(-1)).toMatchObject({ provider: "claude", type: "exit", code: 0 });
75
- expect(result.finalText).toBe("Likely root cause.");
76
- });
77
-
78
- it("cancels the child process", async () => {
79
- const { startClaudeHostedSession } = await import("./claude.mjs");
80
- const handle = createChildHandle();
81
- execaMock.mockReturnValue(handle.child);
82
-
83
- const session = startClaudeHostedSession({
84
- cwd: "/tmp/project",
85
- prompt: "Investigate this failure",
86
- });
87
-
88
- session.cancel();
89
- handle.finish({ exitCode: 130 });
90
- const result = await session.completion;
91
-
92
- expect(handle.child.kill).toHaveBeenCalledWith("SIGTERM");
93
- expect(result.cancelled).toBe(true);
94
- });
95
- });
@@ -1,93 +0,0 @@
1
- import { PassThrough } from "stream";
2
- import { afterEach, describe, expect, it, vi } from "vitest";
3
-
4
- const execaMock = vi.fn();
5
-
6
- vi.mock("execa", () => ({
7
- execa: execaMock,
8
- }));
9
-
10
- function createChildHandle() {
11
- const stdout = new PassThrough();
12
- const stderr = new PassThrough();
13
- let resolveChild;
14
- const promise = new Promise((resolve) => {
15
- resolveChild = resolve;
16
- });
17
- promise.stdout = stdout;
18
- promise.stderr = stderr;
19
- promise.kill = vi.fn(() => {
20
- stdout.end();
21
- stderr.end();
22
- });
23
- return {
24
- child: promise,
25
- finish(result = {}) {
26
- stdout.end();
27
- stderr.end();
28
- resolveChild({
29
- exitCode: 0,
30
- stdout: "",
31
- stderr: "",
32
- ...result,
33
- });
34
- },
35
- };
36
- }
37
-
38
- afterEach(() => {
39
- execaMock.mockReset();
40
- });
41
-
42
- describe("Codex hosted session", () => {
43
- it("normalizes streamed tool, delta, and final output", async () => {
44
- const { startCodexHostedSession } = await import("./codex.mjs");
45
- const handle = createChildHandle();
46
- execaMock.mockReturnValue(handle.child);
47
- const events = [];
48
-
49
- const session = startCodexHostedSession({
50
- cwd: "/tmp/project",
51
- prompt: "Investigate this failure",
52
- onEvent(event) {
53
- events.push(event);
54
- },
55
- });
56
-
57
- handle.child.stdout.write(`${JSON.stringify({ type: "command", command: "rg", status: "done" })}\n`);
58
- handle.child.stdout.write(`${JSON.stringify({ type: "assistant_delta", text: "Check the migration step." })}\n`);
59
- handle.finish({ exitCode: 0, stdout: "Check the migration step." });
60
-
61
- const result = await session.completion;
62
-
63
- expect(execaMock).toHaveBeenCalledWith(
64
- "codex",
65
- expect.arrayContaining(["exec", "--json", "-s", "read-only", "-a", "never"]),
66
- expect.objectContaining({ cwd: "/tmp/project", reject: false })
67
- );
68
- expect(events[0]).toMatchObject({ provider: "codex", type: "start" });
69
- expect(events).toContainEqual(expect.objectContaining({ provider: "codex", type: "tool", name: "rg" }));
70
- expect(events).toContainEqual(expect.objectContaining({ provider: "codex", type: "delta", text: "Check the migration step." }));
71
- expect(events.at(-2)).toMatchObject({ provider: "codex", type: "final", text: "Check the migration step." });
72
- expect(events.at(-1)).toMatchObject({ provider: "codex", type: "exit", code: 0 });
73
- expect(result.finalText).toBe("Check the migration step.");
74
- });
75
-
76
- it("cancels the child process", async () => {
77
- const { startCodexHostedSession } = await import("./codex.mjs");
78
- const handle = createChildHandle();
79
- execaMock.mockReturnValue(handle.child);
80
-
81
- const session = startCodexHostedSession({
82
- cwd: "/tmp/project",
83
- prompt: "Investigate this failure",
84
- });
85
-
86
- session.cancel();
87
- handle.finish({ exitCode: 130 });
88
- const result = await session.completion;
89
-
90
- expect(handle.child.kill).toHaveBeenCalledWith("SIGTERM");
91
- expect(result.cancelled).toBe(true);
92
- });
93
- });