@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
@@ -18,39 +18,11 @@ import fs from 'node:fs/promises';
18
18
  import path from 'node:path';
19
19
  import { SYSTEM_PROMPT, buildTaskPrompt } from './buildTaskPrompt.js';
20
20
  import { loadClaudeAgentSdk, loadCodexSdk } from './loadAgentSdk.js';
21
+ import { MANIFEST_FILENAME, SAFE_EXECUTABLES, appendAgentReply, readManifest, writeErrorDetails, } from './manifest.js';
21
22
  import { mapWithConcurrency } from './mapWithConcurrency.js';
22
23
  import { checkFileTestability } from './tasks/checkTestability.js';
23
- import { manifestSchema, } from './types.js';
24
- const MANIFEST_FILENAME = 'mint-test.json';
25
- const SAFE_EXECUTABLES = new Set([
26
- 'bun',
27
- 'bunx',
28
- 'bundle',
29
- 'cargo',
30
- 'composer',
31
- 'deno',
32
- 'dotnet',
33
- 'go',
34
- 'gradle',
35
- 'java',
36
- 'javac',
37
- 'mvn',
38
- 'node',
39
- 'npm',
40
- 'npx',
41
- 'php',
42
- 'pip',
43
- 'pip3',
44
- 'pnpm',
45
- 'python',
46
- 'python3',
47
- 'pytest',
48
- 'ruby',
49
- 'rustc',
50
- 'uv',
51
- 'vitest',
52
- 'yarn',
53
- ]);
24
+ const SCAFFOLD_FILENAMES = new Set([MANIFEST_FILENAME, 'package.json']);
25
+ const IGNORED_GENERATED_DIRECTORIES = new Set(['.git', 'node_modules']);
54
26
  function promptClaude(_a) {
55
27
  return __awaiter(this, arguments, void 0, function* ({ prompt, systemPrompt, model, workingDirectory, docsRoot, tools, signal, }) {
56
28
  var _b, e_1, _c, _d;
@@ -188,7 +160,7 @@ function validateManifest(manifest, directory) {
188
160
  function readValidManifest(directory) {
189
161
  return __awaiter(this, void 0, void 0, function* () {
190
162
  try {
191
- const manifest = manifestSchema.parse(JSON.parse(yield fs.readFile(path.join(directory, MANIFEST_FILENAME), 'utf8')));
163
+ const manifest = yield readManifest(directory);
192
164
  const errors = yield validateManifest(manifest, directory);
193
165
  return errors.length === 0 ? manifest : null;
194
166
  }
@@ -197,15 +169,47 @@ function readValidManifest(directory) {
197
169
  }
198
170
  });
199
171
  }
172
+ function discoverGeneratedFiles(directory) {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ const realDirectory = yield fs.realpath(directory);
175
+ const files = [];
176
+ function walk(currentDirectory) {
177
+ return __awaiter(this, void 0, void 0, function* () {
178
+ const entries = yield fs.readdir(currentDirectory, { withFileTypes: true }).catch(() => []);
179
+ for (const entry of entries) {
180
+ if (files.length >= 1000)
181
+ return;
182
+ if (currentDirectory === directory && SCAFFOLD_FILENAMES.has(entry.name))
183
+ continue;
184
+ const candidate = path.join(currentDirectory, entry.name);
185
+ if (entry.isDirectory()) {
186
+ if (IGNORED_GENERATED_DIRECTORIES.has(entry.name))
187
+ continue;
188
+ yield walk(candidate);
189
+ continue;
190
+ }
191
+ if (!entry.isFile())
192
+ continue;
193
+ const realPath = yield fs.realpath(candidate).catch(() => undefined);
194
+ if (realPath && isPathInside(realDirectory, realPath))
195
+ files.push(path.resolve(candidate));
196
+ }
197
+ });
198
+ }
199
+ yield walk(directory);
200
+ return files.sort((left, right) => left.localeCompare(right));
201
+ });
202
+ }
200
203
  export const CHECK_CONCURRENCY = 20;
201
204
  export function checkTestability(_a) {
202
- return __awaiter(this, arguments, void 0, function* ({ tasks, signal, onTaskUpdate, }) {
205
+ return __awaiter(this, arguments, void 0, function* ({ tasks, runDirectory, signal, onTaskUpdate, }) {
203
206
  const folderOrdered = [...tasks].sort((left, right) => path.dirname(left.file).localeCompare(path.dirname(right.file)) ||
204
207
  left.file.localeCompare(right.file) ||
205
208
  left.agent.localeCompare(right.agent));
206
209
  return mapWithConcurrency(folderOrdered, CHECK_CONCURRENCY, checkFileTestability, {
207
210
  signal,
208
211
  onTaskUpdate,
212
+ runDirectory,
209
213
  });
210
214
  });
211
215
  }
@@ -214,13 +218,16 @@ export function generateTestCode(_a) {
214
218
  const generateTask = (task) => __awaiter(this, void 0, void 0, function* () {
215
219
  if (task.error || !task.testable)
216
220
  return task;
217
- const emit = (phase, tokens) => onTaskUpdate({ id: task.id, agent: task.agent, file: task.file, phase, tokens });
221
+ const emit = (phase, tokens, generatedFiles = [], error) => onTaskUpdate(Object.assign({ id: task.id, agent: task.agent, file: task.file, directory: task.directory, generatedFiles,
222
+ phase,
223
+ tokens }, (error ? { error } : {})));
218
224
  let attempts = task.attempts;
219
225
  let tokens = task.tokens;
220
226
  let lastError = 'run cancelled';
227
+ const attemptErrors = [];
221
228
  const existing = yield readValidManifest(task.directory);
222
229
  if (existing) {
223
- emit('generated', tokens);
230
+ emit('generated', tokens, existing.generatedFiles.map((file) => path.resolve(task.directory, file)));
224
231
  return Object.assign(Object.assign({}, task), { manifest: existing });
225
232
  }
226
233
  for (let attempt = 1; attempt <= 2 && !signal.aborted; attempt++) {
@@ -247,23 +254,31 @@ export function generateTestCode(_a) {
247
254
  signal,
248
255
  });
249
256
  tokens += reply.tokens;
250
- const manifest = manifestSchema.parse(JSON.parse(yield fs.readFile(path.join(task.directory, MANIFEST_FILENAME), 'utf8')));
257
+ yield appendAgentReply(task.directory, attempt, reply.result);
258
+ const manifest = yield readManifest(task.directory);
251
259
  const errors = yield validateManifest(manifest, task.directory);
252
- if (errors.length > 0)
253
- throw new Error(errors.join('\n'));
254
- emit('generated', tokens);
260
+ if (errors.length > 0) {
261
+ const [first = '', ...rest] = errors;
262
+ throw new Error([`${MANIFEST_FILENAME} has problems: ${first}`, ...rest].join('\n'));
263
+ }
264
+ emit('generated', tokens, manifest.generatedFiles.map((file) => path.resolve(task.directory, file)));
255
265
  return Object.assign(Object.assign({}, task), { attempts, tokens, manifest });
256
266
  }
257
267
  catch (error) {
258
268
  lastError = error instanceof Error ? error.message : String(error);
269
+ attemptErrors.push(`Attempt ${attempt}: ${lastError}`);
259
270
  }
260
271
  }
261
272
  if (signal.aborted) {
262
- emit('cancelled', tokens);
263
- return Object.assign(Object.assign({}, task), { attempts, tokens, error: 'run cancelled' });
273
+ const generatedFiles = yield discoverGeneratedFiles(task.directory);
274
+ emit('cancelled', tokens, generatedFiles, 'run cancelled');
275
+ return Object.assign(Object.assign({}, task), { attempts, tokens, generatedFiles, error: 'run cancelled' });
264
276
  }
265
- emit('agent_error', tokens);
266
- return Object.assign(Object.assign({}, task), { attempts, tokens, error: lastError });
277
+ const generatedFiles = yield discoverGeneratedFiles(task.directory);
278
+ const detailsPath = yield writeErrorDetails(task.directory, attemptErrors);
279
+ const error = detailsPath ? `${lastError}\nDetails: ${detailsPath}` : lastError;
280
+ emit('agent_error', tokens, generatedFiles, error);
281
+ return Object.assign(Object.assign({}, task), { attempts, tokens, generatedFiles, error });
267
282
  });
268
283
  return mapWithConcurrency(tasks, concurrency, generateTask);
269
284
  });
@@ -8,15 +8,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import fs from 'node:fs/promises';
11
+ import { ensureAgentSdks } from './agentPreflight.js';
11
12
  import { discoverCodeBlocks } from './discoverCodeBlocks.js';
12
13
  import { executeCodeBlocks } from './executeCodeBlocks.js';
13
14
  import { filterSelectedPages } from './filterSelectedPages.js';
14
15
  import { checkTestability, generateTestCode } from './generateTestCode.js';
16
+ import { recordTestRun, testRunReportPath } from './runHistory.js';
15
17
  import { clearRunSave, writeRunSave } from './runSave.js';
16
- import { setupFolders } from './setupFolders.js';
18
+ import { scaffoldTaskDirectories, setupFolders } from './setupFolders.js';
17
19
  export const DEFAULT_CONCURRENCY = 5;
18
20
  export const DEFAULT_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
19
21
  export { discoverCodeBlocks } from './discoverCodeBlocks.js';
22
+ export { countReport } from './countReport.js';
23
+ export { loadLatestTestRun, loadTestRuns } from './runHistory.js';
20
24
  export { clearRunSave, loadRunSave } from './runSave.js';
21
25
  export function runCodeTests(_a) {
22
26
  return __awaiter(this, arguments, void 0, function* ({ path: docsPath, agents, model, selectedFiles, outputDirectory, runId: savedRunId, concurrency, commandTimeoutMs, signal, onTaskUpdate, }) {
@@ -24,13 +28,16 @@ export function runCodeTests(_a) {
24
28
  const uniqueAgents = [...new Set(agents)];
25
29
  const { docsRoot, pages: allPages } = yield discoverCodeBlocks(docsPath);
26
30
  const pages = filterSelectedPages(allPages, selectedFiles);
27
- const { runId, runDirectory, reportPath, tasks } = yield setupFolders({
31
+ if (pages.length > 0)
32
+ yield ensureAgentSdks(uniqueAgents);
33
+ const { runId, runDirectory, reportPath: legacyReportPath, tasks, } = yield setupFolders({
28
34
  pages,
29
35
  agents: uniqueAgents,
30
36
  docsRoot,
31
37
  outputDirectory,
32
38
  runId: savedRunId,
33
39
  });
40
+ const reportPath = testRunReportPath(docsRoot, runId);
34
41
  yield writeRunSave(outputDirectory, {
35
42
  version: 1,
36
43
  runId,
@@ -39,7 +46,8 @@ export function runCodeTests(_a) {
39
46
  selectedFiles: pages.map((page) => page.file),
40
47
  createdAt: startedAt.toISOString(),
41
48
  });
42
- const checked = yield checkTestability({ tasks, signal, onTaskUpdate });
49
+ const checked = yield checkTestability({ tasks, runDirectory, signal, onTaskUpdate });
50
+ yield scaffoldTaskDirectories(checked.filter((task) => task.testable === true));
43
51
  const generated = yield generateTestCode({
44
52
  tasks: checked,
45
53
  docsRoot,
@@ -71,15 +79,16 @@ export function runCodeTests(_a) {
71
79
  outputDirectory: runDirectory,
72
80
  reportPath,
73
81
  agents: uniqueAgents,
82
+ model,
74
83
  selectedFiles: pages.map((page) => page.file),
75
84
  startedAt: startedAt.toISOString(),
76
85
  completedAt: completedAt.toISOString(),
77
86
  durationMs: completedAt.getTime() - startedAt.getTime(),
78
87
  tasks: results,
79
88
  };
80
- yield fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
89
+ yield fs.writeFile(legacyReportPath, `${JSON.stringify(report, null, 2)}\n`);
81
90
  if (!signal.aborted)
82
91
  yield clearRunSave(outputDirectory);
83
- return report;
92
+ return recordTestRun(docsRoot, report, legacyReportPath);
84
93
  });
85
94
  }
@@ -0,0 +1,113 @@
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
+ import path from 'node:path';
12
+ import { manifestSchema } from './types.js';
13
+ export const MANIFEST_FILENAME = 'mint-test.json';
14
+ export const AGENT_REPLY_FILENAME = 'mint-test-agent.md';
15
+ export const ERROR_FILENAME = 'mint-test-error.txt';
16
+ export const SAFE_EXECUTABLES = new Set([
17
+ 'bun',
18
+ 'bunx',
19
+ 'bundle',
20
+ 'cargo',
21
+ 'composer',
22
+ 'deno',
23
+ 'dotnet',
24
+ 'go',
25
+ 'gradle',
26
+ 'java',
27
+ 'javac',
28
+ 'mvn',
29
+ 'node',
30
+ 'npm',
31
+ 'npx',
32
+ 'php',
33
+ 'pip',
34
+ 'pip3',
35
+ 'pnpm',
36
+ 'python',
37
+ 'python3',
38
+ 'pytest',
39
+ 'ruby',
40
+ 'rustc',
41
+ 'uv',
42
+ 'vitest',
43
+ 'yarn',
44
+ ]);
45
+ function isMissingFile(error) {
46
+ return (typeof error === 'object' && error !== null && error.code === 'ENOENT');
47
+ }
48
+ /** Turn a schema failure into one readable line per problem, e.g. `testCommands.0.id: expected string`. */
49
+ export function formatManifestIssues(error) {
50
+ return error.issues
51
+ .map((issue) => {
52
+ const location = issue.path.map(String).join('.');
53
+ return `${location || MANIFEST_FILENAME}: ${issue.message}`;
54
+ })
55
+ .join('\n');
56
+ }
57
+ /**
58
+ * Read and schema-check the manifest an agent wrote. Every failure mode has a message a
59
+ * documentation author can act on, because it is what `mint test` shows for the page.
60
+ */
61
+ export function readManifest(directory) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ const manifestPath = path.join(directory, MANIFEST_FILENAME);
64
+ let raw;
65
+ try {
66
+ raw = yield fs.readFile(manifestPath, 'utf8');
67
+ }
68
+ catch (error) {
69
+ if (isMissingFile(error)) {
70
+ throw new Error(`The agent finished without writing ${MANIFEST_FILENAME}`);
71
+ }
72
+ throw error;
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(raw);
77
+ }
78
+ catch (error) {
79
+ const reason = error instanceof Error ? error.message : String(error);
80
+ throw new Error(`${MANIFEST_FILENAME} is not valid JSON: ${reason}`);
81
+ }
82
+ const result = manifestSchema.safeParse(parsed);
83
+ if (!result.success) {
84
+ const [first = '', ...rest] = formatManifestIssues(result.error).split('\n');
85
+ throw new Error([`${MANIFEST_FILENAME} does not match the expected shape: ${first}`, ...rest].join('\n'));
86
+ }
87
+ return result.data;
88
+ });
89
+ }
90
+ /** Keep the agent's final message next to its output so a failure can be traced later. */
91
+ export function appendAgentReply(directory, attempt, reply) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const body = reply.trim() || '(no summary)';
94
+ yield fs
95
+ .appendFile(path.join(directory, AGENT_REPLY_FILENAME), `## Attempt ${attempt}\n\n${body}\n\n`)
96
+ .catch(() => { });
97
+ });
98
+ }
99
+ /** Persist every attempt's failure reason. Returns the file path, or undefined if it could not be written. */
100
+ export function writeErrorDetails(directory, errors) {
101
+ return __awaiter(this, void 0, void 0, function* () {
102
+ if (errors.length === 0)
103
+ return undefined;
104
+ const errorPath = path.join(directory, ERROR_FILENAME);
105
+ try {
106
+ yield fs.writeFile(errorPath, `${errors.join('\n\n')}\n`);
107
+ return errorPath;
108
+ }
109
+ catch (_a) {
110
+ return undefined;
111
+ }
112
+ });
113
+ }
@@ -0,0 +1,252 @@
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 { randomUUID } from 'node:crypto';
11
+ import fs from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { setTimeout as delay } from 'node:timers/promises';
14
+ import { z } from 'zod';
15
+ import { countReport } from './countReport.js';
16
+ const RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
17
+ const INDEX_LOCK_FILENAME = 'testRuns.lock';
18
+ const LOCK_RETRY_MS = 20;
19
+ const STALE_LOCK_MS = 30000;
20
+ const LOCK_TIMEOUT_MS = STALE_LOCK_MS + 10000;
21
+ const commandResultSchema = z
22
+ .object({
23
+ id: z.string(),
24
+ phase: z.enum(['setup', 'test']),
25
+ status: z.enum(['passed', 'failed', 'cancelled']),
26
+ executable: z.string(),
27
+ args: z.array(z.string()),
28
+ cwd: z.string(),
29
+ durationMs: z.number().nonnegative(),
30
+ exitCode: z.number().int().nullable(),
31
+ stdout: z.string(),
32
+ stderr: z.string(),
33
+ error: z.string().optional(),
34
+ })
35
+ .strict();
36
+ const taskResultSchema = z
37
+ .object({
38
+ id: z.string(),
39
+ agent: z.enum(['claude', 'codex']),
40
+ file: z.string(),
41
+ directory: z.string(),
42
+ generatedFiles: z.array(z.string()),
43
+ testable: z.boolean(),
44
+ attempts: z.number().int().nonnegative(),
45
+ tokens: z.number().nonnegative(),
46
+ status: z.enum(['passed', 'failed', 'agent_error', 'cancelled']),
47
+ error: z.string().optional(),
48
+ commands: z.array(commandResultSchema),
49
+ })
50
+ .strict();
51
+ const codeTestReportSchema = z
52
+ .object({
53
+ version: z.literal(1),
54
+ runId: z.string().regex(RUN_ID_PATTERN),
55
+ status: z.enum(['passed', 'completed', 'failed', 'cancelled']),
56
+ docsRoot: z.string(),
57
+ outputDirectory: z.string(),
58
+ reportPath: z.string(),
59
+ agents: z.array(z.enum(['claude', 'codex'])).min(1),
60
+ model: z.string().min(1),
61
+ selectedFiles: z.array(z.string()),
62
+ startedAt: z.string(),
63
+ completedAt: z.string(),
64
+ durationMs: z.number().nonnegative(),
65
+ tasks: z.array(taskResultSchema),
66
+ historyError: z.string().optional(),
67
+ })
68
+ .strict();
69
+ const countsSchema = z
70
+ .object({
71
+ passed: z.number().int().nonnegative(),
72
+ failed: z.number().int().nonnegative(),
73
+ cancelled: z.number().int().nonnegative(),
74
+ agentErrors: z.number().int().nonnegative(),
75
+ })
76
+ .strict();
77
+ const testRunSummarySchema = z
78
+ .object({
79
+ runId: z.string().regex(RUN_ID_PATTERN),
80
+ status: z.enum(['passed', 'completed', 'failed', 'cancelled']),
81
+ agents: z.array(z.enum(['claude', 'codex'])).min(1),
82
+ model: z.string().min(1),
83
+ selectedFiles: z.array(z.string()),
84
+ startedAt: z.string(),
85
+ completedAt: z.string(),
86
+ durationMs: z.number().nonnegative(),
87
+ reportPath: z.string(),
88
+ outputDirectory: z.string(),
89
+ counts: countsSchema,
90
+ })
91
+ .strict();
92
+ const testRunsIndexSchema = z
93
+ .object({ version: z.literal(1), runs: z.array(testRunSummarySchema) })
94
+ .strict();
95
+ function historyDirectory(docsRoot) {
96
+ return path.join(docsRoot, '.mintlify', 'test');
97
+ }
98
+ function runsDirectory(docsRoot) {
99
+ return path.join(historyDirectory(docsRoot), 'runs');
100
+ }
101
+ export function testRunReportPath(docsRoot, runId) {
102
+ if (!RUN_ID_PATTERN.test(runId))
103
+ throw new Error(`Invalid mint test run ID: ${runId}`);
104
+ return path.join(runsDirectory(docsRoot), `${runId}.json`);
105
+ }
106
+ export function testRunsIndexPath(docsRoot) {
107
+ return path.join(historyDirectory(docsRoot), 'testRuns.json');
108
+ }
109
+ function summarizeReport(report) {
110
+ return {
111
+ runId: report.runId,
112
+ status: report.status,
113
+ agents: report.agents,
114
+ model: report.model,
115
+ selectedFiles: report.selectedFiles,
116
+ startedAt: report.startedAt,
117
+ completedAt: report.completedAt,
118
+ durationMs: report.durationMs,
119
+ reportPath: report.reportPath,
120
+ outputDirectory: report.outputDirectory,
121
+ counts: countReport(report.tasks),
122
+ };
123
+ }
124
+ function errorCode(error) {
125
+ return typeof error === 'object' && error !== null && 'code' in error
126
+ ? String(error.code)
127
+ : undefined;
128
+ }
129
+ function writeJsonAtomically(filePath, value) {
130
+ return __awaiter(this, void 0, void 0, function* () {
131
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
132
+ try {
133
+ yield fs.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
134
+ yield fs.rename(temporaryPath, filePath);
135
+ }
136
+ finally {
137
+ yield fs.unlink(temporaryPath).catch(() => undefined);
138
+ }
139
+ });
140
+ }
141
+ function acquireIndexLock(docsRoot) {
142
+ return __awaiter(this, void 0, void 0, function* () {
143
+ const lockPath = path.join(historyDirectory(docsRoot), INDEX_LOCK_FILENAME);
144
+ const token = `${process.pid}:${randomUUID()}`;
145
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
146
+ while (Date.now() < deadline) {
147
+ try {
148
+ const lock = yield fs.open(lockPath, 'wx');
149
+ try {
150
+ yield lock.writeFile(token);
151
+ }
152
+ finally {
153
+ yield lock.close();
154
+ }
155
+ return () => __awaiter(this, void 0, void 0, function* () {
156
+ const owner = yield fs.readFile(lockPath, 'utf8').catch(() => undefined);
157
+ if (owner === token)
158
+ yield fs.unlink(lockPath).catch(() => undefined);
159
+ });
160
+ }
161
+ catch (error) {
162
+ if (errorCode(error) !== 'EEXIST')
163
+ throw error;
164
+ }
165
+ const stats = yield fs.stat(lockPath).catch(() => undefined);
166
+ if (stats && Date.now() - stats.mtimeMs > STALE_LOCK_MS) {
167
+ yield fs.unlink(lockPath).catch(() => undefined);
168
+ continue;
169
+ }
170
+ yield delay(LOCK_RETRY_MS);
171
+ }
172
+ throw new Error('Timed out updating mint test run history');
173
+ });
174
+ }
175
+ function readReport(filePath) {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ try {
178
+ return codeTestReportSchema.parse(JSON.parse(yield fs.readFile(filePath, 'utf8')));
179
+ }
180
+ catch (_a) {
181
+ return null;
182
+ }
183
+ });
184
+ }
185
+ function rebuildIndex(docsRoot) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ const entries = yield fs
188
+ .readdir(runsDirectory(docsRoot), { withFileTypes: true })
189
+ .catch(() => []);
190
+ const reports = yield Promise.all(entries
191
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
192
+ .map((entry) => readReport(path.join(runsDirectory(docsRoot), entry.name))));
193
+ return {
194
+ version: 1,
195
+ runs: reports
196
+ .filter((report) => report !== null)
197
+ .map(summarizeReport)
198
+ .sort((left, right) => right.completedAt.localeCompare(left.completedAt)),
199
+ };
200
+ });
201
+ }
202
+ export function loadTestRuns(docsRoot) {
203
+ return __awaiter(this, void 0, void 0, function* () {
204
+ try {
205
+ return testRunsIndexSchema.parse(JSON.parse(yield fs.readFile(testRunsIndexPath(docsRoot), 'utf8')));
206
+ }
207
+ catch (_a) {
208
+ return rebuildIndex(docsRoot);
209
+ }
210
+ });
211
+ }
212
+ export function loadLatestTestRun(docsRoot) {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const index = yield loadTestRuns(docsRoot);
215
+ for (const run of index.runs) {
216
+ const report = yield readReport(testRunReportPath(docsRoot, run.runId));
217
+ if (report)
218
+ return report;
219
+ }
220
+ return null;
221
+ });
222
+ }
223
+ export function writeTestRunHistory(docsRoot, report) {
224
+ return __awaiter(this, void 0, void 0, function* () {
225
+ yield fs.mkdir(runsDirectory(docsRoot), { recursive: true });
226
+ yield writeJsonAtomically(report.reportPath, report);
227
+ const releaseLock = yield acquireIndexLock(docsRoot);
228
+ try {
229
+ yield writeJsonAtomically(testRunsIndexPath(docsRoot), yield rebuildIndex(docsRoot));
230
+ }
231
+ finally {
232
+ yield releaseLock();
233
+ }
234
+ });
235
+ }
236
+ /**
237
+ * Saves a finished run to history without letting a history failure fail the run.
238
+ * On failure the returned report points at `fallbackReportPath` (the report that was
239
+ * already written) and carries the problem in `historyError`.
240
+ */
241
+ export function recordTestRun(docsRoot, report, fallbackReportPath) {
242
+ return __awaiter(this, void 0, void 0, function* () {
243
+ try {
244
+ yield writeTestRunHistory(docsRoot, report);
245
+ return report;
246
+ }
247
+ catch (error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ return Object.assign(Object.assign({}, report), { reportPath: fallbackReportPath, historyError: `run history could not be saved: ${message}` });
250
+ }
251
+ });
252
+ }
@@ -14,14 +14,30 @@ const PACKAGE_JSON = `${JSON.stringify({
14
14
  name: 'mint-test-task',
15
15
  private: true,
16
16
  type: 'module',
17
- scripts: { test: 'node --test tests/' },
17
+ // No positional path: Node 21+ treats --test arguments as globs, so a bare
18
+ // directory such as `tests/` fails with MODULE_NOT_FOUND. The default patterns
19
+ // pick up tests/*.test.js on every supported Node version.
20
+ scripts: { test: 'node --test' },
18
21
  }, null, 2)}\n`;
