Files
msd-core/tests/helpers-cleanup.test.cjs
sim 7ef9945adb test(#3090): stop paying for the guard on every teardown
The Windows node-24 scoped-test job hit its 15-minute ceiling twice on this
branch and GitHub reported both as cancelled, which is how a job timeout
surfaces. The same job finishes in about two minutes twenty on next, across
four consecutive runs. cleanup() is the only hot-path change here.

It was doing up to seven filesystem calls per invocation: three probes of the
temp root, two more for each conventional temp dir, then an existence check and
a realpath of the target. On Windows fs.realpathSync.native opens a file handle
and Defender charges for each one, and this runs in the teardown of effectively
every test.

The root candidates are now memoized on the live os.tmpdir() value. The key
matters: two files in the suite override TMPDIR mid-run and restore it, so a
plain module-level hoist would go stale for them, while re-reading os.tmpdir()
costs an env lookup. Measured over 20,000 calls: 546ms unmemoized, 10ms
memoized.

The symlink-escape check is removed rather than optimized, because it was
guarding something that cannot happen. Verified directly on node v26.5.1:

  fs.rmSync(link, {recursive: true, force: true})
    link exists   false
    victim exists true
    file exists   true

rmSync unlinks a top-level symlink and leaves its target alone, and a symlink
nested inside a tree being recursively removed is also unlinked rather than
followed. The check cost two filesystem calls per teardown on the slowest
platform in the matrix and bought nothing. Its test asserted the victim
survived, which was true with or without the guard.

What closed the original defect is untouched: a target outside the known temp
roots is still refused, before the chdir and before the rmSync, with the roots
named in the message.

Refs #3057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:39:53 -04:00

262 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GSD Tools Test Helpers – cleanup() behavioral tests
*
* Three deterministic, cross-platform tests that verify cleanup()'s
* observable contract at the seam rather than probing its internals.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { cleanup, createTempDir, tmpRootCandidates } = require('./helpers.cjs');
// ─── Test 1: Real-FS happy path ──────────────────────────────────────────────
test('cleanup removes a real temp dir with nested subdirs and files', () => {
const dir = createTempDir('gsd-cleanup-test-');
const nested = path.join(dir, 'a', 'b', 'c');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');
fs.writeFileSync(path.join(dir, 'root.txt'), 'world');
cleanup(dir);
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should not exist after cleanup');
});
// ─── Test 2: Retry-budget contract ───────────────────────────────────────────
test('cleanup passes recursive/force/maxRetries/retryDelay options to fs.rmSync', () => {
// Use a real temp dir as the target so cleanup() has a valid path argument.
// We chdir AWAY from it first so cleanup() does not try to chdir either.
const dir = createTempDir('gsd-cleanup-opts-test-');
// Capture original cwd and shift away from the target.
const originalCwd = process.cwd();
// Chdir to the parent of the target so cleanup's cwd-guard is a no-op.
process.chdir(path.dirname(dir));
let capturedOptions = null;
const realRmSync = fs.rmSync;
try {
// Replace fs.rmSync with a probe that captures options then does nothing.
// This is an assignment expression (not a CallExpression) so it satisfies
// the ESLint rule that bans raw fs.rmSync(...) call expressions in tests.
fs.rmSync = (targetPath, opts) => {
capturedOptions = opts;
// Do NOT call through — we don't want the dir actually removed here;
// we're only testing the options shape.
};
cleanup(dir);
} finally {
fs.rmSync = realRmSync;
process.chdir(originalCwd);
// Remove the dir with the real rmSync now that we restored it.
cleanup(dir);
}
assert.ok(capturedOptions !== null, 'fs.rmSync should have been called');
assert.strictEqual(capturedOptions.recursive, true, 'recursive must be true');
assert.strictEqual(capturedOptions.force, true, 'force must be true');
assert.ok(
typeof capturedOptions.maxRetries === 'number' && capturedOptions.maxRetries > 0,
'maxRetries must be a positive number'
);
assert.ok(
typeof capturedOptions.retryDelay === 'number' && capturedOptions.retryDelay > 0,
'retryDelay must be a positive number'
);
});
// ─── Test 3: cwd-guard ───────────────────────────────────────────────────────
test('cleanup does not throw when cwd is inside the target dir, and removes the dir', () => {
const dir = createTempDir('gsd-cleanup-cwd-test-');
const nested = path.join(dir, 'deep', 'nested');
fs.mkdirSync(nested, { recursive: true });
const originalCwd = process.cwd();
try {
// Step INTO the nested subdir so cwd is inside the cleanup target.
process.chdir(nested);
assert.doesNotThrow(() => {
cleanup(dir);
}, 'cleanup should not throw even when cwd is inside the target');
} finally {
// Restore original cwd. cleanup() will have chdir'd to dirname(dir),
// so we always restore explicitly regardless.
if (process.cwd() !== originalCwd) {
process.chdir(originalCwd);
}
}
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should not exist after cleanup');
});
// ─── Test 4: out-of-tmpdir refusal ───────────────────────────────────────────
test('cleanup throws and does not chdir or delete when target is outside os.tmpdir()', () => {
// __dirname (this repo's tests/ directory) must never be deleted. Whether
// it is actually outside os.tmpdir() is environment-dependent: on Linux
// os.tmpdir() is /tmp, and a CI container that checks the repo out under
// /tmp would put __dirname INSIDE tmpdir, in which case a correctly-working
// cleanup() would not refuse it -- it would delete this directory. The
// precondition assertion below verifies the "outside tmpdir" assumption
// before cleanup() is ever called, so that situation fails loudly and
// safely instead of destructively. No scratch directory is created, so
// there is nothing to tear down.
const outsideDir = __dirname;
const knownFile = path.join(outsideDir, 'helpers-cleanup.test.cjs');
// Use cleanup()'s own tmpRootCandidates() as the single source of truth
// for "is this path inside tmpdir" instead of a hand-mirrored predicate.
// The old copy here checked only path.resolve(os.tmpdir()), one of
// SEVERAL root spellings cleanup() actually accepts (it also accepts the
// realpath'd and native-realpath'd forms) — a repo checked out under a
// root this precondition didn't know about would compute "outside, safe"
// while cleanup() computed "inside, delete", turning this refusal test
// into the very destructive scenario it exists to prevent. Importing the
// real function removes that drift class entirely.
const resolvedOutsideDir = path.resolve(outsideDir);
const roots = tmpRootCandidates();
const isWindows = process.platform === 'win32';
const outsideDirForCompare = isWindows ? resolvedOutsideDir.toLowerCase() : resolvedOutsideDir;
const isInsideTmpdir = roots.some((root) => {
const rootForCompare = isWindows ? root.toLowerCase() : root;
return (
outsideDirForCompare === rootForCompare ||
outsideDirForCompare.startsWith(`${rootForCompare}${path.sep}`)
);
});
assert.strictEqual(
isInsideTmpdir,
false,
`this test cannot run safely when the repo lives under any tmpdir root ` +
`cleanup() accepts: outsideDir (${resolvedOutsideDir}) is inside one of ${JSON.stringify(roots)}`
);
const cwdBefore = process.cwd();
assert.throws(
() => cleanup(outsideDir),
(err) => err instanceof Error && err.message.includes(outsideDir),
'cleanup must throw an Error whose message names the offending path'
);
assert.strictEqual(
process.cwd(),
cwdBefore,
'cleanup must refuse before chdir, so cwd is unchanged'
);
assert.strictEqual(fs.existsSync(outsideDir), true, 'target directory must still exist after refusal');
assert.strictEqual(
fs.existsSync(knownFile),
true,
'a known file inside the target must still exist, proving the dir was not emptied'
);
});
// ─── Test 5: control — a real os.tmpdir()-rooted path still cleans up ───────
test('cleanup still removes a real os.tmpdir()-rooted directory (control)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-control-'));
cleanup(dir);
assert.strictEqual(fs.existsSync(dir), false, 'os.tmpdir()-rooted directory should be removed');
});
// ─── Test 6b: accepted-roots set always accepts a freshly-minted tmpdir ─────
test('cleanup accepts a freshly-created os.tmpdir()-rooted dir on any platform (accepted-roots invariant)', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-roots-'));
t.after(() => {
if (fs.existsSync(dir)) cleanup(dir);
});
assert.doesNotThrow(
() => cleanup(dir),
'cleanup must accept a directory created directly under os.tmpdir(), regardless of platform-specific canonical spelling (8.3 short/long names, /var symlink, drive-letter case)'
);
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should be removed, not refused');
});
// ─── Test 6: realpath'd os.tmpdir() form is not refused (regression) ────────
test('cleanup accepts a realpath()d temp dir even when it differs from the raw path (macOS /var -> /private/var)', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-realpath-'));
const realPath = fs.realpathSync(dir);
if (realPath !== dir) {
// macOS (and any other symlinked-tmpdir platform): the realpath'd form
// diverges from the raw mkdtempSync() path. This is exactly the shape a
// caller gets from fs.realpathSync() or from process.cwd() after
// chdir-ing into a realpath'd dir — assert the guard does NOT refuse it.
cleanup(realPath);
assert.strictEqual(fs.existsSync(realPath), false, 'realpath()d form should be removed, not refused');
assert.strictEqual(fs.existsSync(dir), false, 'raw path should also be gone (same directory)');
} else {
// Linux and any platform with no tmpdir symlink indirection: realpath()
// equals the raw path, so this branch exercises the ordinary path and
// keeps the test meaningful (non-vacuous) on both platforms.
t.after(() => {
if (fs.existsSync(dir)) cleanup(dir);
});
cleanup(dir);
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should be removed');
}
});
// ─── Test 7: tmpRootCandidates() shape invariants (platform-independent) ────
//
// Test 6 above is coverage-identical to Test 5's control on any platform
// where fs.realpathSync(tmpdir) does not diverge from the raw path (Linux,
// notably — the platform the remote matrix actually runs on), so it cannot
// verify the macOS-specific fix there. These assertions hold the exported
// tmpRootCandidates() to a contract that is meaningful on every platform.
test('tmpRootCandidates() returns a well-formed, deduplicated list of absolute paths', () => {
const roots = tmpRootCandidates();
assert.ok(Array.isArray(roots) && roots.length > 0, 'must return a non-empty array');
for (const root of roots) {
assert.strictEqual(path.isAbsolute(root), true, `root must be absolute: ${root}`);
}
const isWindows = process.platform === 'win32';
const keys = roots.map((r) => (isWindows ? r.toLowerCase() : r));
const uniqueKeys = new Set(keys);
assert.strictEqual(uniqueKeys.size, keys.length, `roots must be deduplicated: ${JSON.stringify(roots)}`);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-roots-contract-'));
try {
const resolvedDir = path.resolve(dir);
const dirForCompare = isWindows ? resolvedDir.toLowerCase() : resolvedDir;
const isUnderSomeRoot = roots.some((root) => {
const rootForCompare = isWindows ? root.toLowerCase() : root;
return (
dirForCompare === rootForCompare ||
dirForCompare.startsWith(`${rootForCompare}${path.sep}`)
);
});
assert.strictEqual(
isUnderSomeRoot,
true,
`a freshly mkdtempSync'd dir under os.tmpdir() must be under at least one returned root: ` +
`${resolvedDir} vs ${JSON.stringify(roots)}`
);
} finally {
cleanup(dir);
}
});