@mintlify/cli 4.0.1466 → 4.0.1468

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 +14 -12
  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 +2 -2
  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 +15 -11
  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
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { PREVIEW_BYTE_LIMIT, readFilePreview } from '../src/mintTestFilePreview.js';
6
+
7
+ let temporaryDirectory: string;
8
+
9
+ beforeEach(async () => {
10
+ temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-preview-'));
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await fs.rm(temporaryDirectory, { recursive: true });
15
+ });
16
+
17
+ it('reads at most the preview limit plus the truncation marker byte', async () => {
18
+ const filePath = path.join(temporaryDirectory, 'large.txt');
19
+ await fs.writeFile(filePath, 'a'.repeat(PREVIEW_BYTE_LIMIT + 10_000));
20
+
21
+ const preview = await readFilePreview(filePath);
22
+
23
+ expect(preview).toEqual({
24
+ binary: false,
25
+ text: 'a'.repeat(PREVIEW_BYTE_LIMIT),
26
+ truncated: true,
27
+ });
28
+ });
29
+
30
+ it('rejects binary previews', async () => {
31
+ const filePath = path.join(temporaryDirectory, 'binary.dat');
32
+ await fs.writeFile(filePath, new Uint8Array([1, 0, 2]));
33
+
34
+ await expect(readFilePreview(filePath)).resolves.toEqual({ binary: true });
35
+ });
@@ -0,0 +1,194 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { executeCodeBlocks } from '../src/agent-harness/executeCodeBlocks.js';
6
+ import { generateTestCode } from '../src/agent-harness/generateTestCode.js';
7
+ import type { GeneratedTask, TaskUpdate } from '../src/agent-harness/types.js';
8
+
9
+ const agentMocks = vi.hoisted(() => ({ codexRun: vi.fn() }));
10
+
11
+ vi.mock('../src/agent-harness/loadAgentSdk.js', () => ({
12
+ loadCodexSdk: vi.fn(async () => ({
13
+ Codex: class {
14
+ startThread() {
15
+ return { run: agentMocks.codexRun };
16
+ }
17
+ },
18
+ })),
19
+ }));
20
+
21
+ const temporaryDirectories: string[] = [];
22
+
23
+ afterEach(async () => {
24
+ vi.clearAllMocks();
25
+ await Promise.all(
26
+ temporaryDirectories.splice(0).map((directory) => fs.rm(directory, { recursive: true }))
27
+ );
28
+ });
29
+
30
+ it('includes absolute generated file paths in updates and results', async () => {
31
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
32
+ temporaryDirectories.push(directory);
33
+ const generatedFile = path.join(directory, 'tests', 'generated.test.ts');
34
+ await fs.mkdir(path.dirname(generatedFile), { recursive: true });
35
+ await fs.writeFile(generatedFile, 'export const generated = true;\n');
36
+ const task: GeneratedTask = {
37
+ id: 'claude:guide.mdx',
38
+ agent: 'claude',
39
+ docsRoot: directory,
40
+ file: 'guide.mdx',
41
+ directory,
42
+ attempts: 1,
43
+ tokens: 10,
44
+ testable: true,
45
+ manifest: {
46
+ version: 1,
47
+ generatedFiles: ['tests/generated.test.ts'],
48
+ setupCommands: [],
49
+ testCommands: [],
50
+ },
51
+ };
52
+ const updates: TaskUpdate[] = [];
53
+
54
+ const results = await executeCodeBlocks({
55
+ tasks: [task],
56
+ docsRoot: directory,
57
+ concurrency: 1,
58
+ commandTimeoutMs: 1_000,
59
+ signal: new AbortController().signal,
60
+ onTaskUpdate: (update) => updates.push(update),
61
+ });
62
+
63
+ expect(results).toHaveLength(1);
64
+ expect(results[0]?.generatedFiles).toEqual([generatedFile]);
65
+ expect(updates.map((update) => update.generatedFiles)).toEqual([
66
+ [generatedFile],
67
+ [generatedFile],
68
+ ]);
69
+ });
70
+
71
+ it('includes partial generated files when generation fails', async () => {
72
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
73
+ temporaryDirectories.push(directory);
74
+ const generatedFile = path.join(directory, 'tests', 'partial.test.ts');
75
+ await fs.mkdir(path.dirname(generatedFile), { recursive: true });
76
+ await fs.writeFile(path.join(directory, 'package.json'), '{}\n');
77
+ agentMocks.codexRun.mockImplementation(async () => {
78
+ await fs.writeFile(generatedFile, 'export const partial = true;\n');
79
+ throw new Error('generation failed');
80
+ });
81
+ const updates: TaskUpdate[] = [];
82
+
83
+ const generated = await generateTestCode({
84
+ tasks: [generatedTask(directory)],
85
+ docsRoot: directory,
86
+ model: 'unused',
87
+ concurrency: 1,
88
+ signal: new AbortController().signal,
89
+ onTaskUpdate: (update) => updates.push(update),
90
+ });
91
+
92
+ expect(updates.at(-1)).toMatchObject({
93
+ phase: 'agent_error',
94
+ generatedFiles: [generatedFile],
95
+ });
96
+ const results = await executeCodeBlocks({
97
+ tasks: generated,
98
+ docsRoot: directory,
99
+ concurrency: 1,
100
+ commandTimeoutMs: 1_000,
101
+ signal: new AbortController().signal,
102
+ onTaskUpdate: (update) => updates.push(update),
103
+ });
104
+ expect(results[0]?.generatedFiles).toEqual([generatedFile]);
105
+ expect(updates.at(-1)).toMatchObject({
106
+ phase: 'agent_error',
107
+ generatedFiles: [generatedFile],
108
+ });
109
+ });
110
+
111
+ it('includes partial generated files when generation is cancelled', async () => {
112
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
113
+ temporaryDirectories.push(directory);
114
+ const generatedFile = path.join(directory, 'tests', 'partial.test.ts');
115
+ await fs.mkdir(path.dirname(generatedFile), { recursive: true });
116
+ const controller = new AbortController();
117
+ agentMocks.codexRun.mockImplementation(async () => {
118
+ await fs.writeFile(generatedFile, 'export const partial = true;\n');
119
+ controller.abort();
120
+ throw new Error('cancelled');
121
+ });
122
+ const updates: TaskUpdate[] = [];
123
+
124
+ const generated = await generateTestCode({
125
+ tasks: [generatedTask(directory)],
126
+ docsRoot: directory,
127
+ model: 'unused',
128
+ concurrency: 1,
129
+ signal: controller.signal,
130
+ onTaskUpdate: (update) => updates.push(update),
131
+ });
132
+
133
+ expect(updates.at(-1)).toMatchObject({
134
+ phase: 'cancelled',
135
+ generatedFiles: [generatedFile],
136
+ });
137
+ const results = await executeCodeBlocks({
138
+ tasks: generated,
139
+ docsRoot: directory,
140
+ concurrency: 1,
141
+ commandTimeoutMs: 1_000,
142
+ signal: controller.signal,
143
+ onTaskUpdate: (update) => updates.push(update),
144
+ });
145
+ expect(results[0]?.generatedFiles).toEqual([generatedFile]);
146
+ expect(updates.at(-1)).toMatchObject({
147
+ phase: 'cancelled',
148
+ generatedFiles: [generatedFile],
149
+ });
150
+ });
151
+
152
+ it.each([
153
+ { outcome: 'fails', aborted: false, status: 'agent_error', error: 'generation failed' },
154
+ { outcome: 'is cancelled', aborted: true, status: 'cancelled', error: 'run cancelled' },
155
+ ] as const)(
156
+ 'forwards partial generated files through execution when generation $outcome',
157
+ async ({ aborted, status, error }) => {
158
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-output-'));
159
+ temporaryDirectories.push(directory);
160
+ const generatedFile = path.join(directory, 'tests', 'partial.test.ts');
161
+ const controller = new AbortController();
162
+ if (aborted) controller.abort();
163
+ const updates: TaskUpdate[] = [];
164
+
165
+ const results = await executeCodeBlocks({
166
+ tasks: [{ ...generatedTask(directory), error, generatedFiles: [generatedFile] }],
167
+ docsRoot: directory,
168
+ concurrency: 1,
169
+ commandTimeoutMs: 1_000,
170
+ signal: controller.signal,
171
+ onTaskUpdate: (update) => updates.push(update),
172
+ });
173
+
174
+ expect(results).toEqual([
175
+ expect.objectContaining({ status, error, generatedFiles: [generatedFile], commands: [] }),
176
+ ]);
177
+ expect(updates).toEqual([
178
+ expect.objectContaining({ phase: status, generatedFiles: [generatedFile] }),
179
+ ]);
180
+ }
181
+ );
182
+
183
+ function generatedTask(directory: string): GeneratedTask {
184
+ return {
185
+ id: 'codex:guide.mdx',
186
+ agent: 'codex',
187
+ docsRoot: directory,
188
+ file: 'guide.mdx',
189
+ directory,
190
+ attempts: 0,
191
+ tokens: 0,
192
+ testable: true,
193
+ };
194
+ }
@@ -0,0 +1,164 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import {
6
+ loadLatestTestRun,
7
+ loadTestRuns,
8
+ recordTestRun,
9
+ testRunReportPath,
10
+ testRunsIndexPath,
11
+ writeTestRunHistory,
12
+ } from '../src/agent-harness/runHistory.js';
13
+ import type { CodeTestReport } from '../src/agent-harness/types.js';
14
+
15
+ const temporaryDirectories: string[] = [];
16
+
17
+ afterEach(async () => {
18
+ await Promise.all(
19
+ temporaryDirectories.splice(0).map((directory) => fs.rm(directory, { recursive: true }))
20
+ );
21
+ });
22
+
23
+ function makeReport(docsRoot: string, runId: string, completedAt: string): CodeTestReport {
24
+ return {
25
+ version: 1,
26
+ runId,
27
+ status: 'passed',
28
+ docsRoot,
29
+ outputDirectory: path.join(docsRoot, 'tests', 'mint-test', runId),
30
+ reportPath: testRunReportPath(docsRoot, runId),
31
+ agents: ['claude'],
32
+ model: 'claude-opus-4-8',
33
+ selectedFiles: ['guide.mdx'],
34
+ startedAt: '2026-09-01T12:00:00.000Z',
35
+ completedAt,
36
+ durationMs: 1_000,
37
+ tasks: [
38
+ {
39
+ id: 'claude:guide.mdx',
40
+ agent: 'claude',
41
+ file: 'guide.mdx',
42
+ directory: path.join(docsRoot, 'tests', 'mint-test', runId, 'claude', '001-guide'),
43
+ generatedFiles: [
44
+ path.join(
45
+ docsRoot,
46
+ 'tests',
47
+ 'mint-test',
48
+ runId,
49
+ 'claude',
50
+ '001-guide',
51
+ 'tests',
52
+ 'guide.test.ts'
53
+ ),
54
+ ],
55
+ testable: true,
56
+ attempts: 1,
57
+ tokens: 100,
58
+ status: 'passed',
59
+ commands: [],
60
+ },
61
+ ],
62
+ };
63
+ }
64
+
65
+ it('stores each result and tracks every run in newest-first order', async () => {
66
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
67
+ temporaryDirectories.push(docsRoot);
68
+ const first = makeReport(docsRoot, '20260901120000-first', '2026-09-01T12:00:01.000Z');
69
+ const second = makeReport(docsRoot, '20260901130000-second', '2026-09-01T13:00:01.000Z');
70
+
71
+ await writeTestRunHistory(docsRoot, first);
72
+ await writeTestRunHistory(docsRoot, second);
73
+
74
+ expect(JSON.parse(await fs.readFile(testRunReportPath(docsRoot, first.runId), 'utf8'))).toEqual(
75
+ first
76
+ );
77
+ const index = await loadTestRuns(docsRoot);
78
+ expect(index.runs.map((run) => run.runId)).toEqual([second.runId, first.runId]);
79
+ expect(index.runs[0]?.counts).toEqual({
80
+ passed: 1,
81
+ failed: 0,
82
+ cancelled: 0,
83
+ agentErrors: 0,
84
+ });
85
+ await expect(loadLatestTestRun(docsRoot)).resolves.toEqual(second);
86
+ });
87
+
88
+ it('rebuilds testRuns.json data from saved run results', async () => {
89
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
90
+ temporaryDirectories.push(docsRoot);
91
+ const report = makeReport(docsRoot, '20260901120000-rebuild', '2026-09-01T12:00:01.000Z');
92
+ await writeTestRunHistory(docsRoot, report);
93
+ await fs.unlink(testRunsIndexPath(docsRoot));
94
+
95
+ const rebuilt = await loadTestRuns(docsRoot);
96
+
97
+ expect(rebuilt.runs).toHaveLength(1);
98
+ expect(rebuilt.runs[0]?.runId).toBe(report.runId);
99
+ });
100
+
101
+ it('keeps every run when history writes overlap', async () => {
102
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
103
+ temporaryDirectories.push(docsRoot);
104
+ const reports = Array.from({ length: 12 }, (_, index) =>
105
+ makeReport(
106
+ docsRoot,
107
+ `20260901120000-concurrent-${index}`,
108
+ `2026-09-01T12:00:${String(index).padStart(2, '0')}.000Z`
109
+ )
110
+ );
111
+
112
+ await Promise.all(reports.map((report) => writeTestRunHistory(docsRoot, report)));
113
+
114
+ const index = JSON.parse(await fs.readFile(testRunsIndexPath(docsRoot), 'utf8')) as {
115
+ runs: Array<{ runId: string }>;
116
+ };
117
+ expect(index.runs.map((run) => run.runId).sort()).toEqual(
118
+ reports.map((report) => report.runId).sort()
119
+ );
120
+ });
121
+
122
+ it('recovers a stale history lock left by an interrupted process', async () => {
123
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
124
+ temporaryDirectories.push(docsRoot);
125
+ const historyDirectory = path.join(docsRoot, '.mintlify', 'test');
126
+ const lockPath = path.join(historyDirectory, 'testRuns.lock');
127
+ await fs.mkdir(historyDirectory, { recursive: true });
128
+ await fs.writeFile(lockPath, 'interrupted-process');
129
+ const staleTime = new Date(Date.now() - 31_000);
130
+ await fs.utimes(lockPath, staleTime, staleTime);
131
+ const report = makeReport(docsRoot, '20260901120000-after-crash', '2026-09-01T12:00:01.000Z');
132
+
133
+ await writeTestRunHistory(docsRoot, report);
134
+
135
+ await expect(fs.access(lockPath)).rejects.toMatchObject({ code: 'ENOENT' });
136
+ await expect(loadLatestTestRun(docsRoot)).resolves.toEqual(report);
137
+ });
138
+
139
+ it('returns the report as-is when history is saved', async () => {
140
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
141
+ temporaryDirectories.push(docsRoot);
142
+ const report = makeReport(docsRoot, '20260901120000-saved', '2026-09-01T12:00:01.000Z');
143
+ const fallbackReportPath = path.join(report.outputDirectory, 'report.json');
144
+
145
+ await expect(recordTestRun(docsRoot, report, fallbackReportPath)).resolves.toBe(report);
146
+ await expect(loadLatestTestRun(docsRoot)).resolves.toEqual(report);
147
+ });
148
+
149
+ it('keeps a finished run usable when history cannot be saved', async () => {
150
+ const docsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mint-test-history-'));
151
+ temporaryDirectories.push(docsRoot);
152
+ await fs.mkdir(path.join(docsRoot, '.mintlify'), { recursive: true });
153
+ await fs.writeFile(path.join(docsRoot, '.mintlify', 'test'), 'blocks the history directory\n');
154
+ const report = makeReport(docsRoot, '20260901120000-unsaved', '2026-09-01T12:00:01.000Z');
155
+ const fallbackReportPath = path.join(report.outputDirectory, 'report.json');
156
+
157
+ const recorded = await recordTestRun(docsRoot, report, fallbackReportPath);
158
+
159
+ expect(recorded).toEqual({
160
+ ...report,
161
+ reportPath: fallbackReportPath,
162
+ historyError: expect.stringContaining('run history could not be saved: '),
163
+ });
164
+ });
@@ -0,0 +1,24 @@
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 { loadClaudeAgentSdk, loadCodexSdk } from './loadAgentSdk.js';
11
+ /**
12
+ * Fail fast, before any page is checked, when an agent SDK is not installed. Without this every
13
+ * page reports the same missing-dependency error minutes later as "test could not be generated".
14
+ */
15
+ export function ensureAgentSdks(agents) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ for (const agent of new Set(agents)) {
18
+ if (agent === 'claude')
19
+ yield loadClaudeAgentSdk();
20
+ else
21
+ yield loadCodexSdk();
22
+ }
23
+ });
24
+ }
@@ -1,8 +1,9 @@
1
1
  import path from 'node:path';