22
+ function isExistingFile(error) {
23
+ return (typeof error === 'object' && error !== null && error.code === 'EEXIST');
24
+ }
19
25
  function scaffoldTaskDirectory(directory) {
20
26
  return __awaiter(this, void 0, void 0, function* () {
21
27
  yield fs.mkdir(path.join(directory, 'tests'), { recursive: true });
22
- yield fs
23
- .writeFile(path.join(directory, 'package.json'), PACKAGE_JSON, { flag: 'wx' })
24
- .catch(() => { });
28
+ try {
29
+ yield fs.writeFile(path.join(directory, 'package.json'), PACKAGE_JSON, { flag: 'wx' });
30
+ }
31
+ catch (error) {
32
+ if (!isExistingFile(error))
33
+ throw error;
34
+ }
35
+ });
36
+ }
37
+ export function scaffoldTaskDirectories(tasks) {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ for (const task of tasks)
40
+ yield scaffoldTaskDirectory(task.directory);
25
41
  });
26
42
  }
27
43
  export function setupFolders(_a) {
@@ -52,8 +68,6 @@ export function setupFolders(_a) {
52
68
  });
53
69
  }
54
70
  }
55
- for (const task of tasks)
56
- yield scaffoldTaskDirectory(task.directory);
57
71
  return { runId, runDirectory, reportPath, tasks };
