@mintlify/cli 4.0.1465 → 4.0.1467
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/__test__/mintTestFilePreview.test.ts +35 -0
- package/__test__/mintTestOutput.test.ts +194 -0
- package/__test__/runHistory.test.ts +164 -0
- package/bin/agent-harness/agentPreflight.js +24 -0
- package/bin/agent-harness/buildTaskPrompt.js +17 -7
- package/bin/agent-harness/countReport.js +10 -0
- package/bin/agent-harness/executeCodeBlocks.js +53 -10
- package/bin/agent-harness/generateTestCode.js +58 -43
- package/bin/agent-harness/index.js +14 -5
- package/bin/agent-harness/manifest.js +113 -0
- package/bin/agent-harness/runHistory.js +252 -0
- package/bin/agent-harness/setupFolders.js +20 -6
- package/bin/agent-harness/tasks/checkTestability.js +7 -5
- package/bin/agent-harness/types.js +33 -2
- package/bin/constants.js +3 -3
- package/bin/mintTest.js +31 -9
- package/bin/mintTestFilePreview.js +39 -0
- package/bin/mintTestText.js +31 -0
- package/bin/mintTestUi.js +295 -105
- package/bin/status.js +26 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +15 -9
- package/src/agent-harness/agentPreflight.ts +13 -0
- package/src/agent-harness/buildTaskPrompt.ts +18 -7
- package/src/agent-harness/countReport.ts +17 -0
- package/src/agent-harness/executeCodeBlocks.ts +67 -1
- package/src/agent-harness/generateTestCode.ts +81 -51
- package/src/agent-harness/index.ts +20 -5
- package/src/agent-harness/manifest.ts +116 -0
- package/src/agent-harness/runHistory.ts +250 -0
- package/src/agent-harness/setupFolders.ts +19 -5
- package/src/agent-harness/tasks/checkTestability.ts +15 -6
- package/src/agent-harness/types.ts +81 -5
- package/src/constants.ts +6 -2
- package/src/mintTest.tsx +34 -8
- package/src/mintTestFilePreview.ts +32 -0
- package/src/mintTestText.ts +35 -0
- package/src/mintTestUi.tsx +586 -224
- package/src/status.tsx +24 -0
package/src/mintTestUi.tsx
CHANGED
|
@@ -12,20 +12,21 @@ import {
|
|
|
12
12
|
type HarnessAgent,
|
|
13
13
|
type RunSave,
|
|
14
14
|
type TaskPhase,
|
|
15
|
-
type TaskResult,
|
|
16
15
|
type TaskUpdate,
|
|
17
16
|
} from './agent-harness/index.js';
|
|
17
|
+
import { readFilePreview } from './mintTestFilePreview.js';
|
|
18
|
+
import { firstLine, footerSegments, taskProblemLabel } from './mintTestText.js';
|
|
18
19
|
import { toggleScopeSelection, visibleScopeRows, type ScopeTreeRow } from './mintTestTree.js';
|
|
19
20
|
|
|
20
|
-
type Screen = 'resume' | 'harnesses' | 'scope' | 'running' | 'report' | 'error';
|
|
21
|
+
type Screen = 'resume' | 'existing' | 'harnesses' | 'scope' | 'running' | 'report' | 'error';
|
|
21
22
|
|
|
22
23
|
interface MintTestUiProps {
|
|
23
24
|
targetPath: string;
|
|
24
25
|
docsRoot: string;
|
|
25
26
|
pages: DocsPage[];
|
|
26
27
|
autoSelectedFiles: string[];
|
|
27
|
-
totalPages: number;
|
|
28
28
|
save: RunSave | null;
|
|
29
|
+
lastReport: CodeTestReport | null;
|
|
29
30
|
onFinish: (exitCode: number) => void;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -40,20 +41,6 @@ const HARNESSES: Array<{ agent: HarnessAgent; model: string; label: string }> =
|
|
|
40
41
|
{ agent: 'codex', model: 'claude-opus-4-8', label: 'Codex' },
|
|
41
42
|
];
|
|
42
43
|
|
|
43
|
-
export function countReport(tasks: TaskResult[]): {
|
|
44
|
-
passed: number;
|
|
45
|
-
failed: number;
|
|
46
|
-
cancelled: number;
|
|
47
|
-
agentErrors: number;
|
|
48
|
-
} {
|
|
49
|
-
const counts = { passed: 0, failed: 0, cancelled: 0, agentErrors: 0 };
|
|
50
|
-
for (const task of tasks) {
|
|
51
|
-
if (task.status === 'agent_error') counts.agentErrors++;
|
|
52
|
-
else counts[task.status]++;
|
|
53
|
-
}
|
|
54
|
-
return counts;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
const STEPS: StepDefinition[] = [
|
|
58
45
|
{ id: 'harnesses', label: 'Harnesses' },
|
|
59
46
|
{ id: 'scope', label: 'Scope' },
|
|
@@ -112,7 +99,7 @@ function stepState(
|
|
|
112
99
|
): 'pending' | 'running' | 'complete' | 'error' {
|
|
113
100
|
const order = ['harnesses', 'scope', 'check', 'generate', 'run', 'report'];
|
|
114
101
|
const screenStep =
|
|
115
|
-
screen === 'resume'
|
|
102
|
+
screen === 'resume' || screen === 'existing'
|
|
116
103
|
? 'harnesses'
|
|
117
104
|
: screen === 'running' || screen === 'error'
|
|
118
105
|
? activeRunStage
|
|
@@ -133,6 +120,11 @@ function StepGlyph({ state }: { state: ReturnType<typeof stepState> }) {
|
|
|
133
120
|
}
|
|
134
121
|
|
|
135
122
|
const RESUME_OPTIONS = ['Resume', 'Start fresh'] as const;
|
|
123
|
+
const EXISTING_RUN_OPTIONS = [
|
|
124
|
+
'Update tests',
|
|
125
|
+
'Review last test run',
|
|
126
|
+
'Start a brand new test',
|
|
127
|
+
] as const;
|
|
136
128
|
const EMPTY_SELECTION: ReadonlySet<string> = new Set();
|
|
137
129
|
|
|
138
130
|
function ResumeScreen({ save, cursor }: { save: RunSave; cursor: number }) {
|
|
@@ -159,11 +151,47 @@ function ResumeScreen({ save, cursor }: { save: RunSave; cursor: number }) {
|
|
|
159
151
|
);
|
|
160
152
|
}
|
|
161
153
|
|
|
154
|
+
function ExistingRunScreen({ report, cursor }: { report: CodeTestReport; cursor: number }) {
|
|
155
|
+
return (
|
|
156
|
+
<Box flexDirection="column">
|
|
157
|
+
<Text bold>Mint test has already run in this directory.</Text>
|
|
158
|
+
<Box flexDirection="column" marginTop={1}>
|
|
159
|
+
<Text>
|
|
160
|
+
Last run <Text color="cyan">{report.runId}</Text> ·{' '}
|
|
161
|
+
<Text
|
|
162
|
+
color={
|
|
163
|
+
report.status === 'passed'
|
|
164
|
+
? 'green'
|
|
165
|
+
: report.status === 'completed'
|
|
166
|
+
? 'cyan'
|
|
167
|
+
: report.status === 'cancelled'
|
|
168
|
+
? 'yellow'
|
|
169
|
+
: 'red'
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
{report.status}
|
|
173
|
+
</Text>
|
|
174
|
+
</Text>
|
|
175
|
+
<Text dimColor>
|
|
176
|
+
{report.selectedFiles.length} {report.selectedFiles.length === 1 ? 'page' : 'pages'} ·{' '}
|
|
177
|
+
{report.model} · {new Date(report.completedAt).toLocaleString()}
|
|
178
|
+
</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
<Box flexDirection="column" marginTop={1}>
|
|
181
|
+
{EXISTING_RUN_OPTIONS.map((option, index) => (
|
|
182
|
+
<Text key={option} color={cursor === index ? 'cyan' : undefined}>
|
|
183
|
+
{cursor === index ? '›' : ' '} {option}
|
|
184
|
+
</Text>
|
|
185
|
+
))}
|
|
186
|
+
</Box>
|
|
187
|
+
</Box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
function HarnessScreen({ cursor }: { cursor: number }) {
|
|
163
192
|
return (
|
|
164
193
|
<Box flexDirection="column">
|
|
165
|
-
<Text bold>
|
|
166
|
-
<Text dimColor>The selected harness tests every page in the chosen scope.</Text>
|
|
194
|
+
<Text bold>Select coding harness?</Text>
|
|
167
195
|
<Box flexDirection="column" marginTop={1}>
|
|
168
196
|
{HARNESSES.map((harness, index) => (
|
|
169
197
|
<Text key={harness.label} color={cursor === index ? 'cyan' : undefined}>
|
|
@@ -179,20 +207,10 @@ function ScopeScreen({
|
|
|
179
207
|
rows,
|
|
180
208
|
cursor,
|
|
181
209
|
height,
|
|
182
|
-
selectedCount,
|
|
183
|
-
selectedBlocks,
|
|
184
|
-
testablePages,
|
|
185
|
-
totalPages,
|
|
186
|
-
outputDirectory,
|
|
187
210
|
}: {
|
|
188
211
|
rows: ReturnType<typeof visibleScopeRows>;
|
|
189
212
|
cursor: number;
|
|
190
213
|
height: number;
|
|
191
|
-
selectedCount: number;
|
|
192
|
-
selectedBlocks: number;
|
|
193
|
-
testablePages: number;
|
|
194
|
-
totalPages: number;
|
|
195
|
-
outputDirectory: string;
|
|
196
214
|
}) {
|
|
197
215
|
const viewport = Math.max(4, height - 18);
|
|
198
216
|
const start = Math.max(0, Math.min(cursor - Math.floor(viewport / 2), rows.length - viewport));
|
|
@@ -200,30 +218,14 @@ function ScopeScreen({
|
|
|
200
218
|
|
|
201
219
|
return (
|
|
202
220
|
<Box flexDirection="column">
|
|
203
|
-
<Box
|
|
204
|
-
<
|
|
205
|
-
<Text
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
</Text>
|
|
210
|
-
<Text>
|
|
211
|
-
<Text color="cyan" bold>
|
|
212
|
-
{selectedCount}
|
|
213
|
-
</Text>{' '}
|
|
214
|
-
{selectedCount === 1 ? 'page' : 'pages'} ·{' '}
|
|
215
|
-
<Text color="cyan" bold>
|
|
216
|
-
{selectedBlocks}
|
|
217
|
-
</Text>{' '}
|
|
218
|
-
{selectedBlocks === 1 ? 'code block' : 'code blocks'} will be tested
|
|
219
|
-
</Text>
|
|
220
|
-
<Text dimColor wrap="truncate-end">
|
|
221
|
-
Tests generated at {outputDirectory}
|
|
221
|
+
<Box flexDirection="column">
|
|
222
|
+
<Box marginTop={1} marginRight={2} flexDirection="column">
|
|
223
|
+
<Text bold>Select pages to test</Text>
|
|
224
|
+
</Box>
|
|
225
|
+
<Text bold color="green">
|
|
226
|
+
enter to confirm
|
|
222
227
|
</Text>
|
|
223
228
|
</Box>
|
|
224
|
-
<Box marginTop={1} flexDirection="column">
|
|
225
|
-
<Text bold>Choose the docs to test</Text>
|
|
226
|
-
</Box>
|
|
227
229
|
<Text dimColor>Folders select or clear every testable page beneath them.</Text>
|
|
228
230
|
<Box flexDirection="column" marginTop={1}>
|
|
229
231
|
{visible.map((row, visibleIndex) => {
|
|
@@ -246,19 +248,6 @@ function ScopeScreen({
|
|
|
246
248
|
);
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
const PHASE_LABELS: Record<TaskPhase, string> = {
|
|
250
|
-
checking: 'validating code can be tested',
|
|
251
|
-
checked: 'code can be tested',
|
|
252
|
-
not_testable: 'no testable code',
|
|
253
|
-
generating: 'generating test code',
|
|
254
|
-
generated: 'test generated',
|
|
255
|
-
running: 'running tests',
|
|
256
|
-
passed: 'passed',
|
|
257
|
-
failed: 'tests failed',
|
|
258
|
-
agent_error: 'test could not be generated',
|
|
259
|
-
cancelled: 'cancelled',
|
|
260
|
-
};
|
|
261
|
-
|
|
262
251
|
const ACTIVE_PHASES = new Set<TaskPhase>(['checking', 'generating', 'running']);
|
|
263
252
|
const DONE_PHASES = new Set<TaskPhase>([
|
|
264
253
|
'passed',
|
|
@@ -274,13 +263,224 @@ interface TaskRow extends TaskUpdate {
|
|
|
274
263
|
finishedAt?: number;
|
|
275
264
|
}
|
|
276
265
|
|
|
266
|
+
interface GeneratedPathRow {
|
|
267
|
+
id: string;
|
|
268
|
+
path: string;
|
|
269
|
+
previewable: boolean;
|
|
270
|
+
task: TaskRow;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const OUTPUT_PHASES = new Set<TaskPhase>([
|
|
274
|
+
'generating',
|
|
275
|
+
'generated',
|
|
276
|
+
'running',
|
|
277
|
+
'passed',
|
|
278
|
+
'failed',
|
|
279
|
+
'agent_error',
|
|
280
|
+
'cancelled',
|
|
281
|
+
]);
|
|
282
|
+
function generatedPathRows(tasks: Iterable<TaskRow>): GeneratedPathRow[] {
|
|
283
|
+
const rows: GeneratedPathRow[] = [];
|
|
284
|
+
for (const task of tasks) {
|
|
285
|
+
if (!OUTPUT_PHASES.has(task.phase)) continue;
|
|
286
|
+
if (task.generatedFiles.length === 0) {
|
|
287
|
+
rows.push({
|
|
288
|
+
id: `${task.id}:directory`,
|
|
289
|
+
path: task.directory,
|
|
290
|
+
previewable: false,
|
|
291
|
+
task,
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
for (const file of task.generatedFiles) {
|
|
296
|
+
rows.push({ id: `${task.id}:${file}`, path: file, previewable: true, task });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return rows.sort((left, right) => left.path.localeCompare(right.path));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type FilePreviewState =
|
|
303
|
+
| { status: 'empty' | 'loading' }
|
|
304
|
+
| { status: 'loaded'; lines: string[]; truncated: boolean }
|
|
305
|
+
| { status: 'error'; message: string };
|
|
306
|
+
|
|
307
|
+
function useFilePreview(row: GeneratedPathRow | undefined): FilePreviewState {
|
|
308
|
+
const [preview, setPreview] = useState<FilePreviewState>({ status: 'empty' });
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!row?.previewable) {
|
|
312
|
+
setPreview({ status: 'empty' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
let active = true;
|
|
316
|
+
setPreview({ status: 'loading' });
|
|
317
|
+
void readFilePreview(row.path)
|
|
318
|
+
.then((result) => {
|
|
319
|
+
if (!active) return;
|
|
320
|
+
if (result.binary) {
|
|
321
|
+
setPreview({ status: 'error', message: 'Binary files cannot be previewed.' });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
setPreview({
|
|
325
|
+
status: 'loaded',
|
|
326
|
+
lines: result.text.split(/\r?\n/),
|
|
327
|
+
truncated: result.truncated,
|
|
328
|
+
});
|
|
329
|
+
})
|
|
330
|
+
.catch((error: unknown) => {
|
|
331
|
+
if (!active) return;
|
|
332
|
+
setPreview({
|
|
333
|
+
status: 'error',
|
|
334
|
+
message: error instanceof Error ? error.message : String(error),
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
return () => {
|
|
338
|
+
active = false;
|
|
339
|
+
};
|
|
340
|
+
}, [row?.path, row?.previewable]);
|
|
341
|
+
|
|
342
|
+
return preview;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function generatedPathStatus(
|
|
346
|
+
phase: TaskPhase,
|
|
347
|
+
spinner: string
|
|
348
|
+
): { glyph: string; color?: string; dim?: boolean } {
|
|
349
|
+
if (phase === 'generating' || phase === 'running') return { glyph: spinner, color: 'green' };
|
|
350
|
+
if (phase === 'generated' || phase === 'passed') return { glyph: '✓', color: 'green' };
|
|
351
|
+
if (phase === 'failed' || phase === 'agent_error') return { glyph: '✗', color: 'red' };
|
|
352
|
+
if (phase === 'cancelled') return { glyph: '●', color: 'yellow' };
|
|
353
|
+
return { glyph: '○', dim: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function FilePreview({ row, height }: { row: GeneratedPathRow | undefined; height: number }) {
|
|
357
|
+
const preview = useFilePreview(row);
|
|
358
|
+
// Border (2), title, path, margin, and one row kept for the truncation notice.
|
|
359
|
+
const lineLimit = Math.max(1, height - 6);
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<Box
|
|
363
|
+
width="52%"
|
|
364
|
+
height={height}
|
|
365
|
+
borderStyle="round"
|
|
366
|
+
borderColor="gray"
|
|
367
|
+
paddingX={1}
|
|
368
|
+
flexDirection="column"
|
|
369
|
+
overflow="hidden"
|
|
370
|
+
>
|
|
371
|
+
<Text bold>File preview</Text>
|
|
372
|
+
{row ? <Text dimColor>{row.path}</Text> : <Text dimColor>Select a generated file.</Text>}
|
|
373
|
+
{!row?.previewable ? (
|
|
374
|
+
<Text dimColor>
|
|
375
|
+
{row?.task.phase === 'generating'
|
|
376
|
+
? 'Files will appear here after generation finishes.'
|
|
377
|
+
: 'No generated file is available to preview.'}
|
|
378
|
+
</Text>
|
|
379
|
+
) : preview.status === 'loading' ? (
|
|
380
|
+
<Text dimColor>Loading preview...</Text>
|
|
381
|
+
) : preview.status === 'error' ? (
|
|
382
|
+
<Text color="yellow">{preview.message}</Text>
|
|
383
|
+
) : preview.status === 'loaded' ? (
|
|
384
|
+
<Box flexDirection="column" marginTop={1}>
|
|
385
|
+
{preview.lines.slice(0, lineLimit).map((line, index) => (
|
|
386
|
+
<Text key={`${index}:${line}`} wrap="truncate-end">
|
|
387
|
+
<Text dimColor>{String(index + 1).padStart(3)} </Text>
|
|
388
|
+
{line || ' '}
|
|
389
|
+
</Text>
|
|
390
|
+
))}
|
|
391
|
+
{preview.truncated || preview.lines.length > lineLimit ? (
|
|
392
|
+
<Text dimColor>Preview truncated</Text>
|
|
393
|
+
) : null}
|
|
394
|
+
</Box>
|
|
395
|
+
) : null}
|
|
396
|
+
</Box>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function GeneratedFilesView({
|
|
401
|
+
rows,
|
|
402
|
+
cursor,
|
|
403
|
+
height,
|
|
404
|
+
spinner,
|
|
405
|
+
relativeRoot,
|
|
406
|
+
emptyMessage = 'Waiting for generated files...',
|
|
407
|
+
}: {
|
|
408
|
+
rows: GeneratedPathRow[];
|
|
409
|
+
cursor: number;
|
|
410
|
+
height: number;
|
|
411
|
+
spinner: string;
|
|
412
|
+
relativeRoot: string;
|
|
413
|
+
emptyMessage?: string;
|
|
414
|
+
}) {
|
|
415
|
+
const safeCursor = Math.max(0, Math.min(cursor, rows.length - 1));
|
|
416
|
+
const viewport = Math.max(2, height - 3);
|
|
417
|
+
const start = Math.max(
|
|
418
|
+
0,
|
|
419
|
+
Math.min(safeCursor - Math.floor(viewport / 2), rows.length - viewport)
|
|
420
|
+
);
|
|
421
|
+
const visible = rows.slice(start, start + viewport);
|
|
422
|
+
const selected = rows[safeCursor];
|
|
423
|
+
const showPreview = selected?.previewable === true;
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<Box flexDirection="row" gap={1} flexGrow={1} overflow="hidden">
|
|
427
|
+
<Box
|
|
428
|
+
width={showPreview ? '48%' : '100%'}
|
|
429
|
+
borderStyle="round"
|
|
430
|
+
borderColor="gray"
|
|
431
|
+
paddingX={1}
|
|
432
|
+
flexDirection="column"
|
|
433
|
+
overflow="hidden"
|
|
434
|
+
>
|
|
435
|
+
<Text bold>Generated files</Text>
|
|
436
|
+
{visible.length === 0 ? <Text dimColor>{emptyMessage}</Text> : null}
|
|
437
|
+
{visible.map((row, visibleIndex) => {
|
|
438
|
+
const index = start + visibleIndex;
|
|
439
|
+
const status = generatedPathStatus(row.task.phase, spinner);
|
|
440
|
+
const relativePath = path.relative(relativeRoot, row.path) || '.';
|
|
441
|
+
return (
|
|
442
|
+
<Text
|
|
443
|
+
key={row.id}
|
|
444
|
+
color={index === safeCursor ? 'cyan' : undefined}
|
|
445
|
+
wrap="truncate-end"
|
|
446
|
+
>
|
|
447
|
+
{index === safeCursor ? '›' : ' '}{' '}
|
|
448
|
+
<Text color={status.color} dimColor={status.dim}>
|
|
449
|
+
{status.glyph}
|
|
450
|
+
</Text>{' '}
|
|
451
|
+
{row.previewable
|
|
452
|
+
? row.path
|
|
453
|
+
: row.task.phase === 'generating'
|
|
454
|
+
? `Writing under ${relativePath}`
|
|
455
|
+
: `No generated file in ${relativePath}`}
|
|
456
|
+
{row.task.phase === 'failed' || row.task.phase === 'agent_error' ? (
|
|
457
|
+
<Text color="red">
|
|
458
|
+
{' '}
|
|
459
|
+
{taskProblemLabel(row.task.phase)}
|
|
460
|
+
{row.task.error ? <Text dimColor> · {firstLine(row.task.error)}</Text> : null}
|
|
461
|
+
</Text>
|
|
462
|
+
) : null}
|
|
463
|
+
</Text>
|
|
464
|
+
);
|
|
465
|
+
})}
|
|
466
|
+
</Box>
|
|
467
|
+
{showPreview ? <FilePreview row={selected} height={height} /> : null}
|
|
468
|
+
</Box>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
277
472
|
function fileStatus(
|
|
278
473
|
row: TaskRow | undefined,
|
|
279
474
|
spinner: string
|
|
280
475
|
): { glyph: string; color?: string; dim?: boolean } {
|
|
281
476
|
if (row && ACTIVE_PHASES.has(row.phase)) return { glyph: spinner, color: 'green' };
|
|
282
|
-
if (row?.phase === 'not_testable') return { glyph: '✗', color: 'yellow' };
|
|
283
477
|
if (row?.phase === 'failed' || row?.phase === 'agent_error') return { glyph: '✗', color: 'red' };
|
|
478
|
+
if (row?.phase === 'checked' || row?.phase === 'generated' || row?.phase === 'passed') {
|
|
479
|
+
return { glyph: '✓', color: 'green' };
|
|
480
|
+
}
|
|
481
|
+
if (row?.phase === 'not_testable' || row?.phase === 'cancelled') {
|
|
482
|
+
return { glyph: '●', color: 'yellow' };
|
|
483
|
+
}
|
|
284
484
|
return { glyph: '○', dim: true };
|
|
285
485
|
}
|
|
286
486
|
|
|
@@ -295,22 +495,35 @@ function directoryStatus(
|
|
|
295
495
|
if (rows.some((row) => row && ACTIVE_PHASES.has(row.phase))) {
|
|
296
496
|
return { glyph: spinner, color: 'green' };
|
|
297
497
|
}
|
|
498
|
+
if (rows.some((row) => row?.phase === 'failed' || row?.phase === 'agent_error')) {
|
|
499
|
+
return { glyph: '✗', color: 'red' };
|
|
500
|
+
}
|
|
298
501
|
if (rows.every((row) => row && SETTLED_PHASES.has(row.phase))) {
|
|
299
|
-
if (rows.
|
|
300
|
-
return { glyph: '●', color: '
|
|
502
|
+
if (rows.every((row) => row?.phase === 'not_testable' || row?.phase === 'cancelled')) {
|
|
503
|
+
return { glyph: '●', color: 'yellow' };
|
|
301
504
|
}
|
|
302
|
-
return
|
|
303
|
-
? { glyph: '●', color: 'green' }
|
|
304
|
-
: { glyph: '●', dim: true };
|
|
505
|
+
return { glyph: '✓', color: 'green' };
|
|
305
506
|
}
|
|
306
507
|
return { glyph: '○', dim: true };
|
|
307
508
|
}
|
|
308
509
|
|
|
510
|
+
function ActivityLegend({ spinner }: { spinner: string }) {
|
|
511
|
+
return (
|
|
512
|
+
<Text dimColor>
|
|
513
|
+
○ waiting · <Text color="green">{spinner}</Text> active · <Text color="green">✓</Text>{' '}
|
|
514
|
+
complete · <Text color="red">✗</Text> failed · <Text color="yellow">●</Text> skipped /
|
|
515
|
+
cancelled
|
|
516
|
+
</Text>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
309
520
|
function ActivityScreen({
|
|
310
521
|
stage,
|
|
311
522
|
cancelling,
|
|
312
523
|
harness,
|
|
524
|
+
docsRoot,
|
|
313
525
|
rows,
|
|
526
|
+
generatedRows,
|
|
314
527
|
cursor,
|
|
315
528
|
phases,
|
|
316
529
|
height,
|
|
@@ -322,7 +535,9 @@ function ActivityScreen({
|
|
|
322
535
|
stage: 'check' | 'generate' | 'run';
|
|
323
536
|
cancelling: boolean;
|
|
324
537
|
harness: string;
|
|
538
|
+
docsRoot: string;
|
|
325
539
|
rows: ReturnType<typeof visibleScopeRows>;
|
|
540
|
+
generatedRows: GeneratedPathRow[];
|
|
326
541
|
cursor: number;
|
|
327
542
|
phases: ReadonlyMap<string, TaskRow>;
|
|
328
543
|
height: number;
|
|
@@ -368,93 +583,140 @@ function ActivityScreen({
|
|
|
368
583
|
</Text>
|
|
369
584
|
<Text dimColor>Harness: {harness}</Text>
|
|
370
585
|
<Text dimColor>{progress}</Text>
|
|
586
|
+
{stage === 'check' || stage === 'generate' ? <ActivityLegend spinner={spinner} /> : null}
|
|
371
587
|
</Box>
|
|
372
|
-
<Box
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
|
|
588
|
+
<Box marginTop={1} flexGrow={1} overflow="hidden">
|
|
589
|
+
{stage === 'check' ? (
|
|
590
|
+
<Box
|
|
591
|
+
borderStyle="round"
|
|
592
|
+
borderColor="gray"
|
|
593
|
+
paddingX={1}
|
|
594
|
+
flexDirection="column"
|
|
595
|
+
flexGrow={1}
|
|
596
|
+
>
|
|
597
|
+
{visible.map((row, visibleIndex) => {
|
|
598
|
+
const index = start + visibleIndex;
|
|
599
|
+
const disclosure = row.kind === 'directory' ? (row.expanded ? '▾' : '▸') : ' ';
|
|
600
|
+
const status =
|
|
601
|
+
row.kind === 'directory'
|
|
602
|
+
? directoryStatus(row.selectableFiles, phases, spinner)
|
|
603
|
+
: fileStatus(row.file ? phases.get(row.file) : undefined, spinner);
|
|
604
|
+
const task = row.file ? phases.get(row.file) : undefined;
|
|
605
|
+
const settled =
|
|
606
|
+
row.kind === 'directory'
|
|
607
|
+
? row.selectableFiles.filter((file) => {
|
|
608
|
+
const phase = phases.get(file)?.phase;
|
|
609
|
+
return phase !== undefined && SETTLED_PHASES.has(phase);
|
|
610
|
+
}).length
|
|
611
|
+
: 0;
|
|
612
|
+
return (
|
|
613
|
+
<Text
|
|
614
|
+
key={row.id}
|
|
615
|
+
color={index === cursor ? 'cyan' : undefined}
|
|
616
|
+
wrap="truncate-end"
|
|
617
|
+
>
|
|
618
|
+
{' '.repeat(row.depth * 2)}
|
|
619
|
+
{index === cursor ? '›' : ' '}{' '}
|
|
620
|
+
<Text color={status.color} dimColor={status.dim}>
|
|
621
|
+
{status.glyph}
|
|
622
|
+
</Text>{' '}
|
|
623
|
+
{disclosure} {row.label}
|
|
624
|
+
{row.kind === 'directory' ? (
|
|
625
|
+
<Text dimColor>
|
|
626
|
+
{' '}
|
|
627
|
+
{padCount(settled, row.selectableFiles.length)}/{row.selectableFiles.length}{' '}
|
|
628
|
+
complete
|
|
629
|
+
</Text>
|
|
630
|
+
) : null}
|
|
631
|
+
{task ? (
|
|
632
|
+
<Text dimColor> {padDuration((task.finishedAt ?? now) - task.startedAt)}</Text>
|
|
633
|
+
) : null}
|
|
634
|
+
{task && (task.phase === 'failed' || task.phase === 'agent_error') ? (
|
|
635
|
+
<Text color="red">
|
|
636
|
+
{' '}
|
|
637
|
+
{taskProblemLabel(task.phase)}
|
|
638
|
+
{task.error ? <Text dimColor> · {firstLine(task.error)}</Text> : null}
|
|
639
|
+
</Text>
|
|
640
|
+
) : null}
|
|
408
641
|
</Text>
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
642
|
+
);
|
|
643
|
+
})}
|
|
644
|
+
</Box>
|
|
645
|
+
) : (
|
|
646
|
+
<GeneratedFilesView
|
|
647
|
+
rows={generatedRows}
|
|
648
|
+
cursor={cursor}
|
|
649
|
+
height={Math.max(4, height - 7)}
|
|
650
|
+
spinner={spinner}
|
|
651
|
+
relativeRoot={docsRoot}
|
|
652
|
+
/>
|
|
653
|
+
)}
|
|
416
654
|
</Box>
|
|
417
655
|
</Box>
|
|
418
656
|
);
|
|
419
657
|
}
|
|
420
658
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
return
|
|
659
|
+
const MAX_PROBLEM_ROWS = 6;
|
|
660
|
+
|
|
661
|
+
function ProblemsView({ report }: { report: CodeTestReport }) {
|
|
662
|
+
const problems = report.tasks.filter(
|
|
663
|
+
(task): task is CodeTestReport['tasks'][number] & { status: 'failed' | 'agent_error' } =>
|
|
664
|
+
task.status === 'failed' || task.status === 'agent_error'
|
|
665
|
+
);
|
|
666
|
+
if (problems.length === 0) return null;
|
|
667
|
+
const hidden = problems.length - MAX_PROBLEM_ROWS;
|
|
668
|
+
return (
|
|
669
|
+
<Box
|
|
670
|
+
marginTop={1}
|
|
671
|
+
borderStyle="round"
|
|
672
|
+
borderColor="red"
|
|
673
|
+
paddingX={1}
|
|
674
|
+
flexDirection="column"
|
|
675
|
+
flexShrink={0}
|
|
676
|
+
>
|
|
677
|
+
<Text bold color="red">
|
|
678
|
+
{problems.length} {problems.length === 1 ? 'page needs' : 'pages need'} attention
|
|
679
|
+
</Text>
|
|
680
|
+
{problems.slice(0, MAX_PROBLEM_ROWS).map((task) => (
|
|
681
|
+
<Text key={task.id} wrap="truncate-end">
|
|
682
|
+
<Text color="red">✗</Text> {task.file}{' '}
|
|
683
|
+
<Text dimColor>{taskProblemLabel(task.status)}</Text>
|
|
684
|
+
{task.error ? <Text dimColor> · {firstLine(task.error)}</Text> : null}
|
|
685
|
+
</Text>
|
|
686
|
+
))}
|
|
687
|
+
{hidden > 0 ? <Text dimColor>+{hidden} more in the report</Text> : null}
|
|
688
|
+
</Box>
|
|
689
|
+
);
|
|
431
690
|
}
|
|
432
691
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
692
|
+
/**
|
|
693
|
+
* Rows the report header takes, including its border and the margin below it. The optional
|
|
694
|
+
* lines must be counted, or the screen is one row taller than the terminal and Ink drops the
|
|
695
|
+
* heading (it showed as "Finished in 1m 37s passed").
|
|
696
|
+
*/
|
|
697
|
+
function reportHeaderHeight(report: CodeTestReport, rows: GeneratedPathRow[]): number {
|
|
698
|
+
const optionalLines =
|
|
699
|
+
(report.historyError ? 1 : 0) + (rows.some((row) => row.previewable) ? 1 : 0);
|
|
700
|
+
return 2 + 4 + optionalLines + 1;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function problemsViewHeight(report: CodeTestReport): number {
|
|
704
|
+
const problems = report.tasks.filter(
|
|
705
|
+
(task) => task.status === 'failed' || task.status === 'agent_error'
|
|
706
|
+
).length;
|
|
707
|
+
if (problems === 0) return 0;
|
|
708
|
+
return Math.min(problems, MAX_PROBLEM_ROWS) + 4 + (problems > MAX_PROBLEM_ROWS ? 1 : 0);
|
|
445
709
|
}
|
|
446
710
|
|
|
447
711
|
function ReportScreen({
|
|
448
712
|
report,
|
|
449
713
|
rows,
|
|
450
714
|
cursor,
|
|
451
|
-
phases,
|
|
452
715
|
height,
|
|
453
716
|
}: {
|
|
454
717
|
report: CodeTestReport;
|
|
455
|
-
rows:
|
|
718
|
+
rows: GeneratedPathRow[];
|
|
456
719
|
cursor: number;
|
|
457
|
-
phases: ReadonlyMap<string, TaskRow>;
|
|
458
720
|
height: number;
|
|
459
721
|
}) {
|
|
460
722
|
const statusColor =
|
|
@@ -471,13 +733,15 @@ function ReportScreen({
|
|
|
471
733
|
: report.status === 'completed'
|
|
472
734
|
? 'Run completed'
|
|
473
735
|
: `Run ${report.status}`;
|
|
474
|
-
const viewport = Math.max(4, height - 12);
|
|
475
|
-
const start = Math.max(0, Math.min(cursor - Math.floor(viewport / 2), rows.length - viewport));
|
|
476
|
-
const visible = rows.slice(start, start + viewport);
|
|
477
|
-
|
|
478
736
|
return (
|
|
479
737
|
<Box flexDirection="column" flexGrow={1}>
|
|
480
|
-
<Box
|
|
738
|
+
<Box
|
|
739
|
+
borderStyle="round"
|
|
740
|
+
borderColor="gray"
|
|
741
|
+
paddingX={1}
|
|
742
|
+
flexDirection="column"
|
|
743
|
+
flexShrink={0}
|
|
744
|
+
>
|
|
481
745
|
<Text bold color={statusColor}>
|
|
482
746
|
{heading}
|
|
483
747
|
</Text>
|
|
@@ -487,66 +751,73 @@ function ReportScreen({
|
|
|
487
751
|
<Text dimColor wrap="truncate-end">
|
|
488
752
|
Report at {report.reportPath}
|
|
489
753
|
</Text>
|
|
754
|
+
<Text dimColor wrap="truncate-end">
|
|
755
|
+
Generated files at {report.outputDirectory}
|
|
756
|
+
</Text>
|
|
757
|
+
{report.historyError ? (
|
|
758
|
+
<Text color="yellow" wrap="truncate-end">
|
|
759
|
+
Warning: {report.historyError}
|
|
760
|
+
</Text>
|
|
761
|
+
) : null}
|
|
762
|
+
{rows.some((row) => row.previewable) ? (
|
|
763
|
+
<Text>Select a file below to preview its contents.</Text>
|
|
764
|
+
) : null}
|
|
490
765
|
</Box>
|
|
491
|
-
<
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
? reportDirectoryStatus(row.selectableFiles, phases)
|
|
505
|
-
: reportFileStatus(row.file ? phases.get(row.file) : undefined);
|
|
506
|
-
const task = row.file ? phases.get(row.file) : undefined;
|
|
507
|
-
const passed =
|
|
508
|
-
row.kind === 'directory'
|
|
509
|
-
? row.selectableFiles.filter((file) => phases.get(file)?.phase === 'passed').length
|
|
510
|
-
: 0;
|
|
511
|
-
return (
|
|
512
|
-
<Text key={row.id} color={index === cursor ? 'cyan' : undefined} wrap="truncate-end">
|
|
513
|
-
{' '.repeat(row.depth * 2)}
|
|
514
|
-
{index === cursor ? '›' : ' '}{' '}
|
|
515
|
-
<Text color={status.color} dimColor={status.dim}>
|
|
516
|
-
{status.glyph}
|
|
517
|
-
</Text>{' '}
|
|
518
|
-
{disclosure} {row.label}
|
|
519
|
-
{row.kind === 'directory' ? (
|
|
520
|
-
<Text dimColor>
|
|
521
|
-
{' '}
|
|
522
|
-
{padCount(passed, row.selectableFiles.length)}/{row.selectableFiles.length} passed
|
|
523
|
-
</Text>
|
|
524
|
-
) : task ? (
|
|
525
|
-
<Text dimColor>
|
|
526
|
-
{' '}
|
|
527
|
-
{PHASE_LABELS[task.phase]}
|
|
528
|
-
{task.finishedAt ? ` · ${formatDuration(task.finishedAt - task.startedAt)}` : ''}
|
|
529
|
-
</Text>
|
|
530
|
-
) : null}
|
|
531
|
-
</Text>
|
|
532
|
-
);
|
|
533
|
-
})}
|
|
766
|
+
<ProblemsView report={report} />
|
|
767
|
+
<Box marginTop={1} flexGrow={1} overflow="hidden">
|
|
768
|
+
<GeneratedFilesView
|
|
769
|
+
rows={rows}
|
|
770
|
+
cursor={cursor}
|
|
771
|
+
height={Math.max(
|
|
772
|
+
4,
|
|
773
|
+
height - reportHeaderHeight(report, rows) - problemsViewHeight(report)
|
|
774
|
+
)}
|
|
775
|
+
spinner="●"
|
|
776
|
+
relativeRoot={report.docsRoot}
|
|
777
|
+
emptyMessage="No generated files."
|
|
778
|
+
/>
|
|
534
779
|
</Box>
|
|
535
780
|
</Box>
|
|
536
781
|
);
|
|
537
782
|
}
|
|
538
783
|
|
|
539
|
-
function
|
|
540
|
-
|
|
784
|
+
function FooterHints({ text }: { text: string }) {
|
|
785
|
+
return (
|
|
786
|
+
<>
|
|
787
|
+
{footerSegments(text).map((segment, index) => (
|
|
788
|
+
<Text key={`${index}:${segment.key}`}>
|
|
789
|
+
{index > 0 ? ' · ' : ''}
|
|
790
|
+
<Text bold>{segment.key}</Text>
|
|
791
|
+
{segment.action ? ` ${segment.action}` : ''}
|
|
792
|
+
</Text>
|
|
793
|
+
))}
|
|
794
|
+
</>
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function footerForScreen(
|
|
799
|
+
screen: Screen,
|
|
800
|
+
running: boolean,
|
|
801
|
+
cancelled: boolean,
|
|
802
|
+
activeRunStage: 'check' | 'generate' | 'run',
|
|
803
|
+
reviewingSavedRun: boolean
|
|
804
|
+
): string {
|
|
805
|
+
if (running) {
|
|
806
|
+
return activeRunStage === 'check'
|
|
807
|
+
? '↑/↓/j/k move · ←/→/h/l collapse/expand · Ctrl-C cancel'
|
|
808
|
+
: '↑/↓/j/k preview generated files · Ctrl-C cancel';
|
|
809
|
+
}
|
|
541
810
|
if (screen === 'resume') return '↑/↓/j/k move · Enter confirm · Ctrl-C cancel';
|
|
811
|
+
if (screen === 'existing') return '↑/↓/j/k move · Enter confirm · Ctrl-C cancel';
|
|
542
812
|
if (screen === 'harnesses') return '↑/↓/j/k move · Enter continue · Ctrl-C cancel';
|
|
543
813
|
if (screen === 'scope') {
|
|
544
814
|
return '↑/↓/j/k move · ←/→/h/l collapse/expand · Space select · a all · Enter start';
|
|
545
815
|
}
|
|
546
816
|
if (screen === 'report') {
|
|
817
|
+
if (reviewingSavedRun) return '↑/↓/j/k preview generated files · Enter exit';
|
|
547
818
|
return cancelled
|
|
548
|
-
? '↑/↓/j/k
|
|
549
|
-
: '↑/↓/j/k
|
|
819
|
+
? '↑/↓/j/k preview generated files · Ctrl-C exit · save kept for resume'
|
|
820
|
+
: '↑/↓/j/k preview generated files · Enter exit';
|
|
550
821
|
}
|
|
551
822
|
return 'Enter exit';
|
|
552
823
|
}
|
|
@@ -556,15 +827,18 @@ function MintTestApp({
|
|
|
556
827
|
docsRoot,
|
|
557
828
|
pages,
|
|
558
829
|
autoSelectedFiles,
|
|
559
|
-
totalPages,
|
|
560
830
|
save,
|
|
831
|
+
lastReport,
|
|
561
832
|
onFinish,
|
|
562
833
|
}: MintTestUiProps) {
|
|
563
834
|
const size = useTerminalSize();
|
|
564
835
|
const files = useMemo(() => pages.map((page) => page.file), [pages]);
|
|
565
836
|
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
566
|
-
const [screen, setScreen] = useState<Screen>(
|
|
837
|
+
const [screen, setScreen] = useState<Screen>(
|
|
838
|
+
save ? 'resume' : lastReport ? 'existing' : 'harnesses'
|
|
839
|
+
);
|
|
567
840
|
const [resumeCursor, setResumeCursor] = useState(0);
|
|
841
|
+
const [existingRunCursor, setExistingRunCursor] = useState(0);
|
|
568
842
|
const [harnessIndex, setHarnessIndex] = useState(0);
|
|
569
843
|
const [runHarness, setRunHarness] = useState('');
|
|
570
844
|
const harness = HARNESSES[harnessIndex] ?? HARNESSES[0]!;
|
|
@@ -583,9 +857,11 @@ function MintTestApp({
|
|
|
583
857
|
const [runStartedAt, setRunStartedAt] = useState(0);
|
|
584
858
|
const autoCollapsed = useRef<Set<string>>(new Set());
|
|
585
859
|
const manualCursor = useRef(false);
|
|
860
|
+
const previousRunStage = useRef(activeRunStage);
|
|
586
861
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
587
862
|
const [now, setNow] = useState(() => Date.now());
|
|
588
863
|
const [report, setReport] = useState<CodeTestReport>();
|
|
864
|
+
const [reviewingSavedRun, setReviewingSavedRun] = useState(false);
|
|
589
865
|
const [error, setError] = useState<string>();
|
|
590
866
|
const abortController = useRef<AbortController | undefined>(undefined);
|
|
591
867
|
const finished = useRef(false);
|
|
@@ -612,6 +888,7 @@ function MintTestApp({
|
|
|
612
888
|
EMPTY_SELECTION
|
|
613
889
|
);
|
|
614
890
|
}, [pages, visibleRunFiles, runExpanded]);
|
|
891
|
+
const generatedRows = useMemo(() => generatedPathRows(taskPhases.values()), [taskPhases]);
|
|
615
892
|
|
|
616
893
|
useEffect(() => {
|
|
617
894
|
if (screen !== 'running') return;
|
|
@@ -624,7 +901,7 @@ function MintTestApp({
|
|
|
624
901
|
}, [screen]);
|
|
625
902
|
|
|
626
903
|
useEffect(() => {
|
|
627
|
-
if (screen !== 'running') return;
|
|
904
|
+
if (screen !== 'running' || activeRunStage !== 'check') return;
|
|
628
905
|
const fileDone = (file: string) => {
|
|
629
906
|
const phase = phasesByFile.get(file)?.phase;
|
|
630
907
|
return phase !== undefined && DONE_PHASES.has(phase);
|
|
@@ -660,7 +937,14 @@ function MintTestApp({
|
|
|
660
937
|
const nextIndex = runRows.findIndex((row) => !rowDone(row));
|
|
661
938
|
if (nextIndex !== -1 && nextIndex !== runCursor) setRunCursor(nextIndex);
|
|
662
939
|
}
|
|
663
|
-
}, [screen, phasesByFile, visibleRunFiles, runRows, runCursor]);
|
|
940
|
+
}, [screen, activeRunStage, phasesByFile, visibleRunFiles, runRows, runCursor]);
|
|
941
|
+
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (previousRunStage.current === activeRunStage) return;
|
|
944
|
+
previousRunStage.current = activeRunStage;
|
|
945
|
+
manualCursor.current = false;
|
|
946
|
+
setRunCursor(0);
|
|
947
|
+
}, [activeRunStage]);
|
|
664
948
|
|
|
665
949
|
const finish = (exitCode: number) => {
|
|
666
950
|
if (finished.current) return;
|
|
@@ -668,6 +952,40 @@ function MintTestApp({
|
|
|
668
952
|
onFinish(exitCode);
|
|
669
953
|
};
|
|
670
954
|
|
|
955
|
+
const reviewRun = (savedReport: CodeTestReport) => {
|
|
956
|
+
const startedAt = Date.parse(savedReport.startedAt);
|
|
957
|
+
const finishedAt = Date.parse(savedReport.completedAt);
|
|
958
|
+
setTaskPhases(
|
|
959
|
+
new Map(
|
|
960
|
+
savedReport.tasks.map((task) => [
|
|
961
|
+
task.id,
|
|
962
|
+
{
|
|
963
|
+
id: task.id,
|
|
964
|
+
agent: task.agent,
|
|
965
|
+
file: task.file,
|
|
966
|
+
directory: task.directory,
|
|
967
|
+
generatedFiles: task.generatedFiles,
|
|
968
|
+
phase: !task.testable && task.status === 'passed' ? 'not_testable' : task.status,
|
|
969
|
+
tokens: task.tokens,
|
|
970
|
+
startedAt: Number.isNaN(startedAt) ? 0 : startedAt,
|
|
971
|
+
finishedAt: Number.isNaN(finishedAt) ? undefined : finishedAt,
|
|
972
|
+
} satisfies TaskRow,
|
|
973
|
+
])
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
setRunFiles(savedReport.selectedFiles);
|
|
977
|
+
setRunHarness(
|
|
978
|
+
HARNESSES.find(
|
|
979
|
+
(entry) => entry.agent === savedReport.agents[0] && entry.model === savedReport.model
|
|
980
|
+
)?.label ?? `${savedReport.agents.join(', ')} · ${savedReport.model}`
|
|
981
|
+
);
|
|
982
|
+
setReport(savedReport);
|
|
983
|
+
setReviewingSavedRun(true);
|
|
984
|
+
setActiveRunStage('run');
|
|
985
|
+
setRunCursor(0);
|
|
986
|
+
setScreen('report');
|
|
987
|
+
};
|
|
988
|
+
|
|
671
989
|
const startRun = ({
|
|
672
990
|
runId,
|
|
673
991
|
runAgents,
|
|
@@ -682,6 +1000,10 @@ function MintTestApp({
|
|
|
682
1000
|
if (abortController.current) return;
|
|
683
1001
|
const controller = new AbortController();
|
|
684
1002
|
abortController.current = controller;
|
|
1003
|
+
setTaskPhases(new Map());
|
|
1004
|
+
setReport(undefined);
|
|
1005
|
+
setReviewingSavedRun(false);
|
|
1006
|
+
setError(undefined);
|
|
685
1007
|
setTotalTasks(runAgents.length * runFiles_.length);
|
|
686
1008
|
setRunHarness(
|
|
687
1009
|
HARNESSES.find((entry) => entry.agent === runAgents[0] && entry.model === runModel)?.label ??
|
|
@@ -782,13 +1104,15 @@ function MintTestApp({
|
|
|
782
1104
|
if (screen === 'running' || screen === 'report') {
|
|
783
1105
|
const collapse = key.leftArrow || input === 'h';
|
|
784
1106
|
const expand = key.rightArrow || input === 'l';
|
|
1107
|
+
const showingGeneratedFiles = screen === 'report' || activeRunStage !== 'check';
|
|
1108
|
+
const rowCount = showingGeneratedFiles ? generatedRows.length : runRows.length;
|
|
785
1109
|
if (key.upArrow || input === 'k') {
|
|
786
1110
|
manualCursor.current = true;
|
|
787
1111
|
setRunCursor((current) => Math.max(0, current - 1));
|
|
788
1112
|
} else if (key.downArrow || input === 'j') {
|
|
789
1113
|
manualCursor.current = true;
|
|
790
|
-
setRunCursor((current) => Math.max(0, Math.min(
|
|
791
|
-
} else if (collapse || expand) {
|
|
1114
|
+
setRunCursor((current) => Math.max(0, Math.min(rowCount - 1, current + 1)));
|
|
1115
|
+
} else if ((collapse || expand) && !showingGeneratedFiles) {
|
|
792
1116
|
manualCursor.current = true;
|
|
793
1117
|
const row = runRows[runCursor];
|
|
794
1118
|
if (row?.kind !== 'directory') return;
|
|
@@ -799,7 +1123,51 @@ function MintTestApp({
|
|
|
799
1123
|
return next;
|
|
800
1124
|
});
|
|
801
1125
|
} else if (screen === 'report' && key.return && report) {
|
|
802
|
-
finish(
|
|
1126
|
+
finish(
|
|
1127
|
+
reviewingSavedRun
|
|
1128
|
+
? 0
|
|
1129
|
+
: report.status === 'passed'
|
|
1130
|
+
? 0
|
|
1131
|
+
: report.status === 'cancelled'
|
|
1132
|
+
? 130
|
|
1133
|
+
: 1
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (screen === 'existing' && lastReport) {
|
|
1140
|
+
if (key.upArrow || input === 'k') {
|
|
1141
|
+
setExistingRunCursor(
|
|
1142
|
+
(current) => (current + EXISTING_RUN_OPTIONS.length - 1) % EXISTING_RUN_OPTIONS.length
|
|
1143
|
+
);
|
|
1144
|
+
} else if (key.downArrow || input === 'j') {
|
|
1145
|
+
setExistingRunCursor((current) => (current + 1) % EXISTING_RUN_OPTIONS.length);
|
|
1146
|
+
} else if (key.return) {
|
|
1147
|
+
if (existingRunCursor === 0) {
|
|
1148
|
+
const knownFiles = new Set(files);
|
|
1149
|
+
const updateFiles = lastReport.selectedFiles.filter((file) => knownFiles.has(file));
|
|
1150
|
+
setSelectedFiles(new Set(updateFiles.length > 0 ? updateFiles : autoSelectedFiles));
|
|
1151
|
+
if (updateFiles.length === 0) {
|
|
1152
|
+
const previousHarness = HARNESSES.findIndex(
|
|
1153
|
+
(entry) => entry.agent === lastReport.agents[0] && entry.model === lastReport.model
|
|
1154
|
+
);
|
|
1155
|
+
if (previousHarness !== -1) setHarnessIndex(previousHarness);
|
|
1156
|
+
setScreen('scope');
|
|
1157
|
+
} else {
|
|
1158
|
+
startRun({
|
|
1159
|
+
runId: null,
|
|
1160
|
+
runAgents: lastReport.agents,
|
|
1161
|
+
runModel: lastReport.model,
|
|
1162
|
+
runFiles: updateFiles,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
} else if (existingRunCursor === 1) {
|
|
1166
|
+
reviewRun(lastReport);
|
|
1167
|
+
} else {
|
|
1168
|
+
setSelectedFiles(new Set(autoSelectedFiles));
|
|
1169
|
+
setScreen('harnesses');
|
|
1170
|
+
}
|
|
803
1171
|
}
|
|
804
1172
|
return;
|
|
805
1173
|
}
|
|
@@ -875,33 +1243,15 @@ function MintTestApp({
|
|
|
875
1243
|
const fullBleed = running || (screen === 'report' && report !== undefined);
|
|
876
1244
|
let content: React.ReactNode;
|
|
877
1245
|
if (screen === 'resume' && save) content = <ResumeScreen save={save} cursor={resumeCursor} />;
|
|
878
|
-
else if (screen === '
|
|
1246
|
+
else if (screen === 'existing' && lastReport) {
|
|
1247
|
+
content = <ExistingRunScreen report={lastReport} cursor={existingRunCursor} />;
|
|
1248
|
+
} else if (screen === 'harnesses') {
|
|
879
1249
|
content = <HarnessScreen cursor={harnessIndex} />;
|
|
880
1250
|
} else if (screen === 'scope') {
|
|
881
|
-
content =
|
|
882
|
-
<ScopeScreen
|
|
883
|
-
rows={rows}
|
|
884
|
-
cursor={scopeCursor}
|
|
885
|
-
height={viewHeight}
|
|
886
|
-
selectedCount={selectedFiles.size}
|
|
887
|
-
selectedBlocks={pages.reduce(
|
|
888
|
-
(total, page) => total + (selectedFiles.has(page.file) ? page.blockCount : 0),
|
|
889
|
-
0
|
|
890
|
-
)}
|
|
891
|
-
testablePages={pages.length}
|
|
892
|
-
totalPages={totalPages}
|
|
893
|
-
outputDirectory={outputDirectory}
|
|
894
|
-
/>
|
|
895
|
-
);
|
|
1251
|
+
content = <ScopeScreen rows={rows} cursor={scopeCursor} height={viewHeight} />;
|
|
896
1252
|
} else if (screen === 'report' && report) {
|
|
897
1253
|
content = (
|
|
898
|
-
<ReportScreen
|
|
899
|
-
report={report}
|
|
900
|
-
rows={runRows}
|
|
901
|
-
cursor={runCursor}
|
|
902
|
-
phases={phasesByFile}
|
|
903
|
-
height={viewHeight}
|
|
904
|
-
/>
|
|
1254
|
+
<ReportScreen report={report} rows={generatedRows} cursor={runCursor} height={viewHeight} />
|
|
905
1255
|
);
|
|
906
1256
|
} else if (screen === 'error') {
|
|
907
1257
|
content = (
|
|
@@ -918,7 +1268,9 @@ function MintTestApp({
|
|
|
918
1268
|
stage={activeRunStage}
|
|
919
1269
|
cancelling={cancelling}
|
|
920
1270
|
harness={runHarness}
|
|
1271
|
+
docsRoot={docsRoot}
|
|
921
1272
|
rows={runRows}
|
|
1273
|
+
generatedRows={generatedRows}
|
|
922
1274
|
cursor={runCursor}
|
|
923
1275
|
phases={phasesByFile}
|
|
924
1276
|
height={viewHeight}
|
|
@@ -963,9 +1315,19 @@ function MintTestApp({
|
|
|
963
1315
|
</Box>
|
|
964
1316
|
</Box>
|
|
965
1317
|
<Text dimColor>
|
|
966
|
-
{ctrlCArmed
|
|
967
|
-
|
|
968
|
-
|
|
1318
|
+
{ctrlCArmed ? (
|
|
1319
|
+
<FooterHints text="Ctrl-C again to exit" />
|
|
1320
|
+
) : (
|
|
1321
|
+
<FooterHints
|
|
1322
|
+
text={footerForScreen(
|
|
1323
|
+
screen,
|
|
1324
|
+
running,
|
|
1325
|
+
report?.status === 'cancelled',
|
|
1326
|
+
activeRunStage,
|
|
1327
|
+
reviewingSavedRun
|
|
1328
|
+
)}
|
|
1329
|
+
/>
|
|
1330
|
+
)}
|
|
969
1331
|
</Text>
|
|
970
1332
|
</Box>
|
|
971
1333
|
);
|