@mintlify/cli 4.0.1466 → 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 +2 -2
- 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
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
const identifier = z.string().trim().min(1).max(120);
|
|
3
3
|
const commandSchema = z
|
|
4
4
|
.object({
|
|
5
|
-
id: identifier,
|
|
5
|
+
id: identifier.optional(),
|
|
6
6
|
executable: z.string().trim().min(1).max(512),
|
|
7
7
|
args: z.array(z.string().max(10000)).max(100),
|
|
8
8
|
cwd: z.string().max(1024).optional(),
|
|
@@ -14,7 +14,7 @@ const commandSchema = z
|
|
|
14
14
|
.optional(),
|
|
15
15
|
})
|
|
16
16
|
.strict();
|
|
17
|
-
|
|
17
|
+
const manifestFileSchema = z
|
|
18
18
|
.object({
|
|
19
19
|
version: z.literal(1),
|
|
20
20
|
generatedFiles: z.array(z.string().min(1).max(1024)).max(1000),
|
|
@@ -22,6 +22,37 @@ export const manifestSchema = z
|
|
|
22
22
|
testCommands: z.array(commandSchema).max(200),
|
|
23
23
|
})
|
|
24
24
|
.strict();
|
|
25
|
+
/**
|
|
26
|
+
* Agents often copy the manifest example verbatim and omit command ids. The id only needs to be
|
|
27
|
+
* unique within the manifest, so assign `setup-N` / `test-N` instead of rejecting the whole page.
|
|
28
|
+
*/
|
|
29
|
+
export function assignCommandIds(manifest) {
|
|
30
|
+
const taken = new Set([...manifest.setupCommands, ...manifest.testCommands]
|
|
31
|
+
.map((command) => command.id)
|
|
32
|
+
.filter((id) => id !== undefined));
|
|
33
|
+
const withIds = (commands, phase) => {
|
|
34
|
+
let next = 1;
|
|
35
|
+
return commands.map((command) => {
|
|
36
|
+
if (command.id !== undefined)
|
|
37
|
+
return Object.assign(Object.assign({}, command), { id: command.id });
|
|
38
|
+
let id = `${phase}-${next}`;
|
|
39
|
+
while (taken.has(id)) {
|
|
40
|
+
next += 1;
|
|
41
|
+
id = `${phase}-${next}`;
|
|
42
|
+
}
|
|
43
|
+
taken.add(id);
|
|
44
|
+
next += 1;
|
|
45
|
+
return Object.assign(Object.assign({}, command), { id });
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
version: manifest.version,
|
|
50
|
+
generatedFiles: manifest.generatedFiles,
|
|
51
|
+
setupCommands: withIds(manifest.setupCommands, 'setup'),
|
|
52
|
+
testCommands: withIds(manifest.testCommands, 'test'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export const manifestSchema = manifestFileSchema.transform(assignCommandIds);
|
|
25
56
|
export const testCheckSchema = z
|
|
26
57
|
.object({
|
|
27
58
|
version: z.literal(1),
|
package/bin/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _a, _b;
|
|
1
|
+
var _a, _b, _c, _d;
|
|
2
2
|
import { LOCAL_LINKED_CLI_VERSION } from '@mintlify/previewing';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
@@ -15,6 +15,6 @@ const DEV_TOKEN_ENDPOINT = 'https://test.stytch.com/v1/public/project-test-2d863
|
|
|
15
15
|
const DEV_STYTCH_CLIENT_ID = 'connected-app-test-b597afb3-304a-420f-bc13-dacca566c59f';
|
|
16
16
|
const PROD_TOKEN_ENDPOINT = 'https://api.stytch.com/v1/public/project-live-731b7a04-9ac3-4923-90b8-0806d4aa29d4/oauth2/token';
|
|
17
17
|
const PROD_STYTCH_CLIENT_ID = 'connected-app-live-d813eedd-dbb0-434b-a1f9-2ce69e5efc49';
|
|
18
|
-
export const TOKEN_ENDPOINT = IS_LOCAL_BUILD ? DEV_TOKEN_ENDPOINT : PROD_TOKEN_ENDPOINT;
|
|
19
|
-
export const STYTCH_CLIENT_ID = IS_LOCAL_BUILD ? DEV_STYTCH_CLIENT_ID : PROD_STYTCH_CLIENT_ID;
|
|
18
|
+
export const TOKEN_ENDPOINT = (_c = process.env.MINTLIFY_TOKEN_ENDPOINT) !== null && _c !== void 0 ? _c : (IS_LOCAL_BUILD ? DEV_TOKEN_ENDPOINT : PROD_TOKEN_ENDPOINT);
|
|
19
|
+
export const STYTCH_CLIENT_ID = (_d = process.env.MINTLIFY_STYTCH_CLIENT_ID) !== null && _d !== void 0 ? _d : (IS_LOCAL_BUILD ? DEV_STYTCH_CLIENT_ID : PROD_STYTCH_CLIENT_ID);
|
|
20
20
|
export const CUSTOM_DOMAIN_CNAME_TARGET = 'cname.mintlify.builders';
|
package/bin/mintTest.js
CHANGED
|
@@ -8,12 +8,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, discoverCodeBlocks, loadRunSave, runCodeTests, } from './agent-harness/index.js';
|
|
11
|
+
import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, countReport, discoverCodeBlocks, loadLatestTestRun, loadRunSave, runCodeTests, } from './agent-harness/index.js';
|
|
12
|
+
import { API_URL } from './constants.js';
|
|
12
13
|
import { CMD_EXEC_PATH, findDocsRoot, isAI, terminate } from './helpers.js';
|
|
13
14
|
import { getAccessToken } from './keyring.js';
|
|
14
15
|
import { fileInNavPaths, loadDocsNavScope } from './mintTestNav.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { firstLine, taskProblemLabel } from './mintTestText.js';
|
|
17
|
+
import { formatDuration, runMintTestUi } from './mintTestUi.js';
|
|
18
|
+
import { fetchCliStatus } from './status.js';
|
|
19
|
+
const ALLOWED_ORGS = new Set(['mintlify', 'mintlify internal']);
|
|
17
20
|
export function mintTestHandler() {
|
|
18
21
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19
22
|
var _a, _b, _c, _d;
|
|
@@ -24,10 +27,20 @@ export function mintTestHandler() {
|
|
|
24
27
|
yield terminate(1);
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
|
-
const cliStatus = yield
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const cliStatus = yield fetchCliStatus();
|
|
31
|
+
if (!cliStatus.ok) {
|
|
32
|
+
const message = cliStatus.reason === 'unauthenticated'
|
|
33
|
+
? 'your session has expired. Run `mint login` to authenticate.'
|
|
34
|
+
: cliStatus.reason === 'unreachable'
|
|
35
|
+
? `could not reach Mintlify at ${API_URL}${cliStatus.detail ? ` (${cliStatus.detail})` : ''}.`
|
|
36
|
+
: 'unexpected response from Mintlify. Please try again.';
|
|
37
|
+
process.stderr.write(`${message}\n`);
|
|
38
|
+
yield terminate(1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const orgName = cliStatus.status.org.name;
|
|
42
|
+
if (!ALLOWED_ORGS.has(orgName.toLowerCase())) {
|
|
43
|
+
process.stderr.write(`mint test is not available for your organization (${orgName}).\n`);
|
|
31
44
|
yield terminate(1);
|
|
32
45
|
return;
|
|
33
46
|
}
|
|
@@ -43,7 +56,6 @@ export function mintTestHandler() {
|
|
|
43
56
|
? discoveredPages.filter((page) => fileInNavPaths(page.file, scope.navPaths))
|
|
44
57
|
: discoveredPages;
|
|
45
58
|
const autoSelectedFiles = (scope ? pages.filter((page) => fileInNavPaths(page.file, scope.autoSelectedPaths)) : pages).map((page) => page.file);
|
|
46
|
-
const totalPages = scope ? scope.navPaths.size : pages.length;
|
|
47
59
|
const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
|
|
48
60
|
let save = yield loadRunSave(outputDirectory);
|
|
49
61
|
if (save) {
|
|
@@ -57,14 +69,15 @@ export function mintTestHandler() {
|
|
|
57
69
|
save = Object.assign(Object.assign({}, save), { selectedFiles });
|
|
58
70
|
}
|
|
59
71
|
}
|
|
72
|
+
const lastReport = yield loadLatestTestRun(docsRoot);
|
|
60
73
|
if (interactive) {
|
|
61
74
|
const exitCode = yield runMintTestUi({
|
|
62
75
|
targetPath,
|
|
63
76
|
docsRoot,
|
|
64
77
|
pages,
|
|
65
78
|
autoSelectedFiles,
|
|
66
|
-
totalPages,
|
|
67
79
|
save,
|
|
80
|
+
lastReport,
|
|
68
81
|
});
|
|
69
82
|
yield terminate(exitCode);
|
|
70
83
|
return;
|
|
@@ -87,6 +100,15 @@ export function mintTestHandler() {
|
|
|
87
100
|
process.stdout.write(`mint test ${report.status}: ${counts.passed} passed, ${counts.failed} failed, ${counts.agentErrors} agent errors\n`);
|
|
88
101
|
process.stdout.write(`finished in ${formatDuration(report.durationMs)}\n`);
|
|
89
102
|
process.stdout.write(`report: ${report.reportPath}\n`);
|
|
103
|
+
for (const task of report.tasks) {
|
|
104
|
+
if (task.status !== 'failed' && task.status !== 'agent_error')
|
|
105
|
+
continue;
|
|
106
|
+
const reason = firstLine(task.error);
|
|
107
|
+
process.stdout.write(` ✗ ${task.file}: ${taskProblemLabel(task.status)}${reason ? ` (${reason})` : ''}\n`);
|
|
108
|
+
process.stdout.write(` output: ${task.directory}\n`);
|
|
109
|
+
}
|
|
110
|
+
if (report.historyError)
|
|
111
|
+
process.stderr.write(`warning: ${report.historyError}\n`);
|
|
90
112
|
yield terminate(report.status === 'passed' ? 0 : 1);
|
|
91
113
|
}
|
|
92
114
|
catch (error) {
|
|
@@ -0,0 +1,39 @@
|
|
|
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 fs from 'node:fs/promises';
|
|
11
|
+
export const PREVIEW_BYTE_LIMIT = 64 * 1024;
|
|
12
|
+
export function readFilePreview(filePath) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const buffer = new Uint8Array(PREVIEW_BYTE_LIMIT + 1);
|
|
15
|
+
const file = yield fs.open(filePath, 'r');
|
|
16
|
+
let bytesRead = 0;
|
|
17
|
+
try {
|
|
18
|
+
while (bytesRead < buffer.length) {
|
|
19
|
+
const result = yield file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
20
|
+
if (result.bytesRead === 0)
|
|
21
|
+
break;
|
|
22
|
+
bytesRead += result.bytesRead;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
yield file.close();
|
|
27
|
+
}
|
|
28
|
+
const contents = buffer.subarray(0, bytesRead);
|
|
29
|
+
if (contents.includes(0))
|
|
30
|
+
return { binary: true };
|
|
31
|
+
return {
|
|
32
|
+
binary: false,
|
|
33
|
+
text: new TextDecoder()
|
|
34
|
+
.decode(contents.subarray(0, PREVIEW_BYTE_LIMIT))
|
|
35
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '�'),
|
|
36
|
+
truncated: bytesRead > PREVIEW_BYTE_LIMIT,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a footer hint such as `↑/↓/j/k move · Enter exit` into key/action pairs so the key can
|
|
3
|
+
* be rendered bold and the action plain.
|
|
4
|
+
*/
|
|
5
|
+
export function footerSegments(text) {
|
|
6
|
+
return text
|
|
7
|
+
.split(' · ')
|
|
8
|
+
.map((segment) => segment.trim())
|
|
9
|
+
.filter((segment) => segment.length > 0)
|
|
10
|
+
.map((segment) => {
|
|
11
|
+
const split = segment.indexOf(' ');
|
|
12
|
+
if (split === -1)
|
|
13
|
+
return { key: segment, action: '' };
|
|
14
|
+
return { key: segment.slice(0, split), action: segment.slice(split + 1).trim() };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** First non-empty line of a multi-line message, trimmed to fit a terminal row. */
|
|
18
|
+
export function firstLine(text, maxLength = 160) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return '';
|
|
21
|
+
const line = text
|
|
22
|
+
.split(/\r?\n/)
|
|
23
|
+
.map((candidate) => candidate.trim())
|
|
24
|
+
.find((candidate) => candidate.length > 0);
|
|
25
|
+
if (!line)
|
|
26
|
+
return '';
|
|
27
|
+
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
28
|
+
}
|
|
29
|
+
export function taskProblemLabel(status) {
|
|
30
|
+
return status === 'agent_error' ? 'test could not be generated' : 'tests failed';
|
|
31
|
+
}
|