58
72
  });
59
73
  }
@@ -47,9 +47,8 @@ function parseJsonReply(reply) {
47
47
  return JSON.parse(raw.slice(start, end + 1));
48
48
  }
49
49
  function runTestabilityCheck(_a) {
50
- return __awaiter(this, arguments, void 0, function* ({ file, content, agent, outputPath, signal, }) {
50
+ return __awaiter(this, arguments, void 0, function* ({ file, content, agent, workingDirectory, signal, }) {
51
51
  const prompt = buildCheckPrompt(file, content);
52
- const workingDirectory = path.dirname(outputPath);
53
52
  let tokens = 0;
54
53
  let raw;
55
54
  if (agent === 'claude') {
@@ -73,15 +72,16 @@ function runTestabilityCheck(_a) {
73
72
  const errors = validateCheck(check);
74
73
  if (errors.length > 0)
75
74
  throw new Error(errors.join('\n'));
76
- yield fs.writeFile(outputPath, `${JSON.stringify(check, null, 2)}\n`);
77
75
  return { tokens, check };
78
76
  });
79
77
  }
80
- export const checkFileTestability = (task_1, _a) => __awaiter(void 0, [task_1, _a], void 0, function* (task, { onTaskUpdate, signal }) {
78
+ export const checkFileTestability = (task_1, _a) => __awaiter(void 0, [task_1, _a], void 0, function* (task, { onTaskUpdate, signal, runDirectory, }) {
81
79
  const emit = (phase, tokens) => onTaskUpdate({
82
80
  id: task.id,
83
81
  agent: task.agent,
84
82
  file: task.file,
83
+ directory: task.directory,
84
+ generatedFiles: [],
85
85
  phase,
86
86
  tokens,
87
87
  });
@@ -99,7 +99,7 @@ export const checkFileTestability = (task_1, _a) => __awaiter(void 0, [task_1, _
99
99
  file: task.file,
100
100
  content,
101
101
  agent: task.agent,
102
- outputPath: path.join(task.directory, CHECK_FILENAME),
102
+ workingDirectory: runDirectory,
103
103
  signal,
104
104
  });
105
105
  tokens += reply.tokens;
@@ -122,6 +122,8 @@ export const checkFileTestability = (task_1, _a) => __awaiter(void 0, [task_1, _
122
122
  emit('not_testable', tokens);
123
123
  return Object.assign(Object.assign({}, task), { attempts, tokens, testable: false });
124
124
  }
125
+ yield fs.mkdir(task.directory, { recursive: true });
126
+ yield fs.writeFile(path.join(task.directory, CHECK_FILENAME), `${JSON.stringify(check, null, 2)}\n`);
125
127
  emit('checked', tokens);
126
128
  return Object.assign(Object.assign({}, task), { attempts, tokens, testable: true });
127
129
  });