@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/mintTest.tsx
CHANGED
|
@@ -4,16 +4,22 @@ import {
|
|
|
4
4
|
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
5
5
|
DEFAULT_CONCURRENCY,
|
|
6
6
|
clearRunSave,
|
|
7
|
+
countReport,
|
|
7
8
|
discoverCodeBlocks,
|
|
9
|
+
loadLatestTestRun,
|
|
8
10
|
loadRunSave,
|
|
9
11
|
runCodeTests,
|
|
10
12
|
type RunSave,
|
|
11
13
|
} from './agent-harness/index.js';
|
|
14
|
+
import { API_URL } from './constants.js';
|
|
12
15
|
import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
|
|
13
16
|
import { getAccessToken } from './keyring.js';
|
|
14
17
|
import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
18
|
+
import { firstLine, taskProblemLabel } from './mintTestText.js';
|
|
19
|
+
import { formatDuration, runMintTestUi } from './mintTestUi.js';
|
|
20
|
+
import { fetchCliStatus } from './status.js';
|
|
21
|
+
|
|
22
|
+
const ALLOWED_ORGS = new Set(['mintlify', 'mintlify internal']);
|
|
17
23
|
|
|
18
24
|
export async function mintTestHandler(): Promise<void> {
|
|
19
25
|
try {
|
|
@@ -23,10 +29,21 @@ export async function mintTestHandler(): Promise<void> {
|
|
|
23
29
|
await terminate(1);
|
|
24
30
|
return;
|
|
25
31
|
}
|
|
26
|
-
const cliStatus = await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const cliStatus = await fetchCliStatus();
|
|
33
|
+
if (!cliStatus.ok) {
|
|
34
|
+
const message =
|
|
35
|
+
cliStatus.reason === 'unauthenticated'
|
|
36
|
+
? 'your session has expired. Run `mint login` to authenticate.'
|
|
37
|
+
: cliStatus.reason === 'unreachable'
|
|
38
|
+
? `could not reach Mintlify at ${API_URL}${cliStatus.detail ? ` (${cliStatus.detail})` : ''}.`
|
|
39
|
+
: 'unexpected response from Mintlify. Please try again.';
|
|
40
|
+
process.stderr.write(`${message}\n`);
|
|
41
|
+
await terminate(1);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const orgName = cliStatus.status.org.name;
|
|
45
|
+
if (!ALLOWED_ORGS.has(orgName.toLowerCase())) {
|
|
46
|
+
process.stderr.write(`mint test is not available for your organization (${orgName}).\n`);
|
|
30
47
|
await terminate(1);
|
|
31
48
|
return;
|
|
32
49
|
}
|
|
@@ -46,7 +63,6 @@ export async function mintTestHandler(): Promise<void> {
|
|
|
46
63
|
const autoSelectedFiles = (
|
|
47
64
|
scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages
|
|
48
65
|
).map((page) => page.file);
|
|
49
|
-
const totalPages = scope ? scope.navPaths.size : pages.length;
|
|
50
66
|
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
51
67
|
|
|
52
68
|
let save: RunSave | null = await loadRunSave(outputDirectory);
|
|
@@ -60,6 +76,7 @@ export async function mintTestHandler(): Promise<void> {
|
|
|
60
76
|
save = { ...save, selectedFiles };
|
|
61
77
|
}
|
|
62
78
|
}
|
|
79
|
+
const lastReport = await loadLatestTestRun(docsRoot);
|
|
63
80
|
|
|
64
81
|
if (interactive) {
|
|
65
82
|
const exitCode = await runMintTestUi({
|
|
@@ -67,8 +84,8 @@ export async function mintTestHandler(): Promise<void> {
|
|
|
67
84
|
docsRoot,
|
|
68
85
|
pages,
|
|
69
86
|
autoSelectedFiles,
|
|
70
|
-
totalPages,
|
|
71
87
|
save,
|
|
88
|
+
lastReport,
|
|
72
89
|
});
|
|
73
90
|
await terminate(exitCode);
|
|
74
91
|
return;
|
|
@@ -93,6 +110,15 @@ export async function mintTestHandler(): Promise<void> {
|
|
|
93
110
|
);
|
|
94
111
|
process.stdout.write(`finished in ${formatDuration(report.durationMs)}\n`);
|
|
95
112
|
process.stdout.write(`report: ${report.reportPath}\n`);
|
|
113
|
+
for (const task of report.tasks) {
|
|
114
|
+
if (task.status !== 'failed' && task.status !== 'agent_error') continue;
|
|
115
|
+
const reason = firstLine(task.error);
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
` ✗ ${task.file}: ${taskProblemLabel(task.status)}${reason ? ` (${reason})` : ''}\n`
|
|
118
|
+
);
|
|
119
|
+
process.stdout.write(` output: ${task.directory}\n`);
|
|
120
|
+
}
|
|
121
|
+
if (report.historyError) process.stderr.write(`warning: ${report.historyError}\n`);
|
|
96
122
|
await terminate(report.status === 'passed' ? 0 : 1);
|
|
97
123
|
} catch (error) {
|
|
98
124
|
process.stderr.write(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export const PREVIEW_BYTE_LIMIT = 64 * 1024;
|
|
4
|
+
|
|
5
|
+
export type FilePreview = { binary: true } | { binary: false; text: string; truncated: boolean };
|
|
6
|
+
|
|
7
|
+
export async function readFilePreview(filePath: string): Promise<FilePreview> {
|
|
8
|
+
const buffer = new Uint8Array(PREVIEW_BYTE_LIMIT + 1);
|
|
9
|
+
const file = await fs.open(filePath, 'r');
|
|
10
|
+
let bytesRead = 0;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
while (bytesRead < buffer.length) {
|
|
14
|
+
const result = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
15
|
+
if (result.bytesRead === 0) break;
|
|
16
|
+
bytesRead += result.bytesRead;
|
|
17
|
+
}
|
|
18
|
+
} finally {
|
|
19
|
+
await file.close();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const contents = buffer.subarray(0, bytesRead);
|
|
23
|
+
if (contents.includes(0)) return { binary: true };
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
binary: false,
|
|
27
|
+
text: new TextDecoder()
|
|
28
|
+
.decode(contents.subarray(0, PREVIEW_BYTE_LIMIT))
|
|
29
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '�'),
|
|
30
|
+
truncated: bytesRead > PREVIEW_BYTE_LIMIT,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface FooterSegment {
|
|
2
|
+
key: string;
|
|
3
|
+
action: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Split a footer hint such as `↑/↓/j/k move · Enter exit` into key/action pairs so the key can
|
|
8
|
+
* be rendered bold and the action plain.
|
|
9
|
+
*/
|
|
10
|
+
export function footerSegments(text: string): FooterSegment[] {
|
|
11
|
+
return text
|
|
12
|
+
.split(' · ')
|
|
13
|
+
.map((segment) => segment.trim())
|
|
14
|
+
.filter((segment) => segment.length > 0)
|
|
15
|
+
.map((segment) => {
|
|
16
|
+
const split = segment.indexOf(' ');
|
|
17
|
+
if (split === -1) return { key: segment, action: '' };
|
|
18
|
+
return { key: segment.slice(0, split), action: segment.slice(split + 1).trim() };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** First non-empty line of a multi-line message, trimmed to fit a terminal row. */
|
|
23
|
+
export function firstLine(text: string | undefined, maxLength = 160): string {
|
|
24
|
+
if (!text) return '';
|
|
25
|
+
const line = text
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((candidate) => candidate.trim())
|
|
28
|
+
.find((candidate) => candidate.length > 0);
|
|
29
|
+
if (!line) return '';
|
|
30
|
+
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function taskProblemLabel(status: 'failed' | 'agent_error'): string {
|
|
34
|
+
return status === 'agent_error' ? 'test could not be generated' : 'tests failed';
|
|
35
|
+
}
|