test: add process-state isolation helper for reliability (#331)
This commit is contained in:
@@ -17,6 +17,7 @@ const projection = require(path.join(
|
|||||||
'shell-command-projection.cjs',
|
'shell-command-projection.cjs',
|
||||||
));
|
));
|
||||||
const install = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
const install = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
||||||
|
const { withIsolatedProcessState } = require('./helpers.cjs');
|
||||||
|
|
||||||
function createTempHome() {
|
function createTempHome() {
|
||||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-home-3441-'));
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-home-3441-'));
|
||||||
@@ -88,38 +89,37 @@ describe('bug #3441: PATH guidance is projected from typed shell action IR', ()
|
|||||||
|
|
||||||
test('maybeSuggestPathExport renders commands projected by path-action seam', () => {
|
test('maybeSuggestPathExport renders commands projected by path-action seam', () => {
|
||||||
const home = createTempHome();
|
const home = createTempHome();
|
||||||
const originalPath = process.env.PATH;
|
|
||||||
try {
|
try {
|
||||||
const globalBin = path.join(home, '.npm-global', 'bin');
|
withIsolatedProcessState(() => {
|
||||||
fs.mkdirSync(globalBin, { recursive: true });
|
const globalBin = path.join(home, '.npm-global', 'bin');
|
||||||
fs.writeFileSync(path.join(home, '.zshrc'), 'export PATH="$HOME/.cargo/bin:$PATH"\n');
|
fs.mkdirSync(globalBin, { recursive: true });
|
||||||
process.env.PATH = '';
|
fs.writeFileSync(path.join(home, '.zshrc'), 'export PATH="$HOME/.cargo/bin:$PATH"\n');
|
||||||
|
process.env.PATH = '';
|
||||||
|
|
||||||
const expected = projection.projectPathActionProjection({
|
const expected = projection.projectPathActionProjection({
|
||||||
mode: 'persist',
|
mode: 'persist',
|
||||||
targetDir: globalBin,
|
targetDir: globalBin,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
});
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
const originalLog = console.log;
|
||||||
|
console.log = (...args) => logs.push(args.join(' '));
|
||||||
|
try {
|
||||||
|
install.maybeSuggestPathExport(globalBin, home);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
const joined = logs.join('\n');
|
||||||
|
for (const action of expected.shellActions) {
|
||||||
|
assert.ok(
|
||||||
|
joined.includes(action.command),
|
||||||
|
`expected installer output to include projected command: ${action.command}\nOutput:\n${joined}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = [];
|
|
||||||
const originalLog = console.log;
|
|
||||||
console.log = (...args) => logs.push(args.join(' '));
|
|
||||||
try {
|
|
||||||
install.maybeSuggestPathExport(globalBin, home);
|
|
||||||
} finally {
|
|
||||||
console.log = originalLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
const joined = logs.join('\n');
|
|
||||||
for (const action of expected.shellActions) {
|
|
||||||
assert.ok(
|
|
||||||
joined.includes(action.command),
|
|
||||||
`expected installer output to include projected command: ${action.command}\nOutput:\n${joined}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (originalPath == null) delete process.env.PATH;
|
|
||||||
else process.env.PATH = originalPath;
|
|
||||||
cleanup(home);
|
cleanup(home);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
41
tests/helpers-process-isolation.test.cjs
Normal file
41
tests/helpers-process-isolation.test.cjs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const { test, describe } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const { withIsolatedProcessState } = require('./helpers.cjs');
|
||||||
|
|
||||||
|
describe('withIsolatedProcessState', () => {
|
||||||
|
test('restores env, cwd, and exitCode after callback', () => {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const originalExitCode = process.exitCode;
|
||||||
|
const originalMarker = process.env.GSD_TEST_ISOLATION_MARKER;
|
||||||
|
|
||||||
|
const tempCwd = path.dirname(originalCwd);
|
||||||
|
|
||||||
|
withIsolatedProcessState(() => {
|
||||||
|
process.env.GSD_TEST_ISOLATION_MARKER = 'changed';
|
||||||
|
process.exitCode = 73;
|
||||||
|
process.chdir(tempCwd);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(process.cwd(), originalCwd);
|
||||||
|
assert.strictEqual(process.exitCode, originalExitCode);
|
||||||
|
assert.strictEqual(process.env.GSD_TEST_ISOLATION_MARKER, originalMarker);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restores state even when callback throws', () => {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
withIsolatedProcessState(() => {
|
||||||
|
process.env.PATH = '';
|
||||||
|
process.chdir(path.dirname(originalCwd));
|
||||||
|
throw new Error('boom');
|
||||||
|
});
|
||||||
|
}, /boom/);
|
||||||
|
|
||||||
|
assert.strictEqual(process.cwd(), originalCwd);
|
||||||
|
assert.strictEqual(process.env.PATH, originalPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -315,4 +315,34 @@ function isolatedNpmEnv() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, runNpm, isolatedNpmEnv, TOOLS_PATH };
|
/**
|
||||||
|
* Run a callback with process-level state isolation.
|
||||||
|
* Restores cwd, exitCode, and process.env after callback returns or throws.
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {() => T} fn
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
function withIsolatedProcessState(fn) {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const originalExitCode = process.exitCode;
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
if (process.cwd() !== originalCwd) {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
process.exitCode = originalExitCode;
|
||||||
|
|
||||||
|
for (const key of Object.keys(process.env)) {
|
||||||
|
if (!(key in originalEnv)) delete process.env[key];
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(originalEnv)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, TOOLS_PATH };
|
||||||
|
|||||||
Reference in New Issue
Block a user