@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.
Files changed (40) hide show
  1. package/__test__/mintTestFilePreview.test.ts +35 -0
  2. package/__test__/mintTestOutput.test.ts +194 -0
  3. package/__test__/runHistory.test.ts +164 -0
  4. package/bin/agent-harness/agentPreflight.js +24 -0
  5. package/bin/agent-harness/buildTaskPrompt.js +17 -7
  6. package/bin/agent-harness/countReport.js +10 -0
  7. package/bin/agent-harness/executeCodeBlocks.js +53 -10
  8. package/bin/agent-harness/generateTestCode.js +58 -43
  9. package/bin/agent-harness/index.js +14 -5
  10. package/bin/agent-harness/manifest.js +113 -0
  11. package/bin/agent-harness/runHistory.js +252 -0
  12. package/bin/agent-harness/setupFolders.js +20 -6
  13. package/bin/agent-harness/tasks/checkTestability.js +7 -5
  14. package/bin/agent-harness/types.js +33 -2
  15. package/bin/constants.js +3 -3
  16. package/bin/mintTest.js +31 -9
  17. package/bin/mintTestFilePreview.js +39 -0
  18. package/bin/mintTestText.js +31 -0
  19. package/bin/mintTestUi.js +295 -105
  20. package/bin/status.js +26 -0
  21. package/bin/tsconfig.build.tsbuildinfo +1 -1
  22. package/package.json +7 -7
  23. package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +15 -9
  24. package/src/agent-harness/agentPreflight.ts +13 -0
  25. package/src/agent-harness/buildTaskPrompt.ts +18 -7
  26. package/src/agent-harness/countReport.ts +17 -0
  27. package/src/agent-harness/executeCodeBlocks.ts +67 -1
  28. package/src/agent-harness/generateTestCode.ts +81 -51
  29. package/src/agent-harness/index.ts +20 -5
  30. package/src/agent-harness/manifest.ts +116 -0
  31. package/src/agent-harness/runHistory.ts +250 -0
  32. package/src/agent-harness/setupFolders.ts +19 -5
  33. package/src/agent-harness/tasks/checkTestability.ts +15 -6
  34. package/src/agent-harness/types.ts +81 -5
  35. package/src/constants.ts +6 -2
  36. package/src/mintTest.tsx +34 -8
  37. package/src/mintTestFilePreview.ts +32 -0
  38. package/src/mintTestText.ts +35 -0
  39. package/src/mintTestUi.tsx +586 -224
  40. 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 { countReport, formatDuration, runMintTestUi } from './mintTestUi.js';
16
- import { getCliStatus } from './status.js';
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 getCliStatus(accessToken);
27
- const orgName = cliStatus?.org.name.toLowerCase();
28
- if (orgName !== 'mintlify' && orgName !== 'mintlify internal') {
29
- process.stderr.write('mint test is not available for your organization.\n');
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
+ }