@mintlify/cli 4.0.1470 → 4.0.1471
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__/mintTestOutput.test.ts +62 -0
- package/bin/agent-harness/generateTestCode.js +31 -2
- package/bin/agent-harness/index.js +2 -1
- package/bin/mintTest.js +26 -3
- package/bin/mintTestTelemetry.js +63 -0
- package/bin/mintTestUi.js +19 -1
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent-harness/generateTestCode.ts +39 -5
- package/src/agent-harness/index.ts +3 -0
- package/src/agent-harness/types.ts +11 -0
- package/src/mintTest.tsx +34 -3
- package/src/mintTestTelemetry.ts +101 -0
- package/src/mintTestUi.tsx +28 -0
|
@@ -79,6 +79,7 @@ it('includes partial generated files when generation fails', async () => {
|
|
|
79
79
|
throw new Error('generation failed');
|
|
80
80
|
});
|
|
81
81
|
const updates: TaskUpdate[] = [];
|
|
82
|
+
const onGenerationResult = vi.fn();
|
|
82
83
|
|
|
83
84
|
const generated = await generateTestCode({
|
|
84
85
|
tasks: [generatedTask(directory)],
|
|
@@ -87,12 +88,22 @@ it('includes partial generated files when generation fails', async () => {
|
|
|
87
88
|
concurrency: 1,
|
|
88
89
|
signal: new AbortController().signal,
|
|
89
90
|
onTaskUpdate: (update) => updates.push(update),
|
|
91
|
+
onGenerationResult,
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
expect(updates.at(-1)).toMatchObject({
|
|
93
95
|
phase: 'agent_error',
|
|
94
96
|
generatedFiles: [generatedFile],
|
|
95
97
|
});
|
|
98
|
+
expect(onGenerationResult).toHaveBeenCalledWith({
|
|
99
|
+
status: 'failed',
|
|
100
|
+
agent: 'codex',
|
|
101
|
+
model: 'unused',
|
|
102
|
+
attemptCount: 2,
|
|
103
|
+
tokenCount: 0,
|
|
104
|
+
durationMs: expect.any(Number),
|
|
105
|
+
generatedFileCount: 1,
|
|
106
|
+
});
|
|
96
107
|
const results = await executeCodeBlocks({
|
|
97
108
|
tasks: generated,
|
|
98
109
|
docsRoot: directory,
|
|
@@ -108,6 +119,57 @@ it('includes partial generated files when generation fails', async () => {
|
|
|
108
119
|
});
|
|
109
120
|
});
|
|
110
121
|
|
|
122
|
+
it('reports a successful generation run', async () => {
|
|
123
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
|
|
124
|
+
temporaryDirectories.push(directory);
|
|
125
|
+
const generatedFile = path.join(directory, 'tests', 'generated.test.ts');
|
|
126
|
+
await fs.mkdir(path.dirname(generatedFile), { recursive: true });
|
|
127
|
+
await fs.writeFile(path.join(directory, 'package.json'), '{}\n');
|
|
128
|
+
agentMocks.codexRun.mockImplementation(async () => {
|
|
129
|
+
await fs.writeFile(generatedFile, 'export const generated = true;\n');
|
|
130
|
+
await fs.writeFile(
|
|
131
|
+
path.join(directory, 'mint-test.json'),
|
|
132
|
+
`${JSON.stringify({
|
|
133
|
+
version: 1,
|
|
134
|
+
generatedFiles: ['tests/generated.test.ts'],
|
|
135
|
+
setupCommands: [],
|
|
136
|
+
testCommands: [],
|
|
137
|
+
})}\n`
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
finalResponse: 'done',
|
|
141
|
+
usage: {
|
|
142
|
+
input_tokens: 100,
|
|
143
|
+
cached_input_tokens: 25,
|
|
144
|
+
cache_write_input_tokens: 10,
|
|
145
|
+
output_tokens: 50,
|
|
146
|
+
reasoning_output_tokens: 15,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const onGenerationResult = vi.fn();
|
|
151
|
+
|
|
152
|
+
await generateTestCode({
|
|
153
|
+
tasks: [generatedTask(directory)],
|
|
154
|
+
docsRoot: directory,
|
|
155
|
+
model: 'gpt-test',
|
|
156
|
+
concurrency: 1,
|
|
157
|
+
signal: new AbortController().signal,
|
|
158
|
+
onTaskUpdate: () => {},
|
|
159
|
+
onGenerationResult,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(onGenerationResult).toHaveBeenCalledWith({
|
|
163
|
+
status: 'succeeded',
|
|
164
|
+
agent: 'codex',
|
|
165
|
+
model: 'gpt-test',
|
|
166
|
+
attemptCount: 1,
|
|
167
|
+
tokenCount: 200,
|
|
168
|
+
durationMs: expect.any(Number),
|
|
169
|
+
generatedFileCount: 1,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
111
173
|
it('includes partial generated files when generation is cancelled', async () => {
|
|
112
174
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
|
|
113
175
|
temporaryDirectories.push(directory);
|
|
@@ -214,7 +214,7 @@ export function checkTestability(_a) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
export function generateTestCode(_a) {
|
|
217
|
-
return __awaiter(this, arguments, void 0, function* ({ tasks, docsRoot, model, concurrency, signal, onTaskUpdate, }) {
|
|
217
|
+
return __awaiter(this, arguments, void 0, function* ({ tasks, docsRoot, model, concurrency, signal, onTaskUpdate, onGenerationResult, }) {
|
|
218
218
|
const generateTask = (task) => __awaiter(this, void 0, void 0, function* () {
|
|
219
219
|
if (task.error || !task.testable)
|
|
220
220
|
return task;
|
|
@@ -230,8 +230,18 @@ export function generateTestCode(_a) {
|
|
|
230
230
|
emit('generated', tokens, existing.generatedFiles.map((file) => path.resolve(task.directory, file)));
|
|
231
231
|
return Object.assign(Object.assign({}, task), { manifest: existing });
|
|
232
232
|
}
|
|
233
|
+
const startedAt = Date.now();
|
|
234
|
+
const startingTokens = tokens;
|
|
235
|
+
let generationAttempts = 0;
|
|
236
|
+
const emitGenerationResult = (result) => {
|
|
237
|
+
try {
|
|
238
|
+
onGenerationResult === null || onGenerationResult === void 0 ? void 0 : onGenerationResult(result);
|
|
239
|
+
}
|
|
240
|
+
catch (_a) { }
|
|
241
|
+
};
|
|
233
242
|
for (let attempt = 1; attempt <= 2 && !signal.aborted; attempt++) {
|
|
234
243
|
attempts += 1;
|
|
244
|
+
generationAttempts += 1;
|
|
235
245
|
if (attempt === 1)
|
|
236
246
|
emit('generating', tokens);
|
|
237
247
|
try {
|
|
@@ -261,7 +271,17 @@ export function generateTestCode(_a) {
|
|
|
261
271
|
const [first = '', ...rest] = errors;
|
|
262
272
|
throw new Error([`${MANIFEST_FILENAME} has problems: ${first}`, ...rest].join('\n'));
|
|
263
273
|
}
|
|
264
|
-
|
|
274
|
+
const generatedFiles = manifest.generatedFiles.map((file) => path.resolve(task.directory, file));
|
|
275
|
+
emit('generated', tokens, generatedFiles);
|
|
276
|
+
emitGenerationResult({
|
|
277
|
+
status: 'succeeded',
|
|
278
|
+
agent: task.agent,
|
|
279
|
+
model,
|
|
280
|
+
attemptCount: generationAttempts,
|
|
281
|
+
tokenCount: tokens - startingTokens,
|
|
282
|
+
durationMs: Date.now() - startedAt,
|
|
283
|
+
generatedFileCount: generatedFiles.length,
|
|
284
|
+
});
|
|
265
285
|
return Object.assign(Object.assign({}, task), { attempts, tokens, manifest });
|
|
266
286
|
}
|
|
267
287
|
catch (error) {
|
|
@@ -275,6 +295,15 @@ export function generateTestCode(_a) {
|
|
|
275
295
|
return Object.assign(Object.assign({}, task), { attempts, tokens, generatedFiles, error: 'run cancelled' });
|
|
276
296
|
}
|
|
277
297
|
const generatedFiles = yield discoverGeneratedFiles(task.directory);
|
|
298
|
+
emitGenerationResult({
|
|
299
|
+
status: 'failed',
|
|
300
|
+
agent: task.agent,
|
|
301
|
+
model,
|
|
302
|
+
attemptCount: generationAttempts,
|
|
303
|
+
tokenCount: tokens - startingTokens,
|
|
304
|
+
durationMs: Date.now() - startedAt,
|
|
305
|
+
generatedFileCount: generatedFiles.length,
|
|
306
|
+
});
|
|
278
307
|
const detailsPath = yield writeErrorDetails(task.directory, attemptErrors);
|
|
279
308
|
const error = detailsPath ? `${lastError}\nDetails: ${detailsPath}` : lastError;
|
|
280
309
|
emit('agent_error', tokens, generatedFiles, error);
|
|
@@ -23,7 +23,7 @@ export { countReport } from './countReport.js';
|
|
|
23
23
|
export { loadLatestTestRun, loadTestRuns } from './runHistory.js';
|
|
24
24
|
export { clearRunSave, loadRunSave } from './runSave.js';
|
|
25
25
|
export function runCodeTests(_a) {
|
|
26
|
-
return __awaiter(this, arguments, void 0, function* ({ path: docsPath, agents, model, selectedFiles, outputDirectory, runId: savedRunId, concurrency, commandTimeoutMs, signal, onTaskUpdate, }) {
|
|
26
|
+
return __awaiter(this, arguments, void 0, function* ({ path: docsPath, agents, model, selectedFiles, outputDirectory, runId: savedRunId, concurrency, commandTimeoutMs, signal, onTaskUpdate, onGenerationResult, }) {
|
|
27
27
|
const startedAt = new Date();
|
|
28
28
|
const uniqueAgents = [...new Set(agents)];
|
|
29
29
|
const { docsRoot, pages: allPages } = yield discoverCodeBlocks(docsPath);
|
|
@@ -55,6 +55,7 @@ export function runCodeTests(_a) {
|
|
|
55
55
|
concurrency,
|
|
56
56
|
signal,
|
|
57
57
|
onTaskUpdate,
|
|
58
|
+
onGenerationResult,
|
|
58
59
|
});
|
|
59
60
|
const results = yield executeCodeBlocks({
|
|
60
61
|
tasks: generated,
|
package/bin/mintTest.js
CHANGED
|
@@ -12,6 +12,7 @@ import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, countRep
|
|
|
12
12
|
import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
|
|
13
13
|
import { getAccessToken } from './keyring.js';
|
|
14
14
|
import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
|
|
15
|
+
import { trackMintTestGenerationResult, trackMintTestRunCompleted, trackMintTestRunStarted, } from './mintTestTelemetry.js';
|
|
15
16
|
import { firstLine, taskProblemLabel } from './mintTestText.js';
|
|
16
17
|
import { formatDuration, runMintTestUi } from './mintTestUi.js';
|
|
17
18
|
export function mintTestHandler() {
|
|
@@ -36,6 +37,7 @@ export function mintTestHandler() {
|
|
|
36
37
|
? discoveredPages.filter((page) => fileInNavPaths(page.file, scope.navPaths))
|
|
37
38
|
: discoveredPages;
|
|
38
39
|
const autoSelectedFiles = (scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages).map((page) => page.file);
|
|
40
|
+
const totalPages = scope ? scope.navPaths.size : pages.length;
|
|
39
41
|
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
40
42
|
let save = yield loadRunSave(outputDirectory);
|
|
41
43
|
if (save) {
|
|
@@ -56,25 +58,46 @@ export function mintTestHandler() {
|
|
|
56
58
|
docsRoot,
|
|
57
59
|
pages,
|
|
58
60
|
autoSelectedFiles,
|
|
61
|
+
totalPages,
|
|
59
62
|
save,
|
|
60
63
|
lastReport,
|
|
61
64
|
});
|
|
62
65
|
yield terminate(exitCode);
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
68
|
+
const agents = (_a = save === null || save === void 0 ? void 0 : save.agents) !== null && _a !== void 0 ? _a : ['claude'];
|
|
69
|
+
const model = (_b = save === null || save === void 0 ? void 0 : save.model) !== null && _b !== void 0 ? _b : 'claude-opus-4-8';
|
|
70
|
+
const selectedFiles = (_c = save === null || save === void 0 ? void 0 : save.selectedFiles) !== null && _c !== void 0 ? _c : autoSelectedFiles;
|
|
71
|
+
const resumed = save !== null;
|
|
65
72
|
if (save)
|
|
66
73
|
process.stdout.write(`resuming run ${save.runId}\n`);
|
|
74
|
+
void trackMintTestRunStarted({
|
|
75
|
+
mode: 'non_interactive',
|
|
76
|
+
resumed,
|
|
77
|
+
agents,
|
|
78
|
+
model,
|
|
79
|
+
selectedPageCount: new Set(selectedFiles).size,
|
|
80
|
+
totalPageCount: totalPages,
|
|
81
|
+
});
|
|
67
82
|
const report = yield runCodeTests({
|
|
68
83
|
path: targetPath,
|
|
69
|
-
agents
|
|
70
|
-
model
|
|
71
|
-
selectedFiles
|
|
84
|
+
agents,
|
|
85
|
+
model,
|
|
86
|
+
selectedFiles,
|
|
72
87
|
outputDirectory,
|
|
73
88
|
runId: (_d = save === null || save === void 0 ? void 0 : save.runId) !== null && _d !== void 0 ? _d : null,
|
|
74
89
|
concurrency: DEFAULT_CONCURRENCY,
|
|
75
90
|
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
76
91
|
signal: new AbortController().signal,
|
|
77
92
|
onTaskUpdate: () => { },
|
|
93
|
+
onGenerationResult: (result) => {
|
|
94
|
+
void trackMintTestGenerationResult(Object.assign(Object.assign({}, result), { mode: 'non_interactive', resumed }));
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
yield trackMintTestRunCompleted(report, {
|
|
98
|
+
mode: 'non_interactive',
|
|
99
|
+
resumed,
|
|
100
|
+
totalPageCount: totalPages,
|
|
78
101
|
});
|
|
79
102
|
const counts = countReport(report.tasks);
|
|
80
103
|
process.stdout.write(`mint test ${report.status}: ${counts.passed} passed, ${counts.failed} failed, ${counts.agentErrors} agent errors\n`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { countReport, } from './agent-harness/index.js';
|
|
11
|
+
import { trackEvent } from './telemetry/track.js';
|
|
12
|
+
export function trackMintTestRunStarted(_a) {
|
|
13
|
+
return __awaiter(this, arguments, void 0, function* ({ mode, resumed, agents, model, selectedPageCount, totalPageCount, }) {
|
|
14
|
+
yield trackEvent('cli.test.started', {
|
|
15
|
+
mode,
|
|
16
|
+
resumed,
|
|
17
|
+
agents,
|
|
18
|
+
model,
|
|
19
|
+
selected_page_count: selectedPageCount,
|
|
20
|
+
total_page_count: totalPageCount,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function trackMintTestRunCompleted(report_1, _a) {
|
|
25
|
+
return __awaiter(this, arguments, void 0, function* (report, { mode, resumed, totalPageCount }) {
|
|
26
|
+
const counts = countReport(report.tasks);
|
|
27
|
+
const skippedCheckCount = report.tasks.filter((task) => !task.testable && task.status === 'passed').length;
|
|
28
|
+
const tokenCount = report.tasks.reduce((total, task) => total + task.tokens, 0);
|
|
29
|
+
const runnableCheckCount = report.tasks.filter((task) => task.testable).length;
|
|
30
|
+
yield trackEvent('cli.test.completed', {
|
|
31
|
+
mode,
|
|
32
|
+
resumed,
|
|
33
|
+
status: report.status,
|
|
34
|
+
agents: report.agents,
|
|
35
|
+
model: report.model,
|
|
36
|
+
selected_page_count: new Set(report.selectedFiles).size,
|
|
37
|
+
total_page_count: totalPageCount,
|
|
38
|
+
total_check_count: report.tasks.length,
|
|
39
|
+
runnable_check_count: runnableCheckCount,
|
|
40
|
+
passed_check_count: counts.passed - skippedCheckCount,
|
|
41
|
+
failed_check_count: counts.failed,
|
|
42
|
+
skipped_check_count: skippedCheckCount,
|
|
43
|
+
agent_error_check_count: counts.agentErrors,
|
|
44
|
+
cancelled_check_count: counts.cancelled,
|
|
45
|
+
token_count: tokenCount,
|
|
46
|
+
duration_ms: report.durationMs,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function trackMintTestGenerationResult(_a) {
|
|
51
|
+
return __awaiter(this, arguments, void 0, function* ({ status, mode, resumed, agent, model, attemptCount, tokenCount, durationMs, generatedFileCount, }) {
|
|
52
|
+
yield trackEvent(`cli.test.generation.${status}`, {
|
|
53
|
+
mode,
|
|
54
|
+
resumed,
|
|
55
|
+
agent,
|
|
56
|
+
model,
|
|
57
|
+
attempt_count: attemptCount,
|
|
58
|
+
token_count: tokenCount,
|
|
59
|
+
duration_ms: durationMs,
|
|
60
|
+
generated_file_count: generatedFileCount,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
package/bin/mintTestUi.js
CHANGED
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
14
|
import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, runCodeTests, } from './agent-harness/index.js';
|
|
15
15
|
import { readFilePreview } from './mintTestFilePreview.js';
|
|
16
|
+
import { trackMintTestGenerationResult, trackMintTestRunCompleted, trackMintTestRunStarted, } from './mintTestTelemetry.js';
|
|
16
17
|
import { firstLine, footerSegments, taskProblemLabel } from './mintTestText.js';
|
|
17
18
|
import { toggleScopeSelection, visibleScopeRows } from './mintTestTree.js';
|
|
18
19
|
const HARNESSES = [
|
|
@@ -375,7 +376,7 @@ function footerForScreen(screen, running, cancelled, activeRunStage, reviewingSa
|
|
|
375
376
|
}
|
|
376
377
|
return 'Enter exit';
|
|
377
378
|
}
|
|
378
|
-
function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, lastReport, onFinish, }) {
|
|
379
|
+
function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPages, save, lastReport, onFinish, }) {
|
|
379
380
|
var _a;
|
|
380
381
|
const size = useTerminalSize();
|
|
381
382
|
const files = useMemo(() => pages.map((page) => page.file), [pages]);
|
|
@@ -515,6 +516,7 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, las
|
|
|
515
516
|
var _a, _b;
|
|
516
517
|
if (abortController.current)
|
|
517
518
|
return;
|
|
519
|
+
const resumed = runId !== null;
|
|
518
520
|
const controller = new AbortController();
|
|
519
521
|
abortController.current = controller;
|
|
520
522
|
setTaskPhases(new Map());
|
|
@@ -535,6 +537,14 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, las
|
|
|
535
537
|
})));
|
|
536
538
|
setScreen('running');
|
|
537
539
|
setActiveRunStage('check');
|
|
540
|
+
void trackMintTestRunStarted({
|
|
541
|
+
mode: 'interactive',
|
|
542
|
+
resumed,
|
|
543
|
+
agents: runAgents,
|
|
544
|
+
model: runModel,
|
|
545
|
+
selectedPageCount: new Set(runFiles_).size,
|
|
546
|
+
totalPageCount: totalPages,
|
|
547
|
+
});
|
|
538
548
|
void runCodeTests({
|
|
539
549
|
path: targetPath,
|
|
540
550
|
agents: runAgents,
|
|
@@ -545,6 +555,9 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, las
|
|
|
545
555
|
concurrency: DEFAULT_CONCURRENCY,
|
|
546
556
|
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
547
557
|
signal: controller.signal,
|
|
558
|
+
onGenerationResult: (result) => {
|
|
559
|
+
void trackMintTestGenerationResult(Object.assign(Object.assign({}, result), { mode: 'interactive', resumed }));
|
|
560
|
+
},
|
|
548
561
|
onTaskUpdate: (update) => {
|
|
549
562
|
const phaseStage = update.phase === 'running'
|
|
550
563
|
? 'run'
|
|
@@ -566,6 +579,11 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, las
|
|
|
566
579
|
},
|
|
567
580
|
})
|
|
568
581
|
.then((nextReport) => {
|
|
582
|
+
void trackMintTestRunCompleted(nextReport, {
|
|
583
|
+
mode: 'interactive',
|
|
584
|
+
resumed,
|
|
585
|
+
totalPageCount: totalPages,
|
|
586
|
+
});
|
|
569
587
|
setReport(nextReport);
|
|
570
588
|
const failedFiles = nextReport.tasks
|
|
571
589
|
.filter((task) => task.status === 'failed' || task.status === 'agent_error')
|