@elench/testkit 0.1.87 → 0.1.88
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 +19 -11
- package/lib/cli/agents/providers/claude.mjs +1 -1
- package/lib/cli/agents/providers/codex.mjs +1 -1
- package/lib/cli/assistant/prompt-builder.mjs +78 -0
- package/lib/cli/assistant/protocol.mjs +67 -0
- package/lib/cli/assistant/session.mjs +92 -0
- package/lib/cli/assistant/slash-commands.mjs +160 -0
- package/lib/cli/assistant/state.mjs +279 -0
- package/lib/cli/assistant/tool-registry.mjs +236 -0
- package/lib/cli/assistant/tool-run-reporter.mjs +80 -0
- package/lib/cli/command-helpers.mjs +40 -24
- package/lib/cli/commands/assistant.mjs +84 -0
- package/lib/cli/entrypoint.mjs +36 -8
- package/lib/cli/tui/assistant-app.mjs +131 -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 +6 -6
- package/lib/cli/commands/inspect.mjs +0 -124
- package/lib/cli/commands/investigate.mjs +0 -87
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
|
|
2
|
+
import { createInspectState } from "../tui/inspect-state.mjs";
|
|
3
|
+
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
4
|
+
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
5
|
+
import { runAssistantConversationTurn } from "./session.mjs";
|
|
6
|
+
|
|
7
|
+
export function createAssistantState({
|
|
8
|
+
productDir,
|
|
9
|
+
provider = "auto",
|
|
10
|
+
initialPane = "detail",
|
|
11
|
+
dataSource = "artifact",
|
|
12
|
+
configs = [],
|
|
13
|
+
} = {}) {
|
|
14
|
+
const inspectState = createInspectState({ dataSource });
|
|
15
|
+
inspectState.setPaneMode(initialPane);
|
|
16
|
+
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
const messages = [];
|
|
19
|
+
let composer = "";
|
|
20
|
+
let notice = null;
|
|
21
|
+
let busy = false;
|
|
22
|
+
let providerName = provider;
|
|
23
|
+
let activeStatus = null;
|
|
24
|
+
|
|
25
|
+
inspectState.subscribe(() => notify());
|
|
26
|
+
|
|
27
|
+
function notify() {
|
|
28
|
+
for (const callback of listeners) callback();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appendMessage(message) {
|
|
32
|
+
messages.push({
|
|
33
|
+
id: `msg-${messages.length + 1}`,
|
|
34
|
+
...message,
|
|
35
|
+
});
|
|
36
|
+
notify();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setBusy(nextBusy, status = null) {
|
|
40
|
+
busy = Boolean(nextBusy);
|
|
41
|
+
activeStatus = status || null;
|
|
42
|
+
notify();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
inspectState,
|
|
47
|
+
|
|
48
|
+
async loadLatestArtifact() {
|
|
49
|
+
try {
|
|
50
|
+
inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
|
|
51
|
+
} catch {
|
|
52
|
+
// No artifact yet.
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async loadCurrentArtifact() {
|
|
57
|
+
try {
|
|
58
|
+
inspectState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
|
|
59
|
+
} catch {
|
|
60
|
+
// No artifact yet.
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
revealFile(serviceName, filePath) {
|
|
65
|
+
return inspectState.revealFile(serviceName, filePath);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
revealService(serviceName) {
|
|
69
|
+
return inspectState.revealService(serviceName);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
setPaneMode(pane) {
|
|
73
|
+
inspectState.setPaneMode(pane);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
setComposer(value) {
|
|
77
|
+
composer = String(value || "");
|
|
78
|
+
notify();
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
appendComposer(text) {
|
|
82
|
+
composer += String(text || "");
|
|
83
|
+
notify();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
backspaceComposer() {
|
|
87
|
+
composer = composer.slice(0, -1);
|
|
88
|
+
notify();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
clearNotice() {
|
|
92
|
+
notice = null;
|
|
93
|
+
notify();
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
setNotice(message) {
|
|
97
|
+
notice = message ? String(message) : null;
|
|
98
|
+
notify();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
setProvider(nextProvider) {
|
|
102
|
+
providerName = nextProvider || "auto";
|
|
103
|
+
notify();
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
clearMessages() {
|
|
107
|
+
messages.length = 0;
|
|
108
|
+
notify();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async submitCurrentComposer() {
|
|
112
|
+
const value = composer.trim();
|
|
113
|
+
composer = "";
|
|
114
|
+
notify();
|
|
115
|
+
if (!value) return;
|
|
116
|
+
await this.submitInput(value);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async submitInput(input) {
|
|
120
|
+
const trimmed = String(input || "").trim();
|
|
121
|
+
if (!trimmed) return;
|
|
122
|
+
appendMessage({ role: "user", text: trimmed });
|
|
123
|
+
|
|
124
|
+
const slash = parseSlashCommandSafe(trimmed);
|
|
125
|
+
if (slash?.type === "__error__") {
|
|
126
|
+
appendMessage({
|
|
127
|
+
role: "system",
|
|
128
|
+
text: slash.error,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (slash) {
|
|
133
|
+
try {
|
|
134
|
+
await executeSlashCommand({
|
|
135
|
+
slash,
|
|
136
|
+
productDir,
|
|
137
|
+
inspectState,
|
|
138
|
+
configs,
|
|
139
|
+
setProvider: (value) => {
|
|
140
|
+
providerName = value;
|
|
141
|
+
},
|
|
142
|
+
appendMessage,
|
|
143
|
+
setNotice: (value) => {
|
|
144
|
+
notice = value;
|
|
145
|
+
},
|
|
146
|
+
clearMessages: () => {
|
|
147
|
+
messages.length = 0;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
appendMessage({
|
|
152
|
+
role: "system",
|
|
153
|
+
text: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
notify();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
setBusy(true, "Waiting for assistant...");
|
|
162
|
+
const emitted = await runAssistantConversationTurn({
|
|
163
|
+
productDir,
|
|
164
|
+
inspectState,
|
|
165
|
+
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
166
|
+
userMessage: trimmed,
|
|
167
|
+
provider: providerName,
|
|
168
|
+
configs,
|
|
169
|
+
onStatus(status) {
|
|
170
|
+
activeStatus = status;
|
|
171
|
+
notify();
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
for (const message of emitted) appendMessage(message);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
appendMessage({
|
|
177
|
+
role: "system",
|
|
178
|
+
text: error instanceof Error ? error.message : String(error),
|
|
179
|
+
});
|
|
180
|
+
} finally {
|
|
181
|
+
setBusy(false, null);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
subscribe(callback) {
|
|
186
|
+
listeners.add(callback);
|
|
187
|
+
return () => listeners.delete(callback);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getSnapshot() {
|
|
191
|
+
return {
|
|
192
|
+
workbench: inspectState.getSnapshot(),
|
|
193
|
+
messages: [...messages],
|
|
194
|
+
composer,
|
|
195
|
+
notice,
|
|
196
|
+
busy,
|
|
197
|
+
provider: providerName,
|
|
198
|
+
activeStatus,
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function executeSlashCommand({
|
|
205
|
+
slash,
|
|
206
|
+
productDir,
|
|
207
|
+
inspectState,
|
|
208
|
+
configs,
|
|
209
|
+
setProvider,
|
|
210
|
+
appendMessage,
|
|
211
|
+
setNotice,
|
|
212
|
+
clearMessages,
|
|
213
|
+
} = {}) {
|
|
214
|
+
if (slash.type === "help") {
|
|
215
|
+
appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (slash.type === "clear") {
|
|
219
|
+
clearMessages?.();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (slash.type === "quit") {
|
|
223
|
+
setNotice?.("Use q or Ctrl+C to quit the interactive assistant.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (slash.type === "provider") {
|
|
227
|
+
setProvider(slash.provider);
|
|
228
|
+
appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = await executeSlashTool(slash, {
|
|
233
|
+
productDir,
|
|
234
|
+
inspectState,
|
|
235
|
+
configs,
|
|
236
|
+
});
|
|
237
|
+
appendMessage({
|
|
238
|
+
role: "tool",
|
|
239
|
+
toolName: slash.type,
|
|
240
|
+
text: result.text || result.title || "Done.",
|
|
241
|
+
data: result.data || null,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function executeSlashTool(slash, context) {
|
|
246
|
+
switch (slash.type) {
|
|
247
|
+
case "pane":
|
|
248
|
+
return executeAssistantTool("set_pane", { pane: slash.pane }, context);
|
|
249
|
+
case "file":
|
|
250
|
+
return executeAssistantTool("select_file", { file: slash.file }, context);
|
|
251
|
+
case "inspect":
|
|
252
|
+
return slash.file
|
|
253
|
+
? executeAssistantTool("select_file", { file: slash.file }, context)
|
|
254
|
+
: executeAssistantTool("inspect_selection", {}, context);
|
|
255
|
+
case "service":
|
|
256
|
+
return executeAssistantTool("select_service", { service: slash.service }, context);
|
|
257
|
+
case "status":
|
|
258
|
+
return executeAssistantTool("show_status", {}, context);
|
|
259
|
+
case "discover":
|
|
260
|
+
return executeAssistantTool("discover_tests", {}, context);
|
|
261
|
+
case "doctor":
|
|
262
|
+
return executeAssistantTool("run_doctor", {}, context);
|
|
263
|
+
case "run":
|
|
264
|
+
return executeAssistantTool("run_tests", slash.options, context);
|
|
265
|
+
default:
|
|
266
|
+
throw new Error(`Unsupported slash command "${slash.type}"`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function parseSlashCommandSafe(input) {
|
|
271
|
+
try {
|
|
272
|
+
return parseSlashCommand(input);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
type: "__error__",
|
|
276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { discoverTests } from "../../discovery/index.mjs";
|
|
2
|
+
import { runDoctor } from "../../app/doctor.mjs";
|
|
3
|
+
import { resolveProductDir } from "../../config/index.mjs";
|
|
4
|
+
import { resolveRequestedFiles } from "../args.mjs";
|
|
5
|
+
import { buildDiscoveryReportLines } from "../presentation/discovery-reporter.mjs";
|
|
6
|
+
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
7
|
+
import { buildInspectPaneContent } from "../tui/detail-pane.mjs";
|
|
8
|
+
import { createAssistantRunReporter } from "./tool-run-reporter.mjs";
|
|
9
|
+
import { buildRunRequest } from "../command-helpers.mjs";
|
|
10
|
+
import * as runner from "../../runner/index.mjs";
|
|
11
|
+
|
|
12
|
+
export function listAssistantTools() {
|
|
13
|
+
return [
|
|
14
|
+
{ name: "run_tests", description: "Run testkit-managed tests with optional type/service/file filters." },
|
|
15
|
+
{ name: "select_file", description: "Focus a specific test file in the workbench." },
|
|
16
|
+
{ name: "select_service", description: "Focus a specific service in the workbench." },
|
|
17
|
+
{ name: "set_pane", description: "Switch the workbench pane: detail, artifacts, logs, or setup." },
|
|
18
|
+
{ name: "inspect_selection", description: "Render the current selection in the active pane." },
|
|
19
|
+
{ name: "discover_tests", description: "Discover managed tests and summarize them." },
|
|
20
|
+
{ name: "show_status", description: "Show the current local testkit state for the product." },
|
|
21
|
+
{ name: "run_doctor", description: "Run built-in testkit doctor checks." },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function executeAssistantTool(name, argumentsObject, context) {
|
|
26
|
+
const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
|
|
27
|
+
switch (name) {
|
|
28
|
+
case "run_tests":
|
|
29
|
+
return runTestsTool(args, context);
|
|
30
|
+
case "select_file":
|
|
31
|
+
return selectFileTool(args, context);
|
|
32
|
+
case "select_service":
|
|
33
|
+
return selectServiceTool(args, context);
|
|
34
|
+
case "set_pane":
|
|
35
|
+
return setPaneTool(args, context);
|
|
36
|
+
case "inspect_selection":
|
|
37
|
+
return inspectSelectionTool(args, context);
|
|
38
|
+
case "discover_tests":
|
|
39
|
+
return discoverTestsTool(args, context);
|
|
40
|
+
case "show_status":
|
|
41
|
+
return showStatusTool(args, context);
|
|
42
|
+
case "run_doctor":
|
|
43
|
+
return runDoctorTool(args, context);
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unknown assistant tool "${name}"`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runTestsTool(args, context) {
|
|
50
|
+
const request = await buildRunRequest(
|
|
51
|
+
{
|
|
52
|
+
dir: context.productDir,
|
|
53
|
+
service: args.service || null,
|
|
54
|
+
type: normalizeArray(args.type),
|
|
55
|
+
suite: normalizeArray(args.suite),
|
|
56
|
+
file: normalizeArray(args.file),
|
|
57
|
+
workers: args.workers == null ? null : String(args.workers),
|
|
58
|
+
"file-timeout-seconds": args.fileTimeoutSeconds == null ? null : String(args.fileTimeoutSeconds),
|
|
59
|
+
shard: args.shard || null,
|
|
60
|
+
seed: args.seed || null,
|
|
61
|
+
"write-status": Boolean(args.writeStatus),
|
|
62
|
+
"allow-partial-status": Boolean(args.allowPartialStatus),
|
|
63
|
+
"ignore-skip-rules": Boolean(args.ignoreSkipRules),
|
|
64
|
+
},
|
|
65
|
+
null,
|
|
66
|
+
context.productDir,
|
|
67
|
+
process.cwd()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
context.inspectState.resetForLive();
|
|
71
|
+
const liveReporter = createAssistantRunReporter({
|
|
72
|
+
inspectState: context.inspectState,
|
|
73
|
+
onStatus(message) {
|
|
74
|
+
context.onEvent?.({ type: "tool-status", tool: "run_tests", message });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await runner.runAll(
|
|
80
|
+
request.configs,
|
|
81
|
+
request.typeValues,
|
|
82
|
+
request.suiteSelectors,
|
|
83
|
+
{
|
|
84
|
+
...request.runOptions,
|
|
85
|
+
reporter: liveReporter,
|
|
86
|
+
},
|
|
87
|
+
request.allConfigs
|
|
88
|
+
);
|
|
89
|
+
await liveReporter.finalize;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
ok: result.ok,
|
|
93
|
+
title: "Run complete",
|
|
94
|
+
text: summarizeRunResult(context.inspectState.getSnapshot()),
|
|
95
|
+
data: result,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
liveReporter.close();
|
|
99
|
+
await liveReporter.finalize.catch(() => {});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function selectFileTool(args, context) {
|
|
105
|
+
const file = args.file || args.path || null;
|
|
106
|
+
if (!file) throw new Error("select_file requires a file argument");
|
|
107
|
+
ensureArtifactLoaded(context);
|
|
108
|
+
const artifact = context.inspectState.getSnapshot().runArtifact;
|
|
109
|
+
const subject = resolveFileSubject(artifact, file, args.service || null);
|
|
110
|
+
context.inspectState.revealFile(subject.service.name, subject.file.path);
|
|
111
|
+
return inspectSelectionTool({}, context);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function selectServiceTool(args, context) {
|
|
115
|
+
const service = args.service || args.name || null;
|
|
116
|
+
if (!service) throw new Error("select_service requires a service argument");
|
|
117
|
+
ensureArtifactLoaded(context);
|
|
118
|
+
if (!context.inspectState.revealService(service)) {
|
|
119
|
+
throw new Error(`Unknown service "${service}"`);
|
|
120
|
+
}
|
|
121
|
+
return inspectSelectionTool({}, context);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setPaneTool(args, context) {
|
|
125
|
+
const pane = args.pane || "detail";
|
|
126
|
+
context.inspectState.setPaneMode(pane);
|
|
127
|
+
return inspectSelectionTool({}, context);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function inspectSelectionTool(_args, context) {
|
|
131
|
+
ensureArtifactLoaded(context);
|
|
132
|
+
const snapshot = context.inspectState.getSnapshot();
|
|
133
|
+
const pane = buildInspectPaneContent({
|
|
134
|
+
productDir: context.productDir,
|
|
135
|
+
snapshot,
|
|
136
|
+
paneMode: snapshot.paneMode,
|
|
137
|
+
logTail: 12,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
title: pane.title,
|
|
142
|
+
text: pane.lines.join("\n"),
|
|
143
|
+
data: {
|
|
144
|
+
title: pane.title,
|
|
145
|
+
lines: pane.lines,
|
|
146
|
+
selection: snapshot.selectedEntry,
|
|
147
|
+
paneMode: snapshot.paneMode,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function discoverTestsTool(args, context) {
|
|
153
|
+
const productDir = resolveProductDir(process.cwd(), context.productDir);
|
|
154
|
+
const fileNames = resolveRequestedFiles(normalizeArray(args.file), productDir, process.cwd());
|
|
155
|
+
const result = await discoverTests({
|
|
156
|
+
dir: productDir,
|
|
157
|
+
service: args.service || null,
|
|
158
|
+
type: normalizeArray(args.type),
|
|
159
|
+
suite: normalizeArray(args.suite),
|
|
160
|
+
file: fileNames,
|
|
161
|
+
runnableOnly: Boolean(args.runnableOnly),
|
|
162
|
+
diagnostics: args.strict ? "error" : "report",
|
|
163
|
+
});
|
|
164
|
+
const lines = buildDiscoveryReportLines(result, { outputMode: args.outputMode || "compact" });
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
title: "Discovery",
|
|
168
|
+
text: lines.join("\n"),
|
|
169
|
+
data: result,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function showStatusTool(_args, context) {
|
|
174
|
+
const { output } = captureConsoleOutput(() => runner.showStatus(context.configs[0]));
|
|
175
|
+
const lines = output.length > 0 ? output : ["No state"];
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
title: "Status",
|
|
179
|
+
text: lines.join("\n"),
|
|
180
|
+
data: { lines },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runDoctorTool(args, context) {
|
|
185
|
+
const result = await runDoctor({
|
|
186
|
+
dir: context.productDir,
|
|
187
|
+
typecheck: args.typecheck ?? true,
|
|
188
|
+
});
|
|
189
|
+
const lines = [
|
|
190
|
+
`testkit doctor ${result.ok ? "passed" : "failed"} for ${result.productDir}`,
|
|
191
|
+
...result.checks.map((check) => `${String(check.level || "").toUpperCase()} ${check.code} ${check.message}`),
|
|
192
|
+
];
|
|
193
|
+
return {
|
|
194
|
+
ok: result.ok,
|
|
195
|
+
title: "Doctor",
|
|
196
|
+
text: lines.join("\n"),
|
|
197
|
+
data: result,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ensureArtifactLoaded(context) {
|
|
202
|
+
const snapshot = context.inspectState.getSnapshot();
|
|
203
|
+
if (snapshot.runArtifact) return snapshot.runArtifact;
|
|
204
|
+
try {
|
|
205
|
+
context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
206
|
+
} catch {
|
|
207
|
+
context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
208
|
+
}
|
|
209
|
+
return context.inspectState.getSnapshot().runArtifact;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeArray(value) {
|
|
213
|
+
if (value == null) return [];
|
|
214
|
+
if (Array.isArray(value)) return value.filter(Boolean);
|
|
215
|
+
return [value].filter(Boolean);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function summarizeRunResult(snapshot) {
|
|
219
|
+
const rows = snapshot?.summaryData?.rows || [];
|
|
220
|
+
if (rows.length === 0) return "Run finished.";
|
|
221
|
+
return rows.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function captureConsoleOutput(fn) {
|
|
225
|
+
const output = [];
|
|
226
|
+
const originalLog = console.log;
|
|
227
|
+
console.log = (...args) => {
|
|
228
|
+
output.push(args.map((value) => String(value)).join(" "));
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
const result = fn();
|
|
232
|
+
return { result, output };
|
|
233
|
+
} finally {
|
|
234
|
+
console.log = originalLog;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyReporterPlans,
|
|
3
|
+
applyReporterRunSummary,
|
|
4
|
+
applyReporterTaskFinished,
|
|
5
|
+
applyReporterTaskStarted,
|
|
6
|
+
} from "../tui/inspect-live-adapter.mjs";
|
|
7
|
+
import { suiteSelectionType } from "../../runner/suite-selection.mjs";
|
|
8
|
+
|
|
9
|
+
export function createAssistantRunReporter({ inspectState, onStatus } = {}) {
|
|
10
|
+
return {
|
|
11
|
+
reporter: {
|
|
12
|
+
outputMode: "compact",
|
|
13
|
+
|
|
14
|
+
setServicePlans(plans) {
|
|
15
|
+
applyReporterPlans(inspectState, plans);
|
|
16
|
+
onStatus?.("Planned run.");
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
setTotalFileCount(count) {
|
|
20
|
+
inspectState.setTotalFileCount(count);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
setRegressionCatalog(document) {
|
|
24
|
+
inspectState.setRegressionCatalog(document);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
serviceSkipped(config, reason) {
|
|
28
|
+
inspectState.markServiceSkipped(config.name, reason);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
plannedSkip(entry) {
|
|
32
|
+
inspectState.markPlannedSkip(entry);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
taskStarted(task) {
|
|
36
|
+
const suiteKey = `${task.displayType || suiteSelectionType(task.type, task.framework)}:${task.suiteName}`;
|
|
37
|
+
applyReporterTaskStarted(inspectState, task, suiteKey);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
taskFinished(task, outcome) {
|
|
41
|
+
applyReporterTaskFinished(inspectState, task, outcome);
|
|
42
|
+
if (outcome.failed) {
|
|
43
|
+
onStatus?.(`Failed ${task.file}`);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
runtimeError(task, message) {
|
|
48
|
+
inspectState.markRuntimeError(task, message);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
setupOperationFinished() {},
|
|
52
|
+
|
|
53
|
+
phaseStarted(label) {
|
|
54
|
+
inspectState.setPhase(label);
|
|
55
|
+
onStatus?.(label);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
toolchainResolved() {},
|
|
59
|
+
localServiceStarting() {},
|
|
60
|
+
writeLine() {},
|
|
61
|
+
writeDebugLine() {},
|
|
62
|
+
telemetry() {},
|
|
63
|
+
|
|
64
|
+
runSummary(results, durationMs, regressionReport) {
|
|
65
|
+
applyReporterRunSummary(inspectState, results, durationMs, regressionReport);
|
|
66
|
+
onStatus?.("Run finished.");
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
error(message) {
|
|
70
|
+
onStatus?.(String(message));
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
finalize: Promise.resolve(),
|
|
75
|
+
|
|
76
|
+
close() {
|
|
77
|
+
// No long-lived UI resources to close.
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -19,7 +19,7 @@ export const sharedFlags = {
|
|
|
19
19
|
description: "Explicit product directory",
|
|
20
20
|
}),
|
|
21
21
|
service: Flags.string({
|
|
22
|
-
description: "
|
|
22
|
+
description: "Limit the operation or assistant context to one service",
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -79,18 +79,8 @@ export async function resolveConfigsForCommand(flags) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export async function executeRunCommand(command, flags, positionalType = null) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const fileTimeoutSeconds =
|
|
85
|
-
flags["file-timeout-seconds"] == null
|
|
86
|
-
? null
|
|
87
|
-
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
88
|
-
const shard = parseShardOption(flags.shard);
|
|
89
|
-
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
90
|
-
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
91
|
-
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
92
|
-
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
93
|
-
const fileNames = resolveRequestedFiles(rawFileNames, productDir, process.cwd());
|
|
82
|
+
const request = await buildRunRequest(flags, positionalType, process.cwd(), process.cwd());
|
|
83
|
+
const { allConfigs, configs, typeValues, suiteSelectors, productDir } = request;
|
|
94
84
|
const outputMode = command.jsonEnabled()
|
|
95
85
|
? "json"
|
|
96
86
|
: flags.debug
|
|
@@ -122,18 +112,8 @@ export async function executeRunCommand(command, flags, positionalType = null) {
|
|
|
122
112
|
typeValues,
|
|
123
113
|
suiteSelectors,
|
|
124
114
|
{
|
|
125
|
-
...flags,
|
|
126
|
-
typeValues,
|
|
127
|
-
fileNames,
|
|
128
|
-
workers,
|
|
129
|
-
fileTimeoutSeconds,
|
|
130
|
-
shard,
|
|
131
|
-
scenarioSeed: flags.seed || null,
|
|
132
|
-
serviceFilter: flags.service || null,
|
|
133
115
|
reporter,
|
|
134
|
-
|
|
135
|
-
allowPartialStatus: flags["allow-partial-status"],
|
|
136
|
-
ignoreSkipRules: flags["ignore-skip-rules"],
|
|
116
|
+
...request.runOptions,
|
|
137
117
|
},
|
|
138
118
|
allConfigs
|
|
139
119
|
);
|
|
@@ -149,6 +129,42 @@ export async function executeRunCommand(command, flags, positionalType = null) {
|
|
|
149
129
|
}
|
|
150
130
|
}
|
|
151
131
|
|
|
132
|
+
export async function buildRunRequest(flags, positionalType = null, cwd = process.cwd(), invocationCwd = process.cwd()) {
|
|
133
|
+
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
134
|
+
const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
|
|
135
|
+
const fileTimeoutSeconds =
|
|
136
|
+
flags["file-timeout-seconds"] == null
|
|
137
|
+
? null
|
|
138
|
+
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
139
|
+
const shard = parseShardOption(flags.shard);
|
|
140
|
+
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
141
|
+
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
142
|
+
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
143
|
+
const productDir = allConfigs[0]?.productDir || cwd;
|
|
144
|
+
const fileNames = resolveRequestedFiles(rawFileNames, productDir, invocationCwd);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
allConfigs,
|
|
148
|
+
configs,
|
|
149
|
+
productDir,
|
|
150
|
+
typeValues,
|
|
151
|
+
suiteSelectors,
|
|
152
|
+
runOptions: {
|
|
153
|
+
...flags,
|
|
154
|
+
typeValues,
|
|
155
|
+
fileNames,
|
|
156
|
+
workers,
|
|
157
|
+
fileTimeoutSeconds,
|
|
158
|
+
shard,
|
|
159
|
+
scenarioSeed: flags.seed || null,
|
|
160
|
+
serviceFilter: flags.service || null,
|
|
161
|
+
writeStatus: flags["write-status"],
|
|
162
|
+
allowPartialStatus: flags["allow-partial-status"],
|
|
163
|
+
ignoreSkipRules: flags["ignore-skip-rules"],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
export async function runStatusLike(commandName, flags) {
|
|
153
169
|
const { allConfigs, configs } = await resolveConfigsForCommand(flags);
|
|
154
170
|
|