@elench/testkit 0.1.82 → 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.
- 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/prompt-builder.mjs +25 -0
- package/lib/cli/agents/providers/claude.mjs +74 -0
- package/lib/cli/agents/providers/codex.mjs +83 -0
- package/lib/cli/agents/providers/shared.mjs +134 -0
- package/lib/cli/command-helpers.mjs +53 -25
- package/lib/cli/commands/investigate.mjs +87 -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/summary-box.mjs +11 -11
- package/lib/cli/presentation/tree-reporter.mjs +159 -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-state.mjs +481 -0
- package/lib/cli/tui/run-tree-state.mjs +1 -0
- package/lib/config-api/auth-fixtures.mjs +15 -10
- 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-suite-runtime.js +5 -1
- package/lib/runtime-src/k6/http.js +213 -23
- package/lib/runtime-src/shared/error-body.mjs +42 -0
- package/lib/runtime-src/shared/http-parsing.mjs +68 -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 +7 -6
- package/lib/app/configs.test.mjs +0 -34
- package/lib/app/typecheck.test.mjs +0 -24
- package/lib/bundler/index.test.mjs +0 -164
- package/lib/cli/args.test.mjs +0 -110
- package/lib/cli/presentation/code-frames.test.mjs +0 -71
- package/lib/cli/presentation/run-reporter.test.mjs +0 -192
- package/lib/cli/presentation/summary-box.test.mjs +0 -43
- package/lib/cli/presentation/terminal-layout.test.mjs +0 -23
- package/lib/config/database.test.mjs +0 -29
- package/lib/config/discovery.test.mjs +0 -276
- package/lib/config/env.test.mjs +0 -40
- package/lib/config/index.test.mjs +0 -44
- package/lib/config/paths.test.mjs +0 -27
- package/lib/config/runtime.test.mjs +0 -82
- package/lib/config/skip-config.test.mjs +0 -63
- package/lib/config-api/index.test.mjs +0 -344
- package/lib/config-api/next-runtime-tsconfig.test.mjs +0 -58
- package/lib/coverage/backend-discovery.test.mjs +0 -61
- package/lib/coverage/evidence.test.mjs +0 -87
- package/lib/coverage/index.test.mjs +0 -715
- package/lib/coverage/routing.test.mjs +0 -36
- package/lib/coverage/shared.test.mjs +0 -72
- package/lib/database/fingerprint.test.mjs +0 -99
- package/lib/database/index.test.mjs +0 -95
- package/lib/database/naming.test.mjs +0 -39
- package/lib/database/state.test.mjs +0 -66
- package/lib/database/template-steps.test.mjs +0 -43
- package/lib/discovery/file-metadata.test.mjs +0 -51
- package/lib/discovery/index.test.mjs +0 -182
- package/lib/discovery/path-policy.test.mjs +0 -65
- package/lib/drizzle/index.test.mjs +0 -33
- package/lib/env/index.test.mjs +0 -82
- package/lib/history/index.test.mjs +0 -115
- package/lib/package.test.mjs +0 -59
- package/lib/playwright/index.test.mjs +0 -43
- package/lib/regressions/github.test.mjs +0 -324
- package/lib/regressions/index.test.mjs +0 -187
- package/lib/reporters/playwright.test.mjs +0 -167
- package/lib/runner/default-runtime-errors.test.mjs +0 -49
- package/lib/runner/execution-config.test.mjs +0 -67
- package/lib/runner/failure-details.test.mjs +0 -114
- package/lib/runner/formatting.test.mjs +0 -205
- package/lib/runner/metadata.test.mjs +0 -52
- package/lib/runner/planning.test.mjs +0 -371
- package/lib/runner/playwright-config.test.mjs +0 -78
- package/lib/runner/processes.test.mjs +0 -21
- package/lib/runner/regressions.test.mjs +0 -168
- package/lib/runner/reporting.test.mjs +0 -310
- package/lib/runner/results.test.mjs +0 -376
- package/lib/runner/runtime-manager.test.mjs +0 -252
- package/lib/runner/runtime-preparation.test.mjs +0 -141
- package/lib/runner/selection.test.mjs +0 -24
- package/lib/runner/setup-operations.test.mjs +0 -94
- package/lib/runner/state.test.mjs +0 -62
- package/lib/runner/suite-selection.test.mjs +0 -49
- package/lib/runner/template.test.mjs +0 -272
- package/lib/shared/build-config.test.mjs +0 -132
- package/lib/shared/configured-steps.test.mjs +0 -102
- package/lib/shared/execution-schema.test.mjs +0 -26
- package/lib/shared/file-timeout.test.mjs +0 -64
- package/lib/shared/test-context.test.mjs +0 -43
- package/lib/timing/index.test.mjs +0 -64
- package/lib/toolchains/index.test.mjs +0 -168
- package/lib/vitest/index.test.mjs +0 -20
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from "./args.mjs";
|
|
12
12
|
import * as runner from "../runner/index.mjs";
|
|
13
13
|
import { createRunReporter } from "./presentation/run-reporter.mjs";
|
|
14
|
+
import { createTreeReporter } from "./presentation/tree-reporter.mjs";
|
|
15
|
+
import { createRunEventsReporter } from "./presentation/events-reporter.mjs";
|
|
14
16
|
|
|
15
17
|
export const sharedFlags = {
|
|
16
18
|
dir: Flags.string({
|
|
@@ -64,7 +66,7 @@ export const runFlags = {
|
|
|
64
66
|
}),
|
|
65
67
|
"output-mode": Flags.string({
|
|
66
68
|
description: "Reporter mode",
|
|
67
|
-
options: ["compact", "debug"],
|
|
69
|
+
options: ["compact", "debug", "events"],
|
|
68
70
|
}),
|
|
69
71
|
debug: Flags.boolean({
|
|
70
72
|
description: "Alias for --output-mode debug",
|
|
@@ -94,31 +96,57 @@ export async function executeRunCommand(command, flags, positionalType = null) {
|
|
|
94
96
|
: flags.debug
|
|
95
97
|
? "debug"
|
|
96
98
|
: flags["output-mode"] || "compact";
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
|
|
100
|
+
let reporter;
|
|
101
|
+
let finalize = Promise.resolve();
|
|
102
|
+
let close = () => {};
|
|
103
|
+
|
|
104
|
+
if (outputMode === "compact" && process.stdout.isTTY) {
|
|
105
|
+
const tree = createTreeReporter({
|
|
106
|
+
stdout: process.stdout,
|
|
107
|
+
stderr: process.stderr,
|
|
108
|
+
productDir,
|
|
109
|
+
});
|
|
110
|
+
reporter = tree.reporter;
|
|
111
|
+
finalize = tree.finalize;
|
|
112
|
+
close = tree.close;
|
|
113
|
+
} else if (outputMode === "events") {
|
|
114
|
+
reporter = createRunEventsReporter({ stdout: process.stdout, stderr: process.stderr });
|
|
115
|
+
} else {
|
|
116
|
+
reporter = createRunReporter({ outputMode });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await runner.runAll(
|
|
121
|
+
configs,
|
|
104
122
|
typeValues,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
suiteSelectors,
|
|
124
|
+
{
|
|
125
|
+
...flags,
|
|
126
|
+
typeValues,
|
|
127
|
+
fileNames,
|
|
128
|
+
workers,
|
|
129
|
+
fileTimeoutSeconds,
|
|
130
|
+
shard,
|
|
131
|
+
scenarioSeed: flags.seed || null,
|
|
132
|
+
serviceFilter: flags.service || null,
|
|
133
|
+
reporter,
|
|
134
|
+
writeStatus: flags["write-status"],
|
|
135
|
+
allowPartialStatus: flags["allow-partial-status"],
|
|
136
|
+
ignoreSkipRules: flags["ignore-skip-rules"],
|
|
137
|
+
},
|
|
138
|
+
allConfigs
|
|
139
|
+
);
|
|
140
|
+
await finalize;
|
|
141
|
+
return {
|
|
142
|
+
outputMode,
|
|
143
|
+
...result,
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
close();
|
|
147
|
+
await finalize.catch(() => {});
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
export async function runStatusLike(commandName, flags) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import { sharedFlags } from "../command-helpers.mjs";
|
|
3
|
+
import { loadCurrentRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
4
|
+
import { runInteractiveInvestigation, startHostedInvestigation } from "../agents/investigate.mjs";
|
|
5
|
+
|
|
6
|
+
export default class InvestigateCommand extends Command {
|
|
7
|
+
static summary = "Investigate a failed file from the latest run with Codex or Claude";
|
|
8
|
+
|
|
9
|
+
static enableJsonFlag = true;
|
|
10
|
+
|
|
11
|
+
static args = {
|
|
12
|
+
file: Args.string({
|
|
13
|
+
description: "Optional file path; defaults to the first failed file",
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
static flags = {
|
|
19
|
+
...sharedFlags,
|
|
20
|
+
provider: Flags.string({
|
|
21
|
+
description: "Agent provider to use",
|
|
22
|
+
options: ["auto", "claude", "codex"],
|
|
23
|
+
default: "auto",
|
|
24
|
+
}),
|
|
25
|
+
message: Flags.string({
|
|
26
|
+
description: "Additional user instruction for the investigation prompt",
|
|
27
|
+
}),
|
|
28
|
+
handoff: Flags.boolean({
|
|
29
|
+
description: "Launch the provider's native interactive TUI instead of hosted output",
|
|
30
|
+
default: false,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(InvestigateCommand);
|
|
36
|
+
const productDir = flags.dir || process.cwd();
|
|
37
|
+
const runArtifact = loadCurrentRunArtifact(productDir);
|
|
38
|
+
const subject = resolveFileSubject(runArtifact, args.file || null, flags.service || null);
|
|
39
|
+
|
|
40
|
+
if (flags.handoff) {
|
|
41
|
+
const result = await runInteractiveInvestigation({
|
|
42
|
+
productDir,
|
|
43
|
+
serviceName: subject.service.name,
|
|
44
|
+
filePath: subject.file.path,
|
|
45
|
+
provider: flags.provider,
|
|
46
|
+
userMessage: flags.message || null,
|
|
47
|
+
});
|
|
48
|
+
if (!this.jsonEnabled()) {
|
|
49
|
+
this.log(`${result.provider} exited with code ${result.exitCode}`);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let finalText = "";
|
|
55
|
+
const session = startHostedInvestigation({
|
|
56
|
+
productDir,
|
|
57
|
+
serviceName: subject.service.name,
|
|
58
|
+
filePath: subject.file.path,
|
|
59
|
+
provider: flags.provider,
|
|
60
|
+
userMessage: flags.message || null,
|
|
61
|
+
onEvent: this.jsonEnabled()
|
|
62
|
+
? null
|
|
63
|
+
: (event) => {
|
|
64
|
+
if (event.type === "status" || event.type === "tool") {
|
|
65
|
+
this.log(event.type === "tool" ? `[tool] ${event.name}${event.detail ? `: ${event.detail}` : ""}` : `[status] ${event.message}`);
|
|
66
|
+
} else if (event.type === "error") {
|
|
67
|
+
this.error(event.message);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const result = await session.completion;
|
|
72
|
+
finalText = result.finalText || "";
|
|
73
|
+
|
|
74
|
+
if (!this.jsonEnabled() && finalText.trim()) {
|
|
75
|
+
this.log("");
|
|
76
|
+
this.log(finalText.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
provider: result.provider,
|
|
81
|
+
exitCode: result.exitCode,
|
|
82
|
+
file: subject.file.path,
|
|
83
|
+
service: subject.service.name,
|
|
84
|
+
finalText,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export function normalizeCliArgs(argv) {
|
|
|
11
11
|
"discover",
|
|
12
12
|
"typecheck",
|
|
13
13
|
"doctor",
|
|
14
|
+
"investigate",
|
|
14
15
|
"browser",
|
|
15
16
|
"db",
|
|
16
17
|
"help",
|
|
@@ -37,6 +38,8 @@ export function normalizeCliArgs(argv) {
|
|
|
37
38
|
"--output-mode",
|
|
38
39
|
"--tail",
|
|
39
40
|
"--log-tail",
|
|
41
|
+
"--provider",
|
|
42
|
+
"--message",
|
|
40
43
|
]);
|
|
41
44
|
const positionals = findPositionals(argv, valueFlags);
|
|
42
45
|
const firstPositional = positionals[0] || null;
|
|
@@ -35,6 +35,18 @@ export function red(text) {
|
|
|
35
35
|
return pc.red(text);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function green(text) {
|
|
39
|
+
return pc.green(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function yellow(text) {
|
|
43
|
+
return pc.yellow(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function cyan(text) {
|
|
47
|
+
return pc.cyan(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
export function muted(text) {
|
|
39
51
|
return pc.dim(text);
|
|
40
52
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export function createRunEventsReporter({ stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
2
|
+
function writeEvent(type, payload = {}) {
|
|
3
|
+
stdout.write(`${JSON.stringify({ type, ...payload })}\n`);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
outputMode: "events",
|
|
8
|
+
|
|
9
|
+
setRegressionCatalog(document) {
|
|
10
|
+
writeEvent("run.regression_catalog", {
|
|
11
|
+
configured: Boolean(document?.configured),
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
setServicePlans(plans) {
|
|
16
|
+
writeEvent("run.service_plans", {
|
|
17
|
+
services: plans.map((plan) => ({
|
|
18
|
+
service: plan.config.name,
|
|
19
|
+
skipped: Boolean(plan.skipped),
|
|
20
|
+
suites: (plan.suites || []).map((suite) => ({
|
|
21
|
+
name: suite.name,
|
|
22
|
+
type: suite.type,
|
|
23
|
+
displayType: suite.displayType,
|
|
24
|
+
framework: suite.framework,
|
|
25
|
+
files: suite.files,
|
|
26
|
+
})),
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
setTotalFileCount(count) {
|
|
32
|
+
writeEvent("run.total_files", { total: count });
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
phaseStarted(label) {
|
|
36
|
+
writeEvent("run.phase_started", { label });
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
toolchainResolved(config, resolvedToolchain) {
|
|
40
|
+
writeEvent("run.toolchain_resolved", {
|
|
41
|
+
service: config.name,
|
|
42
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
43
|
+
summary: resolvedToolchain.summary,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
setupOperationFinished(operation) {
|
|
48
|
+
if (!operation) return;
|
|
49
|
+
writeEvent("run.setup_finished", {
|
|
50
|
+
service: operation.serviceName,
|
|
51
|
+
stage: operation.stage,
|
|
52
|
+
status: operation.status,
|
|
53
|
+
summary: operation.summary || null,
|
|
54
|
+
durationMs: operation.durationMs ?? null,
|
|
55
|
+
error: operation.error || null,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
localServiceStarting(config, command) {
|
|
60
|
+
writeEvent("run.local_service_starting", {
|
|
61
|
+
service: config.name,
|
|
62
|
+
runtimeLabel: config.runtimeLabel || config.name,
|
|
63
|
+
command,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
serviceSkipped(config, reason) {
|
|
68
|
+
writeEvent("run.service_skipped", {
|
|
69
|
+
service: config.name,
|
|
70
|
+
reason,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
plannedSkip(entry) {
|
|
75
|
+
writeEvent("run.task_planned_skip", {
|
|
76
|
+
service: entry.serviceName,
|
|
77
|
+
file: entry.file,
|
|
78
|
+
reason: entry.reason || null,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
taskStarted(task) {
|
|
83
|
+
writeEvent("run.task_started", {
|
|
84
|
+
service: task.serviceName,
|
|
85
|
+
file: task.file,
|
|
86
|
+
suite: task.suiteName,
|
|
87
|
+
type: task.type,
|
|
88
|
+
framework: task.framework,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
taskFinished(task, outcome) {
|
|
93
|
+
writeEvent("run.task_finished", {
|
|
94
|
+
service: task.serviceName,
|
|
95
|
+
file: task.file,
|
|
96
|
+
suite: task.suiteName,
|
|
97
|
+
type: task.type,
|
|
98
|
+
framework: task.framework,
|
|
99
|
+
outcome,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
runtimeError(task, message) {
|
|
104
|
+
writeEvent("run.runtime_error", {
|
|
105
|
+
service: task.serviceName,
|
|
106
|
+
file: task.file,
|
|
107
|
+
message,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
telemetry(message) {
|
|
112
|
+
writeEvent("run.telemetry", { message });
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
writeLine(line = "") {
|
|
116
|
+
writeEvent("run.output", { line });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
writeDebugLine(line = "") {
|
|
120
|
+
writeEvent("run.debug", { line });
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
runSummary(results, durationMs, regressionReport) {
|
|
124
|
+
writeEvent("run.summary", {
|
|
125
|
+
results,
|
|
126
|
+
durationMs,
|
|
127
|
+
regressionReport,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
error(message) {
|
|
132
|
+
stderr.write(`${message}\n`);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -8,8 +8,8 @@ export function renderSummaryBox(
|
|
|
8
8
|
widthRatio = 0.55,
|
|
9
9
|
minWidth = 30,
|
|
10
10
|
maxWidth = 56,
|
|
11
|
+
minKeyWidth = 6,
|
|
11
12
|
minValueWidth = 8,
|
|
12
|
-
maxKeyWidth = 12,
|
|
13
13
|
} = {}
|
|
14
14
|
) {
|
|
15
15
|
if (!Array.isArray(rows) || rows.length === 0) return [];
|
|
@@ -17,23 +17,23 @@ export function renderSummaryBox(
|
|
|
17
17
|
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
18
18
|
const preferredWidth = clamp(Math.floor(terminalWidth * widthRatio), minWidth, maxWidth);
|
|
19
19
|
const maxRenderableWidth = Math.max(minWidth, Math.min(preferredWidth, terminalWidth - 1));
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
24
|
-
const
|
|
25
|
-
const boxWidth = Math.max(minBoxWidth, maxRenderableWidth);
|
|
26
|
-
const valueWidth = Math.max(minValueWidth, boxWidth - keyWidth - 7);
|
|
20
|
+
const widestLabel = Math.max(...rows.map(([label]) => measureWidth(label)), minKeyWidth);
|
|
21
|
+
const boxWidth = Math.max(minWidth, maxRenderableWidth);
|
|
22
|
+
const contentWidth = Math.max(2, boxWidth - 7);
|
|
23
|
+
const keyWidth = Math.min(widestLabel, Math.max(minKeyWidth, contentWidth - minValueWidth));
|
|
24
|
+
const valueWidth = Math.max(1, contentWidth - keyWidth);
|
|
27
25
|
|
|
28
26
|
const top = `${figures.lineDownRight}${figures.line.repeat(boxWidth - 2)}${figures.lineDownLeft}`;
|
|
29
27
|
const bottom = `${figures.lineUpRight}${figures.line.repeat(boxWidth - 2)}${figures.lineUpLeft}`;
|
|
30
28
|
const rendered = [top];
|
|
31
29
|
|
|
32
30
|
for (const [label, value] of rows) {
|
|
31
|
+
const wrappedKeyLines = wrapText(label, keyWidth);
|
|
33
32
|
const wrappedValueLines = wrapText(value, valueWidth);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
33
|
+
const lineCount = Math.max(wrappedKeyLines.length, wrappedValueLines.length);
|
|
34
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
35
|
+
const keyCell = padEndVisible(wrappedKeyLines[index] ?? "", keyWidth);
|
|
36
|
+
const valueCell = padEndVisible(wrappedValueLines[index] ?? "", valueWidth);
|
|
37
37
|
rendered.push(
|
|
38
38
|
`${figures.lineVertical} ${keyCell} ${figures.lineVertical} ${valueCell} ${figures.lineVertical}`
|
|
39
39
|
);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { createRunSessionState } from "../tui/run-session-state.mjs";
|
|
4
|
+
import { RunSessionApp } from "../tui/run-session-app.mjs";
|
|
5
|
+
import { suiteSelectionType } from "../../runner/suite-selection.mjs";
|
|
6
|
+
import { startHostedInvestigation } from "../agents/investigate.mjs";
|
|
7
|
+
|
|
8
|
+
export function createTreeReporter({ stdout = process.stdout, stderr = process.stderr, productDir } = {}) {
|
|
9
|
+
const sessionState = createRunSessionState();
|
|
10
|
+
let activeAgentSession = null;
|
|
11
|
+
let investigationToken = 0;
|
|
12
|
+
|
|
13
|
+
const app = render(
|
|
14
|
+
createElement(RunSessionApp, {
|
|
15
|
+
sessionState,
|
|
16
|
+
stdout,
|
|
17
|
+
productDir,
|
|
18
|
+
onInvestigate: startInvestigation,
|
|
19
|
+
onRequestClose: close,
|
|
20
|
+
onCancelInvestigation: cancelInvestigation,
|
|
21
|
+
}),
|
|
22
|
+
{ stdout, exitOnCtrlC: false }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const finalize = app.waitUntilExit();
|
|
26
|
+
|
|
27
|
+
const reporter = {
|
|
28
|
+
outputMode: "compact",
|
|
29
|
+
|
|
30
|
+
setServicePlans(plans) {
|
|
31
|
+
sessionState.initFromPlans(plans);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
setTotalFileCount(count) {
|
|
35
|
+
sessionState.setTotalFileCount(count);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
setRegressionCatalog(document) {
|
|
39
|
+
sessionState.setRegressionCatalog(document);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
serviceSkipped(config, reason) {
|
|
43
|
+
sessionState.markServiceSkipped(config.name, reason);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
plannedSkip(entry) {
|
|
47
|
+
sessionState.markPlannedSkip(entry);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
taskStarted(task, _config) {
|
|
51
|
+
const suiteKey = `${task.displayType || suiteSelectionType(task.type, task.framework)}:${task.suiteName}`;
|
|
52
|
+
sessionState.markFileRunning(task.serviceName, suiteKey, task.file);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
taskFinished(task, outcome) {
|
|
56
|
+
sessionState.markFileFinished(task, outcome);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
runtimeError(task, message) {
|
|
60
|
+
sessionState.markRuntimeError(task, message);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
setupOperationFinished(_operation) {
|
|
64
|
+
// tree handles this implicitly through phase
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
phaseStarted(label) {
|
|
68
|
+
sessionState.setPhase(label);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
toolchainResolved() {},
|
|
72
|
+
localServiceStarting() {},
|
|
73
|
+
writeLine() {},
|
|
74
|
+
writeDebugLine() {},
|
|
75
|
+
telemetry() {},
|
|
76
|
+
|
|
77
|
+
runSummary(results, durationMs, regressionReport) {
|
|
78
|
+
sessionState.finish(results, durationMs, regressionReport);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
error(message) {
|
|
82
|
+
stderr.write(`${message}\n`);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
reporter,
|
|
88
|
+
finalize,
|
|
89
|
+
close,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
async function startInvestigation({ provider = "auto", userMessage } = {}) {
|
|
93
|
+
const snapshot = sessionState.getSnapshot();
|
|
94
|
+
if (!snapshot.selectedFailure) {
|
|
95
|
+
sessionState.setNotice("No failed file is selected for investigation.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (activeAgentSession) {
|
|
99
|
+
sessionState.setNotice("An investigation is already running.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const token = ++investigationToken;
|
|
104
|
+
sessionState.beginInvestigation({ provider, userMessage });
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
activeAgentSession = startHostedInvestigation({
|
|
108
|
+
productDir,
|
|
109
|
+
serviceName: snapshot.selectedFailure.serviceName,
|
|
110
|
+
filePath: snapshot.selectedFailure.filePath,
|
|
111
|
+
provider,
|
|
112
|
+
userMessage,
|
|
113
|
+
onEvent(event) {
|
|
114
|
+
if (token !== investigationToken) return;
|
|
115
|
+
sessionState.appendAgentEvent(event);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const result = await activeAgentSession.completion;
|
|
119
|
+
if (token !== investigationToken) return;
|
|
120
|
+
activeAgentSession = null;
|
|
121
|
+
if (result.cancelled) {
|
|
122
|
+
sessionState.cancelAgentSession("Cancelled investigation.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (result.exitCode !== 0 && !result.finalText) {
|
|
126
|
+
sessionState.failAgentSession(result.stderr || `Agent exited with code ${result.exitCode}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
sessionState.completeAgentSession({
|
|
130
|
+
finalText: result.finalText,
|
|
131
|
+
exitCode: result.exitCode,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (token !== investigationToken) return;
|
|
135
|
+
activeAgentSession = null;
|
|
136
|
+
sessionState.failAgentSession(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cancelInvestigation() {
|
|
141
|
+
if (!activeAgentSession) {
|
|
142
|
+
sessionState.returnToSummary();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
investigationToken += 1;
|
|
146
|
+
activeAgentSession.cancel();
|
|
147
|
+
activeAgentSession = null;
|
|
148
|
+
sessionState.cancelAgentSession("Cancelled investigation.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function close() {
|
|
152
|
+
if (activeAgentSession) {
|
|
153
|
+
investigationToken += 1;
|
|
154
|
+
activeAgentSession.cancel();
|
|
155
|
+
activeAgentSession = null;
|
|
156
|
+
}
|
|
157
|
+
app.unmount();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RunSessionApp as RunApp } from "./run-session-app.mjs";
|