mandrel 2.19.0 → 2.21.0
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/.agents/agents/story-worker.md +10 -0
- package/.agents/docs/agentrc-reference.json +0 -21
- package/.agents/docs/configuration.md +11 -14
- package/.agents/docs/execution-reference.md +8 -5
- package/.agents/schemas/agentrc.schema.json +0 -31
- package/.agents/scripts/check-test-temp-hygiene.js +153 -14
- package/.agents/scripts/deliver-light.js +72 -8
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +3 -4
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -2
- package/.agents/scripts/lib/config/explain.js +0 -8
- package/.agents/scripts/lib/config/temp-paths.js +74 -1
- package/.agents/scripts/lib/config-settings-schema.js +8 -24
- package/.agents/scripts/lib/orchestration/complexity-gate.js +68 -6
- package/.agents/scripts/lib/orchestration/deliver-recover.js +253 -6
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -2
- package/.agents/scripts/lib/orchestration/light-suitability.js +194 -11
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -14
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +12 -66
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +11 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +1 -1
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +117 -4
- package/.agents/scripts/lib/temp-retention.js +23 -8
- package/.agents/scripts/lib/test-env.js +15 -3
- package/.agents/scripts/lib/test-temp.js +311 -0
- package/.agents/scripts/single-story-confirm-merge.js +1 -1
- package/.agents/workflows/helpers/deliver-light.md +45 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +33 -0
- package/.agents/workflows/helpers/deliver-story.md +6 -3
- package/.agents/workflows/helpers/plan-reference.md +19 -4
- package/.agents/workflows/plan.md +7 -6
- package/docs/CHANGELOG.md +16 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +113 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/codebase-snapshot.js +0 -513
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +0 -147
- package/.agents/scripts/lib/orchestration/spec-freshness.js +0 -129
|
@@ -14,15 +14,12 @@ import {
|
|
|
14
14
|
verifyBddRunnerPendingTag,
|
|
15
15
|
} from '../../bdd-runner-detect.js';
|
|
16
16
|
import { scanBddScenarios } from '../../bdd-scenario-scanner.js';
|
|
17
|
-
import { buildCodebaseSnapshot } from '../../codebase-snapshot.js';
|
|
18
17
|
import { getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
19
18
|
import { scanMemoryFreshness } from '../../feedback-loop/memory-freshness.js';
|
|
20
19
|
import { fetchPriorFeedback } from '../../feedback-loop/prior-feedback-fetcher.js';
|
|
21
20
|
import { Logger } from '../../Logger.js';
|
|
22
21
|
import { hasTicketSection } from '../../ticket-body-sections.js';
|
|
23
22
|
import { ensureDocsDigest } from '../docs-digest.js';
|
|
24
|
-
import { collectReferences, hasNewFileCue } from '../spec-freshness.js';
|
|
25
|
-
import { buildAuthoringGrounding } from './spec-authoring-grounding.js';
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* Resolve the per-project memory directory used by the memory-freshness
|
|
@@ -179,68 +176,18 @@ export async function buildAuthoringContext(
|
|
|
179
176
|
repo: githubCfg?.repo,
|
|
180
177
|
});
|
|
181
178
|
|
|
182
|
-
// Story #
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
exclude: settings?.planning?.codebaseSnapshot?.exclude,
|
|
195
|
-
recentCommitWindow:
|
|
196
|
-
settings?.planning?.codebaseSnapshot?.recentCommitWindow,
|
|
197
|
-
});
|
|
198
|
-
// Story #4139 (F10) — ground the spec author in the files it will cite.
|
|
199
|
-
// Two signals are attached to the snapshot envelope so the author (which
|
|
200
|
-
// consumes the JSON, not stderr) cannot miss them:
|
|
201
|
-
// 1. `grounding.truncation` — the structured, in-envelope form of the
|
|
202
|
-
// Story #3959 dropped-file warning. The skinny-tier cap used to drop
|
|
203
|
-
// the majority of matched files with only a stderr `Logger.warn` and
|
|
204
|
-
// a bare `truncated: true` flag; the author never learned the
|
|
205
|
-
// snapshot was partial (a real run dropped "377 of 627 files").
|
|
206
|
-
// 2. `grounding.citedButAbsent` — path-shaped references in the Epic
|
|
207
|
-
// body (the prose the author grounds *from*) that are absent from
|
|
208
|
-
// the snapshot's file set and not phrased as net-new, so cited-but-
|
|
209
|
-
// absent surfaces are visible *during* authoring rather than only
|
|
210
|
-
// after the post-author freshness gate (Story #2635).
|
|
211
|
-
// The grounding consults only the snapshot's file set and the Epic body —
|
|
212
|
-
// no new filesystem or git probes — so the context stays bounded for cost.
|
|
213
|
-
if (codebaseSnapshot) {
|
|
214
|
-
const grounding = buildAuthoringGrounding({
|
|
215
|
-
snapshot: codebaseSnapshot,
|
|
216
|
-
prose: epic.body ?? '',
|
|
217
|
-
collectReferences,
|
|
218
|
-
hasNewFileCue,
|
|
219
|
-
});
|
|
220
|
-
codebaseSnapshot.grounding = grounding;
|
|
221
|
-
if (grounding.truncation) {
|
|
222
|
-
const { dropped, matched, tier } = grounding.truncation;
|
|
223
|
-
Logger.warn(
|
|
224
|
-
`[plan-context] codebase snapshot truncated: ${dropped} of ` +
|
|
225
|
-
`${matched} matched file(s) dropped from the ${tier}-tier view. ` +
|
|
226
|
-
`The /plan authoring context is partial. To restore full ` +
|
|
227
|
-
`grounding, ` +
|
|
228
|
-
`set planning.codebaseSnapshot.tier: "medium" and/or narrow ` +
|
|
229
|
-
`planning.codebaseSnapshot.include in .agentrc.json.`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
if (grounding.citedButAbsent.length > 0) {
|
|
233
|
-
Logger.warn(
|
|
234
|
-
`[plan-context] ${grounding.citedButAbsent.length} path(s) cited ` +
|
|
235
|
-
`in the authored Spec are absent from the codebase snapshot: ` +
|
|
236
|
-
`${grounding.citedButAbsent.join(', ')}. /plan will flag these ` +
|
|
237
|
-
`as drift unless they are net-new.`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
} catch (err) {
|
|
242
|
-
Logger.warn(`[plan-context] codebase snapshot skipped: ${err.message}`);
|
|
243
|
-
}
|
|
179
|
+
// Story #4811 — the codebase snapshot (#2634), its authoring grounding
|
|
180
|
+
// (#4139 F10) and the spec-freshness helpers behind it are retired. The
|
|
181
|
+
// pre-computed structural view grounded nothing it promised: the default
|
|
182
|
+
// include globs missed the standard monorepo layout outright, its remedies
|
|
183
|
+
// pointed at knobs that re-filtered the same set, and its cited-but-absent
|
|
184
|
+
// signal inverted into noise whenever the snapshot was the thing that was
|
|
185
|
+
// wrong. Grounding now rests on the two mechanisms that read the real tree:
|
|
186
|
+
// the authoring model's own targeted retrieval (the digest-first precedent
|
|
187
|
+
// of Story #4433) and the Phase 8 `validateStoryFileAssumptions` gate, which
|
|
188
|
+
// probes every authored `{path, assumption}` against the working tree as a
|
|
189
|
+
// hard error. Nothing pre-computed replaces it — a stale inventory is the
|
|
190
|
+
// failure mode, not the fix.
|
|
244
191
|
|
|
245
192
|
// Story #4542 — planning authors no risk artifact at all. Review depth and
|
|
246
193
|
// the acceptance-critic mode are derived from the diff at close time
|
|
@@ -262,7 +209,6 @@ export async function buildAuthoringContext(
|
|
|
262
209
|
},
|
|
263
210
|
},
|
|
264
211
|
docsContext,
|
|
265
|
-
codebaseSnapshot,
|
|
266
212
|
bddRunner,
|
|
267
213
|
bddScenarios,
|
|
268
214
|
memoryFreshness,
|
|
@@ -70,7 +70,17 @@ import { Logger, resolveLevel } from '../../Logger.js';
|
|
|
70
70
|
*/
|
|
71
71
|
export const REPLAY_TAIL_LINES = 200;
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Basename of the per-Story gate log inside the temp directory.
|
|
75
|
+
*
|
|
76
|
+
* `closeGateLogPath` in `lib/config/temp-paths.js` spells the same name for the
|
|
77
|
+
* READER — `deliver-recover.js` uses this file's freshness to tell a live close
|
|
78
|
+
* from a dead one. Deliberately not shared through an import: this sink needs
|
|
79
|
+
* the basename alone (it honours a `logDir` override the path helper knows
|
|
80
|
+
* nothing about), and calling that helper for it would drag tempRoot
|
|
81
|
+
* resolution — a git spawn and scratch-dir creation — into a filename lookup.
|
|
82
|
+
* The two spellings are pinned equal by test instead.
|
|
83
|
+
*/
|
|
74
84
|
function logNameFor(storyId) {
|
|
75
85
|
return `close-gates-${storyId ?? 'unknown'}.log`;
|
|
76
86
|
}
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* pre-#4543 pipeline collapsed by treating budget exhaustion as a block.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import nodeFs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { storyTerminalEnvelopePath } from '../config/temp-paths.js';
|
|
34
|
+
import { resolveConfig } from '../config-resolver.js';
|
|
31
35
|
import { validateTerminalEnvelope } from './story-deliver-terminal-schema.js';
|
|
32
36
|
|
|
33
37
|
// Re-exported so the schema split stays an implementation detail: every
|
|
@@ -327,7 +331,95 @@ export const TERMINAL_BEGIN_MARKER = '--- STORY DELIVER TERMINAL ---';
|
|
|
327
331
|
export const TERMINAL_END_MARKER = '--- END TERMINAL ---';
|
|
328
332
|
|
|
329
333
|
/**
|
|
330
|
-
*
|
|
334
|
+
* Resolve the repo config, or `undefined` when it cannot be read.
|
|
335
|
+
*
|
|
336
|
+
* An unreadable `.agentrc.json` must not cost the run its envelope copy: the
|
|
337
|
+
* path helpers fall back to the framework-default temp root, which is still a
|
|
338
|
+
* far better outcome than no artifact at all.
|
|
339
|
+
*
|
|
340
|
+
* @param {typeof resolveConfig} resolveConfigImpl
|
|
341
|
+
* @returns {object|undefined}
|
|
342
|
+
*/
|
|
343
|
+
function tolerantConfig(resolveConfigImpl) {
|
|
344
|
+
try {
|
|
345
|
+
return resolveConfigImpl();
|
|
346
|
+
} catch {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Persist a terminal envelope beside its Story's gate log (Story #4816).
|
|
353
|
+
*
|
|
354
|
+
* Stdout is a channel with exactly one reader — the turn that launched the
|
|
355
|
+
* close — and that reader is not always still listening. A `story-worker`
|
|
356
|
+
* that reports progress and ends its turn while the close it started is
|
|
357
|
+
* mid-gate-chain is behaving reasonably, but the envelope it never relayed is
|
|
358
|
+
* gone: the router then has to reconstruct the Story's state from labels, and
|
|
359
|
+
* `deliver-recover.js` used to answer that reconstruction with
|
|
360
|
+
* "Implementation never finished" — false, and its re-init suggestion can put
|
|
361
|
+
* a second close on one PR. Observed four times across three workers in one
|
|
362
|
+
* consumer run. The file is the second channel, and it outlives the turn.
|
|
363
|
+
*
|
|
364
|
+
* **Best-effort by construction.** Every failure path returns `null`: a
|
|
365
|
+
* delivery must never turn a landed PR into a crash because a temp directory
|
|
366
|
+
* was unwritable. The stdout envelope above is the contract; this is a copy.
|
|
367
|
+
*
|
|
368
|
+
* **Atomic by construction.** The payload is written to a pid-scoped
|
|
369
|
+
* temporary name and renamed into place, because the reader that matters most
|
|
370
|
+
* is a router polling during a live close — a half-written file would hand it
|
|
371
|
+
* a parse error at exactly the moment it is trying to avoid guessing.
|
|
372
|
+
*
|
|
373
|
+
* A null `storyId` (the `escalated` terminal, which by construction never
|
|
374
|
+
* authored a Story) has nowhere to be filed and writes nothing.
|
|
375
|
+
*
|
|
376
|
+
* `config` is optional and resolved lazily when absent. Two of the emit sites
|
|
377
|
+
* are `catch` blocks that crashed before any config was resolved, and those
|
|
378
|
+
* are precisely the runs whose envelope is most worth keeping — so the temp
|
|
379
|
+
* root is looked up here rather than left at the framework default, which
|
|
380
|
+
* would file the artifact where a consumer's router never looks (and where
|
|
381
|
+
* the retention purge would never reap it).
|
|
382
|
+
*
|
|
383
|
+
* @param {object} envelope A validated terminal envelope.
|
|
384
|
+
* @param {{
|
|
385
|
+
* config?: object,
|
|
386
|
+
* fsImpl?: typeof nodeFs,
|
|
387
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
388
|
+
* }} [deps]
|
|
389
|
+
* @returns {string|null} The path written, or `null` when nothing was.
|
|
390
|
+
*/
|
|
391
|
+
export function persistTerminalEnvelope(
|
|
392
|
+
envelope,
|
|
393
|
+
{ config, fsImpl = nodeFs, resolveConfigImpl = resolveConfig } = {},
|
|
394
|
+
) {
|
|
395
|
+
const storyId = envelope?.storyId;
|
|
396
|
+
if (!Number.isInteger(storyId) || storyId <= 0) return null;
|
|
397
|
+
let tmpPath = null;
|
|
398
|
+
try {
|
|
399
|
+
const resolved = config ?? tolerantConfig(resolveConfigImpl);
|
|
400
|
+
const target = storyTerminalEnvelopePath(storyId, resolved);
|
|
401
|
+
fsImpl.mkdirSync(path.dirname(target), { recursive: true });
|
|
402
|
+
tmpPath = `${target}.${process.pid}.tmp`;
|
|
403
|
+
fsImpl.writeFileSync(tmpPath, `${JSON.stringify(envelope)}\n`, 'utf8');
|
|
404
|
+
fsImpl.renameSync(tmpPath, target);
|
|
405
|
+
return target;
|
|
406
|
+
} catch {
|
|
407
|
+
// A rename that never ran leaves the scratch file behind; drop it rather
|
|
408
|
+
// than accumulating one per failed close.
|
|
409
|
+
if (tmpPath) {
|
|
410
|
+
try {
|
|
411
|
+
fsImpl.rmSync(tmpPath, { force: true });
|
|
412
|
+
} catch {
|
|
413
|
+
// Nothing left to try — this whole path is already best-effort.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Write a terminal envelope to stdout, between its markers, and persist a
|
|
422
|
+
* copy to disk.
|
|
331
423
|
*
|
|
332
424
|
* **Deliberately not `Logger.info`.** The envelope is this CLI's
|
|
333
425
|
* machine-readable contract — every invocation emits exactly ONE, and a
|
|
@@ -340,16 +432,37 @@ export const TERMINAL_END_MARKER = '--- END TERMINAL ---';
|
|
|
340
432
|
*
|
|
341
433
|
* Single home for the marker format so the four emit sites (the runner's
|
|
342
434
|
* terminal, the close CLI's failed-terminal catch, and both confirm-CLI
|
|
343
|
-
* paths) cannot drift apart
|
|
435
|
+
* paths) cannot drift apart — and, since Story #4816, the single home for the
|
|
436
|
+
* on-disk copy too, so no emit path can persist and another forget.
|
|
437
|
+
* {@link persistTerminalEnvelope} runs **first**: a caller that has read the
|
|
438
|
+
* markers off stdout can then rely on the file already being there.
|
|
344
439
|
*
|
|
345
440
|
* @param {object} envelope
|
|
346
|
-
* @param {{
|
|
441
|
+
* @param {{
|
|
442
|
+
* write?: (s: string) => void,
|
|
443
|
+
* config?: object,
|
|
444
|
+
* persist?: typeof persistTerminalEnvelope,
|
|
445
|
+
* }} [opts] `write` and `persist` are test seams; `config` resolves the
|
|
446
|
+
* artifact's temp root and is threaded from whichever emit site holds one.
|
|
347
447
|
* @returns {void}
|
|
348
448
|
*/
|
|
349
449
|
export function emitTerminalEnvelope(
|
|
350
450
|
envelope,
|
|
351
|
-
{
|
|
451
|
+
{
|
|
452
|
+
write = (s) => process.stdout.write(s),
|
|
453
|
+
config,
|
|
454
|
+
persist = persistTerminalEnvelope,
|
|
455
|
+
} = {},
|
|
352
456
|
) {
|
|
457
|
+
// Belt and braces around a copy: `persistTerminalEnvelope` already swallows
|
|
458
|
+
// its own failures, but the stdout envelope is the CONTRACT and the disk
|
|
459
|
+
// copy is a convenience. Nothing in the secondary path — including a future
|
|
460
|
+
// injected `persist` — may cost the caller the primary one.
|
|
461
|
+
try {
|
|
462
|
+
persist(envelope, { config });
|
|
463
|
+
} catch {
|
|
464
|
+
// Intentionally silent: see above.
|
|
465
|
+
}
|
|
353
466
|
// Story #4685 — compact (not 2-space pretty) JSON. The envelope is a
|
|
354
467
|
// machine contract callers recover with `JSON.parse`, so pretty-printing
|
|
355
468
|
// only adds turn-resident bytes without helping any consumer.
|
|
@@ -210,28 +210,43 @@ async function makeEntry(fsp, target, className, storyId, keep = false) {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
213
|
+
* Extensions this class owns inside `orchestration/`. Story #4816 added
|
|
214
|
+
* `.json`: the persisted terminal envelope lands beside the gate log, and a
|
|
215
|
+
* `.log`-only scan would have left one immortal file per delivered Story in a
|
|
216
|
+
* directory the purge otherwise keeps clean.
|
|
217
|
+
*/
|
|
218
|
+
const ORCHESTRATION_EXTENSIONS = Object.freeze(['.log', '.json']);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Recover the Story id a run-artifact basename carries. Every writer that
|
|
222
|
+
* lands in `orchestration/` ends its name with the scope: `close-gates-4794.log`
|
|
223
|
+
* from the gate sink, `sync-result-story-4794.log` from the terse-result dump,
|
|
224
|
+
* `story-deliver-terminal-4794.json` from the terminal-envelope persist.
|
|
216
225
|
*
|
|
217
226
|
* @param {string} name
|
|
218
227
|
* @returns {number|null}
|
|
219
228
|
*/
|
|
220
229
|
function storyIdFromLogName(name) {
|
|
221
|
-
const match = TRAILING_ID_PATTERN.exec(name.replace(/\.log$/, ''));
|
|
230
|
+
const match = TRAILING_ID_PATTERN.exec(name.replace(/\.(log|json)$/, ''));
|
|
222
231
|
return match ? Number(match[1]) : null;
|
|
223
232
|
}
|
|
224
233
|
|
|
225
234
|
/**
|
|
226
|
-
* `<tempRoot>/orchestration/*.log` — close gate transcripts
|
|
227
|
-
* detail dumps
|
|
228
|
-
*
|
|
235
|
+
* `<tempRoot>/orchestration/*.{log,json}` — close gate transcripts,
|
|
236
|
+
* terse-result detail dumps, and persisted terminal envelopes. An artifact
|
|
237
|
+
* whose name carries no id (there are none today, but the class owns the
|
|
238
|
+
* directory) is age-floored rather than dropped from the class.
|
|
229
239
|
*/
|
|
230
240
|
async function scanOrchestrationLogs(tempRoot, fsp) {
|
|
231
241
|
const dir = path.join(tempRoot, ORCHESTRATION_DIRNAME);
|
|
232
242
|
const entries = [];
|
|
233
243
|
for (const dirent of await safeReaddir(fsp, dir)) {
|
|
234
|
-
if (
|
|
244
|
+
if (
|
|
245
|
+
!dirent.isFile() ||
|
|
246
|
+
!ORCHESTRATION_EXTENSIONS.some((ext) => dirent.name.endsWith(ext))
|
|
247
|
+
) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
235
250
|
const entry = await makeEntry(
|
|
236
251
|
fsp,
|
|
237
252
|
path.join(dir, dirent.name),
|
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { TEST_TEMP_ROOT_ENV } from './config/temp-paths.js';
|
|
6
|
+
import { reapOnExit } from './test-temp.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Per-process memo for the created scratch dir, so repeated calls in one
|
|
@@ -32,16 +33,26 @@ export function _clearTestScratchTempRootCache() {
|
|
|
32
33
|
* `temp/` telemetry tree — the regression that let 99% of friction records
|
|
33
34
|
* be test-fixture pollution.
|
|
34
35
|
*
|
|
36
|
+
* Reaping (Story #4808): the minting process registers removal of the
|
|
37
|
+
* scratch dir at exit. This branch is creator-only by construction — a
|
|
38
|
+
* process that inherited an absolute root returns above without ever
|
|
39
|
+
* touching the memo, so a child can never delete its parent's scratch
|
|
40
|
+
* while the parent is still writing to it. Left unreaped, this seam was
|
|
41
|
+
* the single largest contributor to the OS-temp-root leak (870 surviving
|
|
42
|
+
* `mandrel-test-temp-` roots on one host), because it mints one per
|
|
43
|
+
* `run-tests.js` invocation.
|
|
44
|
+
*
|
|
35
45
|
* Directly unit-tested via the injectable `mkdtemp` seam in
|
|
36
46
|
* `tests/lib/test-env.test.js` (Story #4711).
|
|
37
47
|
*
|
|
38
48
|
* @param {NodeJS.ProcessEnv} [baseEnv=process.env]
|
|
39
|
-
* @param {{ mkdtemp?: typeof mkdtempSync }} [deps]
|
|
49
|
+
* @param {{ mkdtemp?: typeof mkdtempSync, onExit?: (fn: () => void) => void }} [deps]
|
|
50
|
+
* Injectable for tests.
|
|
40
51
|
* @returns {string} absolute scratch tempRoot
|
|
41
52
|
*/
|
|
42
53
|
export function ensureTestScratchTempRoot(
|
|
43
54
|
baseEnv = process.env,
|
|
44
|
-
{ mkdtemp = mkdtempSync } = {},
|
|
55
|
+
{ mkdtemp = mkdtempSync, onExit } = {},
|
|
45
56
|
) {
|
|
46
57
|
const existing = baseEnv?.[TEST_TEMP_ROOT_ENV];
|
|
47
58
|
if (
|
|
@@ -52,7 +63,8 @@ export function ensureTestScratchTempRoot(
|
|
|
52
63
|
return existing;
|
|
53
64
|
}
|
|
54
65
|
if (_createdScratchDir === null) {
|
|
55
|
-
_createdScratchDir = mkdtemp(path.join(os.tmpdir(), 'mandrel-test-temp-'));
|
|
66
|
+
_createdScratchDir = mkdtemp(path.join(os.tmpdir(), 'mandrel-test-temp-')); // test-temp-allow: children inherit this path, so it lives outside the suite root.
|
|
67
|
+
reapOnExit(_createdScratchDir, onExit ? { onExit } : {});
|
|
56
68
|
}
|
|
57
69
|
return _createdScratchDir;
|
|
58
70
|
}
|