2
+ import { SAFE_EXECUTABLES } from './manifest.js';
2
3
  export const SYSTEM_PROMPT = `# Mint test generator
3
4
 
4
- You will be provided a markdown page guide that contians codeblocks. Follow the guide and implement
5
- the code as instrucuted by the guide into a given output directory.
5
+ You will be given a markdown documentation page that contains code blocks. Follow the page and
6
+ implement the code as the page instructs, inside the given output directory.
6
7
 
7
8
  Once finished, create a mint-test.json file in the current working directory with this shape:
8
9
 
@@ -12,12 +13,14 @@ Once finished, create a mint-test.json file in the current working directory wit
12
13
  "setupCommands": [],
13
14
  "testCommands": [
14
15
  {
16
+ "id": "node-tests",
15
17
  "executable": "node",
16
- "args": ["--test", "tests/"],
18
+ "args": ["--test"],
17
19
  "cwd": ".",
18
20
  "timeoutMs": 120000
19
21
  },
20
22
  {
23
+ "id": "python-tests",
21
24
  "executable": "python3",
22
25
  "args": ["-m", "unittest", "discover", "-s", "tests"],
23
26
  "cwd": ".",
@@ -26,11 +29,18 @@ Once finished, create a mint-test.json file in the current working directory wit
26
29
  ]
27
30
  }
28
31
 
29
- Command cwd values must be relative to the current working directory. Finish with a short plain-text summary.
32
+ Manifest rules:
33
+ - "generatedFiles" lists every file you wrote, as paths relative to the current working directory. Each one must exist.
34
+ - Every command has a unique "id" (letters, digits, dashes). Command "cwd" values are relative to the current working directory.
35
+ - "executable" must be one of: ${[...SAFE_EXECUTABLES].join(', ')}, or a "./" script inside the current working directory. Do not use a shell, pipes, or "&&".
36
+ - Do not add any other keys to the manifest.
37
+
38
+ Finish with a short plain-text summary of what you tested and anything you could not test.
30
39
 
31
40
  Rules:
32
41
  - Read only the page named in the task. You may also read docs.json and an OpenAPI file referenced by that page.
33
- - The output directory is already scaffolded: package.json exists with "npm test" wired to "node --test tests/", and a tests/ directory is ready. Do not recreate or rewrite this scaffolding.
42
+ - The output directory is already scaffolded: package.json exists with "npm test" wired to "node --test", and a tests/ directory is ready. Do not recreate or rewrite this scaffolding.
43
+ - Run JavaScript tests with "node" and args ["--test"] from the task directory (no directory argument: on Node 21+ a path like "tests/" is treated as a glob and finds nothing). Node discovers tests/*.test.js on its own.
34
44
  - Write JavaScript and TypeScript tests as ES modules named tests/*.test.js using the Node built-in test runner (node:test and node:assert). Write Python tests as tests/test_*.py using the standard library unittest module, run with python3 -m unittest discover -s tests.
35
45
  - Prefer the standard library over installing dependencies; add a setup command only when a test truly cannot run without one.
36
46
  - Never write README, docs, or explanation files.
@@ -67,10 +77,10 @@ export const CHECK_JSON_INSTRUCTION = `
67
77
 
