@elench/testkit 0.1.86 → 0.1.87
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 +6 -7
- package/lib/cli/commands/inspect.mjs +124 -0
- package/lib/cli/entrypoint.mjs +2 -4
- package/lib/cli/presentation/tree-reporter.mjs +34 -28
- package/lib/cli/tui/detail-pane.mjs +161 -0
- package/lib/cli/tui/filter-bar.mjs +12 -0
- package/lib/cli/tui/fuzzy-match.mjs +106 -0
- package/lib/cli/tui/inspect-app.mjs +306 -0
- package/lib/cli/tui/inspect-artifact-adapter.mjs +3 -0
- package/lib/cli/tui/inspect-live-adapter.mjs +15 -0
- package/lib/cli/tui/inspect-model.mjs +817 -0
- package/lib/cli/tui/inspect-state.mjs +321 -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 +5 -5
- package/lib/cli/commands/artifacts.mjs +0 -45
- package/lib/cli/commands/logs.mjs +0 -47
- package/lib/cli/commands/show.mjs +0 -47
- package/lib/cli/commands/watch.mjs +0 -23
- package/lib/cli/tui/run-app.mjs +0 -1
- package/lib/cli/tui/run-session-app.mjs +0 -432
- package/lib/cli/tui/run-session-state.mjs +0 -505
- package/lib/cli/tui/run-tree-state.mjs +0 -1
- package/lib/cli/tui/watch-app.mjs +0 -220
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React, { createElement, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Box, Text, useAnimation, useApp, useInput } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { formatDuration } from "../../runner/formatting.mjs";
|
|
5
|
+
import {
|
|
6
|
+
bold,
|
|
7
|
+
colorService,
|
|
8
|
+
colorTypeBadge,
|
|
9
|
+
dim,
|
|
10
|
+
green,
|
|
11
|
+
red,
|
|
12
|
+
yellow,
|
|
13
|
+
} from "../presentation/colors.mjs";
|
|
14
|
+
import { renderSummaryBox } from "../presentation/summary-box.mjs";
|
|
15
|
+
import { getTerminalWidth } from "../presentation/terminal-layout.mjs";
|
|
16
|
+
import { defaultInvestigationMessage } from "../agents/prompt-builder.mjs";
|
|
17
|
+
import { applyHighlight } from "./fuzzy-match.mjs";
|
|
18
|
+
import { buildInspectPaneContent } from "./detail-pane.mjs";
|
|
19
|
+
import { FilterBar } from "./filter-bar.mjs";
|
|
20
|
+
|
|
21
|
+
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
22
|
+
|
|
23
|
+
export function InspectApp({
|
|
24
|
+
inspectState,
|
|
25
|
+
stdout,
|
|
26
|
+
productDir,
|
|
27
|
+
onInvestigate,
|
|
28
|
+
onRequestClose,
|
|
29
|
+
onCancelInvestigation,
|
|
30
|
+
} = {}) {
|
|
31
|
+
const { exit } = useApp();
|
|
32
|
+
const [snapshot, setSnapshot] = useState(() => inspectState.getSnapshot());
|
|
33
|
+
const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
|
|
34
|
+
const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const unsubscribe = inspectState.subscribe(() => {
|
|
38
|
+
setSnapshot(inspectState.getSnapshot());
|
|
39
|
+
});
|
|
40
|
+
return unsubscribe;
|
|
41
|
+
}, [inspectState]);
|
|
42
|
+
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (snapshot.mode === "investigating") {
|
|
45
|
+
if (input === "q") {
|
|
46
|
+
(onRequestClose || exit)();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (input === "t") {
|
|
50
|
+
inspectState.toggleInvestigationViewMode();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (input === "x") {
|
|
54
|
+
onCancelInvestigation?.();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (input === "b" && snapshot.agentSession?.status !== "running" && snapshot.agentSession?.status !== "starting") {
|
|
58
|
+
inspectState.returnToSummary();
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!snapshot.finished) {
|
|
64
|
+
if (input === "q") {
|
|
65
|
+
inspectState.setNotice("Run is still in progress. Wait for completion before closing.");
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (snapshot.filter.active) {
|
|
71
|
+
if (key.escape) {
|
|
72
|
+
inspectState.deactivateFilter();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.return) return;
|
|
76
|
+
if (key.downArrow || input === "j") {
|
|
77
|
+
inspectState.moveCursorDown();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.upArrow || input === "k") {
|
|
81
|
+
inspectState.moveCursorUp();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.backspace || key.delete) {
|
|
85
|
+
inspectState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (isPrintableInput(input, key)) {
|
|
89
|
+
inspectState.updateFilterQuery(`${snapshot.filter.query}${input}`);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (input === "q") {
|
|
95
|
+
(onRequestClose || exit)();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (input === "/") {
|
|
99
|
+
inspectState.activateFilter();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.downArrow || input === "j") {
|
|
103
|
+
inspectState.moveCursorDown();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.upArrow || input === "k") {
|
|
107
|
+
inspectState.moveCursorUp();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.return) {
|
|
111
|
+
inspectState.toggleExpand();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (key.tab) {
|
|
115
|
+
inspectState.cyclePaneMode();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if ((input === "y" || input === "i") && snapshot.selectedFailure) {
|
|
119
|
+
onInvestigate?.({
|
|
120
|
+
provider: "auto",
|
|
121
|
+
userMessage: defaultInvestigationMessage(),
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (input === "c" && snapshot.selectedFailure) {
|
|
126
|
+
onInvestigate?.({ provider: "claude", userMessage: defaultInvestigationMessage() });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (input === "o" && snapshot.selectedFailure) {
|
|
130
|
+
onInvestigate?.({ provider: "codex", userMessage: defaultInvestigationMessage() });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
135
|
+
const leftWidth = Math.max(42, Math.floor(terminalWidth * 0.52));
|
|
136
|
+
const rightWidth = Math.max(28, terminalWidth - leftWidth - 1);
|
|
137
|
+
const visibleTreeEntries = useMemo(
|
|
138
|
+
() => buildTreeViewport(snapshot.visibleEntries, snapshot.selectedEntryId, 17),
|
|
139
|
+
[snapshot.visibleEntries, snapshot.selectedEntryId]
|
|
140
|
+
);
|
|
141
|
+
const paneContent = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
snapshot.mode === "investigating"
|
|
144
|
+
? { title: "Investigation", lines: buildInvestigationLines(snapshot) }
|
|
145
|
+
: buildInspectPaneContent({
|
|
146
|
+
productDir,
|
|
147
|
+
snapshot,
|
|
148
|
+
paneMode: snapshot.paneMode,
|
|
149
|
+
}),
|
|
150
|
+
[productDir, snapshot]
|
|
151
|
+
);
|
|
152
|
+
const summaryLines = snapshot.finished && snapshot.summaryData
|
|
153
|
+
? renderSummaryBox(snapshot.summaryData.rows, { stdout })
|
|
154
|
+
: [];
|
|
155
|
+
|
|
156
|
+
return createElement(
|
|
157
|
+
Box,
|
|
158
|
+
{ flexDirection: "column" },
|
|
159
|
+
createElement(Text, { key: "header" }, dim(buildHeaderText(snapshot))),
|
|
160
|
+
snapshot.notice ? createElement(Text, { key: "notice" }, yellow(snapshot.notice)) : null,
|
|
161
|
+
createElement(
|
|
162
|
+
Box,
|
|
163
|
+
{ key: "main", marginTop: 1, flexDirection: "row" },
|
|
164
|
+
createElement(Box, { width: leftWidth, flexDirection: "column", paddingRight: 1 }, ...visibleTreeEntries.map(renderTreeLine.bind(null, snapshot, spinnerFrame))),
|
|
165
|
+
createElement(
|
|
166
|
+
Box,
|
|
167
|
+
{ width: rightWidth, flexDirection: "column", paddingLeft: 1 },
|
|
168
|
+
createElement(Text, { key: "pane-title" }, bold(paneContent.title)),
|
|
169
|
+
createElement(Text, { key: "pane-gap" }, ""),
|
|
170
|
+
...paneContent.lines.slice(0, 34).map((line, index) => createElement(Text, { key: `pane-${index}` }, line))
|
|
171
|
+
)
|
|
172
|
+
),
|
|
173
|
+
snapshot.filter.active ? createElement(Text, { key: "filter-gap" }, "") : null,
|
|
174
|
+
snapshot.filter.active ? createElement(FilterBar, { key: "filter-bar", filter: snapshot.filter }) : null,
|
|
175
|
+
summaryLines.length > 0 ? createElement(Text, { key: "summary-gap" }, "") : null,
|
|
176
|
+
...summaryLines.map((line, index) => createElement(Text, { key: `summary-${index}` }, line)),
|
|
177
|
+
createElement(Text, { key: "footer-gap" }, ""),
|
|
178
|
+
createElement(Text, { key: "footer" }, dim(buildFooterText(snapshot)))
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function buildHeaderText(snapshot) {
|
|
183
|
+
const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
|
|
184
|
+
const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
|
|
185
|
+
const sourceText = snapshot.dataSource === "artifact" ? "artifact inspect" : snapshot.finished ? "live summary" : "live run";
|
|
186
|
+
const filterText = snapshot.filter.active ? `filter ${snapshot.filter.count}` : null;
|
|
187
|
+
return [progressText, phaseText, sourceText, filterText].filter(Boolean).join(" · ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function buildFooterText(snapshot) {
|
|
191
|
+
if (snapshot.mode === "investigating") {
|
|
192
|
+
if (snapshot.agentSession?.status === "running" || snapshot.agentSession?.status === "starting") {
|
|
193
|
+
return "t transcript · x cancel investigation · q quit";
|
|
194
|
+
}
|
|
195
|
+
return "t transcript · b back · q quit";
|
|
196
|
+
}
|
|
197
|
+
if (!snapshot.finished) return "Run in progress";
|
|
198
|
+
if (snapshot.filter.active) {
|
|
199
|
+
return "type to filter · ↑/↓ move · Esc clear filter · q quit";
|
|
200
|
+
}
|
|
201
|
+
const inspectKeys = "↑/↓ move · Enter collapse/expand · Tab cycle pane · / filter";
|
|
202
|
+
if (!snapshot.selectedFailure) return `${inspectKeys} · q quit`;
|
|
203
|
+
return `${inspectKeys} · y investigate · c Claude · o Codex · q quit`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function formatAgentEntry(entry) {
|
|
207
|
+
if (!entry) return "";
|
|
208
|
+
if (entry.kind === "status") return `[status] ${entry.text}`;
|
|
209
|
+
if (entry.kind === "tool") return `[tool] ${entry.text}`;
|
|
210
|
+
if (entry.kind === "error") return `[error] ${entry.text}`;
|
|
211
|
+
return entry.text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function formatTimelineEntry(entry) {
|
|
215
|
+
if (!entry) return "";
|
|
216
|
+
if (entry.kind === "result") return entry.summary || "";
|
|
217
|
+
if (entry.kind === "notice") return `${entry.severity || "info"}: ${entry.message}`;
|
|
218
|
+
return entry.message || entry.summary || "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildInvestigationLines(snapshot) {
|
|
222
|
+
const transcriptEntries = snapshot.agentSession?.transcriptEntries || [];
|
|
223
|
+
const timeline = snapshot.agentSession?.timeline || [];
|
|
224
|
+
const viewMode = snapshot.agentSession?.viewMode || "summary";
|
|
225
|
+
const status = snapshot.agentSession?.status || "idle";
|
|
226
|
+
const lines = [
|
|
227
|
+
`Status: ${status}`,
|
|
228
|
+
`Phase: ${snapshot.agentSession?.activePhase || "planning"}`,
|
|
229
|
+
"",
|
|
230
|
+
];
|
|
231
|
+
if (viewMode === "transcript") {
|
|
232
|
+
for (const entry of transcriptEntries.slice(-28)) lines.push(formatAgentEntry(entry));
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
for (const entry of timeline.slice(-16)) lines.push(formatTimelineEntry(entry));
|
|
236
|
+
if (timeline.length === 0 && snapshot.agentSession?.finalText) {
|
|
237
|
+
lines.push(snapshot.agentSession.finalText);
|
|
238
|
+
}
|
|
239
|
+
return lines;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildTreeViewport(entries, selectedEntryId, radius) {
|
|
243
|
+
if (entries.length <= radius * 2 + 1) return entries;
|
|
244
|
+
const selectedIndex = Math.max(0, entries.findIndex((entry) => entry.id === selectedEntryId));
|
|
245
|
+
const start = Math.max(0, selectedIndex - radius);
|
|
246
|
+
const end = Math.min(entries.length, selectedIndex + radius + 1);
|
|
247
|
+
return entries.slice(start, end);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderTreeLine(snapshot, spinnerFrame, entry) {
|
|
251
|
+
const selected = entry.id === snapshot.selectedEntryId;
|
|
252
|
+
const pointer = selected ? `${bold(">")} ` : " ";
|
|
253
|
+
const indent = " ".repeat(entry.depth);
|
|
254
|
+
const rawLabel = entry.label;
|
|
255
|
+
const match = entry.match;
|
|
256
|
+
const highlightedLabel = match?.field === "label"
|
|
257
|
+
? applyHighlight(rawLabel, match.positions, bold)
|
|
258
|
+
: rawLabel;
|
|
259
|
+
const renderedLabel = decorateEntryLabel(entry, highlightedLabel, match);
|
|
260
|
+
const icon = entryIcon(entry, spinnerFrame);
|
|
261
|
+
const line = `${pointer}${indent}${icon ? `${icon} ` : ""}${renderedLabel}${entrySuffix(entry)}`;
|
|
262
|
+
return createElement(Text, { key: entry.id }, line);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function entryIcon(entry, spinnerFrame) {
|
|
266
|
+
if (entry.kind === "service" || entry.kind === "type") return "";
|
|
267
|
+
if (entry.kind === "suite") return entry.collapsed ? yellow("+") : yellow("-");
|
|
268
|
+
if (entry.status === "failed") return red(figures.cross);
|
|
269
|
+
if (entry.status === "passed") return green(figures.tick);
|
|
270
|
+
if (entry.status === "skipped") return yellow(figures.arrowDown);
|
|
271
|
+
if (entry.status === "running") return green(spinnerFrame);
|
|
272
|
+
if (entry.status === "not_run") return yellow("!");
|
|
273
|
+
return dim("·");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function decorateEntryLabel(entry, label, match) {
|
|
277
|
+
let rendered = label;
|
|
278
|
+
if (entry.kind === "service") rendered = colorService(label);
|
|
279
|
+
else if (entry.kind === "type") rendered = colorTypeBadge(label.toUpperCase());
|
|
280
|
+
else if (entry.kind === "suite") rendered = bold(label);
|
|
281
|
+
|
|
282
|
+
if (match?.field === "path" && entry.filePath) {
|
|
283
|
+
rendered += ` ${dim(`(${applyHighlight(entry.filePath, match.positions, bold)})`)}`;
|
|
284
|
+
}
|
|
285
|
+
return rendered;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function entrySuffix(entry) {
|
|
289
|
+
if (entry.kind === "file") {
|
|
290
|
+
const duration = entry.durationMs != null ? ` ${dim(formatDuration(entry.durationMs))}` : "";
|
|
291
|
+
return duration;
|
|
292
|
+
}
|
|
293
|
+
if (entry.summary?.total > 0) {
|
|
294
|
+
return dim(` (${entry.summary.total} files)`);
|
|
295
|
+
}
|
|
296
|
+
if (entry.skipReason) {
|
|
297
|
+
return ` ${dim(entry.skipReason)}`;
|
|
298
|
+
}
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isPrintableInput(input, key) {
|
|
303
|
+
if (!input) return false;
|
|
304
|
+
if (key.ctrl || key.meta || key.escape || key.return || key.tab) return false;
|
|
305
|
+
return input >= " ";
|
|
306
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function applyReporterPlans(inspectState, plans) {
|
|
2
|
+
inspectState.initFromPlans(plans);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function applyReporterTaskStarted(inspectState, task, suiteKey) {
|
|
6
|
+
inspectState.markFileRunning(task.serviceName, suiteKey, task.file);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function applyReporterTaskFinished(inspectState, task, outcome) {
|
|
10
|
+
inspectState.markFileFinished(task, outcome);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function applyReporterRunSummary(inspectState, results, durationMs, regressionReport) {
|
|
14
|
+
inspectState.finish(results, durationMs, regressionReport);
|
|
15
|
+
}
|