68
78
  Respond with only this JSON object and nothing else:
69
79
  { "version": 1, "testable": false, "reason": "expected output, not runnable code" }`;
70
- export function buildCheckPrompt(file, fileContnet) {
80
+ export function buildCheckPrompt(file, fileContent) {
71
81
  return `Report if the file at filePath ${JSON.stringify(file)} is testable
72
82
 
73
83
  <file>
74
- ${fileContnet}
84
+ ${fileContent}
75
85
  </file>`;
76
86
  }
@@ -0,0 +1,10 @@
1
+ export function countReport(tasks) {
2
+ const counts = { passed: 0, failed: 0, cancelled: 0, agentErrors: 0 };
3
+ for (const task of tasks) {
4
+ if (task.status === 'agent_error')
5
+ counts.agentErrors++;
6
+ else
7
+ counts[task.status]++;
8
+ }
9
+ return counts;
10
+ }
@@ -48,6 +48,44 @@ function outputCollector() {
48
48
  value: () => output,
49
49
  };
50
50
  }
51
+ const MAX_FAILURE_LINE_LENGTH = 200;
52
+ const TAP_SUMMARY_PATTERN = /^#\s*(tests|suites|pass|fail|cancelled|skipped|todo|duration_ms)\b|^1\.\.\d+$|^TAP version/;
53
+ const ERROR_LINE_PATTERN = /\b(?:[A-Za-z]*Error|Traceback|FAILED|ModuleNotFoundError)\b/;
54
+ const TAP_METADATA_PATTERN = /^(name|code|failureType|location|stack)\s*:/;
55
+ /**
56
+ * The most useful single line from a failed command's output: the first thrown error, else the
57
+ * first failing TAP assertion, else the last line that is not a TAP summary.
58
+ */
59
+ export function pickFailureLine(text) {
60
+ var _a, _b, _c, _d, _e, _f;
61
+ const lines = text
62
+ .split(/\r?\n/)
63
+ .map((raw) => raw.trim())
64
+ .filter((raw) => raw.length > 0 && !raw.startsWith('[mint test:'))
65
+ .map((raw) => ({ raw, text: raw.replace(/^#\s?/, '').trim() }))
66
+ .filter((line) => line.text.length > 0);
67
+ const thrown = lines.find(({ text: candidate }) => ERROR_LINE_PATTERN.test(candidate) &&
68
+ !candidate.startsWith('at ') &&
69
+ !/^(ok|Traceback)\b/.test(candidate) &&
70
+ !TAP_METADATA_PATTERN.test(candidate));
71
+ const assertion = lines.find(({ text: candidate }) => candidate.startsWith('not ok'));
72
+ const informative = lines.filter(({ raw }) => !TAP_SUMMARY_PATTERN.test(raw));
73
+ const line = (_f = (_d = (_b = (_a = thrown === null || thrown === void 0 ? void 0 : thrown.text) !== null && _a !== void 0 ? _a : assertion === null || assertion === void 0 ? void 0 : assertion.text) !== null && _b !== void 0 ? _b : (_c = informative[informative.length - 1]) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : (_e = lines[lines.length - 1]) === null || _e === void 0 ? void 0 : _e.text) !== null && _f !== void 0 ? _f : '';
74
+ return line.length > MAX_FAILURE_LINE_LENGTH
75
+ ? `${line.slice(0, MAX_FAILURE_LINE_LENGTH - 1)}…`
76
+ : line;
77
+ }
78
+ /** One line explaining the first command that did not pass, for the UI and the report. */
79
+ export function summarizeCommandFailure(commands) {
80
+ var _a;
81
+ const failed = commands.find((command) => command.status !== 'passed');
82
+ if (!failed)
83
+ return undefined;
84
+ const invocation = [failed.executable, ...failed.args].join(' ');
85
+ const reason = (_a = failed.error) !== null && _a !== void 0 ? _a : (failed.exitCode === null ? 'did not exit cleanly' : `exited with code ${failed.exitCode}`);
86
+ const detail = pickFailureLine(failed.stderr) || pickFailureLine(failed.stdout);
87
+ return `${failed.phase} command \`${invocation}\` ${reason}${detail ? `: ${detail}` : ''}`;
88
+ }
51
89
  function unrunCommandResult(command, phase, cwd, status, error) {
52
90
  return {
53
91
  id: command.id,
@@ -137,12 +175,15 @@ function spawnCommand({ command, phase, cwd, timeoutMs, signal, docsRootForComma
137
175
  export function executeCodeBlocks(_a) {
138
176
  return __awaiter(this, arguments, void 0, function* ({ tasks, docsRoot, concurrency, commandTimeoutMs, signal, onTaskUpdate, }) {
139
177
  return mapWithConcurrency(tasks, concurrency, (task) => __awaiter(this, void 0, void 0, function* () {
140
- var _a;
178
+ var _a, _b, _c, _d;
179
+ const generatedFiles = (_c = (_b = (_a = task.manifest) === null || _a === void 0 ? void 0 : _a.generatedFiles.map((file) => path.resolve(task.directory, file))) !== null && _b !== void 0 ? _b : task.generatedFiles) !== null && _c !== void 0 ? _c : [];
141
180
  const base = {
142
181
  id: task.id,
143
182
  agent: task.agent,
144
183
  file: task.file,
145
184
  directory: task.directory,
185
+ generatedFiles,
186
+ testable: task.testable === true,
146
187
  attempts: task.attempts,
147
188
  tokens: task.tokens,
148
189
  };
@@ -153,6 +194,8 @@ export function executeCodeBlocks(_a) {
153
194
  id: task.id,
154
195
  agent: task.agent,
155
196
  file: task.file,
197
+ directory: task.directory,
198
+ generatedFiles,
156
199
  phase: 'not_testable',
157
200
  tokens: task.tokens,
158
201
  });
@@ -163,8 +206,11 @@ export function executeCodeBlocks(_a) {
163
206
  id: task.id,
164
207
  agent: task.agent,
165
208
  file: task.file,
209
+ directory: task.directory,
210
+ generatedFiles,
166
211
  phase: status,
167
212
  tokens: task.tokens,
213
+ error: task.error,
168
214
  });
169
215
  return Object.assign(Object.assign({}, base), { status, error: task.error, commands: [] });
170
216
  }
@@ -172,6 +218,8 @@ export function executeCodeBlocks(_a) {
172
218
  id: task.id,
173
219
  agent: task.agent,
174
220
  file: task.file,
221
+ directory: task.directory,
222
+ generatedFiles,
175
223
  phase: 'running',
176
224
  tokens: task.tokens,
177
225
  });
@@ -206,15 +254,10 @@ export function executeCodeBlocks(_a) {
206
254
  for (const command of manifest.testCommands) {
207
255
  commands.push(yield run(command, 'test'));
208
256
  }
209
- const status = (_a = FAILURE_PRECEDENCE.find((candidate) => commands.some((command) => command.status === candidate))) !== null && _a !== void 0 ? _a : 'passed';
210
- onTaskUpdate({
211
- id: task.id,
212
- agent: task.agent,
213
- file: task.file,
214
- phase: status,
215
- tokens: task.tokens,
216
- });
217
- return Object.assign(Object.assign({}, base), { status, commands });
257
+ const status = (_d = FAILURE_PRECEDENCE.find((candidate) => commands.some((command) => command.status === candidate))) !== null && _d !== void 0 ? _d : 'passed';
258
+ const error = status === 'passed' ? undefined : summarizeCommandFailure(commands);
259
+ onTaskUpdate(Object.assign({ id: task.id, agent: task.agent, file: task.file, directory: task.directory, generatedFiles, phase: status, tokens: task.tokens }, (error ? { error } : {})));
260
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { status }), (error ? { error } : {})), { commands });
218
261
  }));
219
262
  });
220
263
  }