fix(tests): keep transcript and hook fixtures in private temp roots

Isolate spawned Stop hooks through TMPDIR/TMP/TEMP, open transcript fixtures exclusively with private modes, and derive intentional stale-race fixture identity from its opened descriptor.
This commit is contained in:
haelyra
2026-10-10 18:38:26 -04:00
parent 8044679227
commit d6dc05a12b
3 changed files with 28 additions and 9 deletions
+17 -5
View File
@@ -37,6 +37,12 @@ function test(name, fn) {
let passed = 0;
let failed = 0;
// Keep accumulator fixtures private, including subprocess hook invocations.
const accumulatorTempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-stop-fixtures-')));
const originalTempEnv = Object.fromEntries(['TMPDIR', 'TMP', 'TEMP'].map(key => [key, process.env[key]]));
for (const key of Object.keys(originalTempEnv)) process.env[key] = accumulatorTempRoot;
process.on('exit', () => fs.rmSync(accumulatorTempRoot, { recursive: true, force: true }));
// Use a unique session ID for tests so we don't pollute real sessions
const TEST_SESSION_ID = `test-${Date.now()}`;
const origSessionId = process.env.CLAUDE_SESSION_ID;
@@ -45,7 +51,7 @@ process.env.CLAUDE_SESSION_ID = TEST_SESSION_ID;
delete process.env.ECC_SESSION_ID;
function getAccumFile() {
return path.join(os.tmpdir(), `ecc-edited-${TEST_SESSION_ID}.txt`);
return path.join(accumulatorTempRoot, `ecc-edited-${TEST_SESSION_ID}.txt`);
}
function cleanAccumFile() {
@@ -275,8 +281,8 @@ console.log('\nsession-scoped accumulator regression (#3460)');
function withPayloadSessions(fn) {
const sessionA = `${TEST_SESSION_ID}-payload-a`;
const sessionB = `${TEST_SESSION_ID}-payload-b`;
const fileA = path.join(os.tmpdir(), `ecc-edited-${sessionA}.txt`);
const fileB = path.join(os.tmpdir(), `ecc-edited-${sessionB}.txt`);
const fileA = path.join(accumulatorTempRoot, `ecc-edited-${sessionA}.txt`);
const fileB = path.join(accumulatorTempRoot, `ecc-edited-${sessionB}.txt`);
try {
cleanAccumFile();
fn({ sessionA, sessionB, fileA, fileB });
@@ -323,7 +329,7 @@ if (test('MultiEdit uses the same stdin session accumulator for every edit', ()
if (test('ECC_SESSION_ID precedes CLAUDE_SESSION_ID while preserving legacy filenames', () => {
const rawId = `${TEST_SESSION_ID}/ecc.with spaces`;
const safeId = rawId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
const file = path.join(os.tmpdir(), `ecc-edited-${safeId}.txt`);
const file = path.join(accumulatorTempRoot, `ecc-edited-${safeId}.txt`);
process.env.ECC_SESSION_ID = rawId;
try {
cleanAccumFile();
@@ -343,7 +349,7 @@ if (test('ECC_SESSION_ID precedes CLAUDE_SESSION_ID while preserving legacy file
if (test('stdin IDs keep the existing filename sanitizer and length limit', () => {
const rawId = `${TEST_SESSION_ID}/../nested\\session ${'x'.repeat(100)}`;
const safeId = rawId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
const file = path.join(os.tmpdir(), `ecc-edited-${safeId}.txt`);
const file = path.join(accumulatorTempRoot, `ecc-edited-${safeId}.txt`);
try {
accumulator.run(JSON.stringify({ session_id: rawId, tool_input: { file_path: '/nonexistent/safe.ts' } }));
assert.strictEqual(fs.readFileSync(file, 'utf8'), '/nonexistent/safe.ts\n');
@@ -395,6 +401,12 @@ if (test('resolves traversal before deciding', () => {
assert.strictEqual(isPluginClonePath(p, FAKE_CWD, FAKE_HOME), true);
})) passed++; else failed++;
fs.rmSync(accumulatorTempRoot, { recursive: true, force: true });
for (const [key, value] of Object.entries(originalTempEnv)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
// Restore env
if (origSessionId === undefined) {
delete process.env.CLAUDE_SESSION_ID;
+6 -1
View File
@@ -185,7 +185,12 @@ for (const phase of ['during hashing', 'after hashing']) {
const edited = 'user edit bytes\n';
assert.strictEqual(original.length, edited.length);
writeFile(file, original);
const identity = fs.statSync(file, { bigint: true });
// Bind the fixture identity to its opened private file, keeping the later
// pathname read an assertion rather than relying on a prior stat check.
const identityFd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
let identity;
try { identity = fs.fstatSync(identityFd, { bigint: true }); }
finally { fs.closeSync(identityFd); }
const originalRead = fs.readFileSync;
const originalFstat = fs.fstatSync;
let mutationApplied = false;
+5 -3
View File
@@ -45,12 +45,14 @@ function test(desc, fn) {
}
}
const transcriptRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-transcript-context-'));
process.on('exit', () => fs.rmSync(transcriptRoot, { recursive: true, force: true }));
let fixtureSeq = 0;
function writeTranscript(lines) {
fixtureSeq += 1;
const filePath = path.join(os.tmpdir(), `transcript-context-test-${process.pid}-${fixtureSeq}.jsonl`);
fs.writeFileSync(filePath, lines.join('\n') + '\n');
const filePath = path.join(transcriptRoot, `transcript-context-test-${process.pid}-${fixtureSeq}.jsonl`);
fs.writeFileSync(filePath, lines.join('\n') + '\n', { flag: 'wx', mode: 0o600 });
return filePath;
}
@@ -111,7 +113,7 @@ test('returns null for a transcript with no usage records', () => {
});
test('returns null for a missing transcript file', () => {
assert.strictEqual(readLatestContextTokens(path.join(os.tmpdir(), 'definitely-missing.jsonl')), null);
assert.strictEqual(readLatestContextTokens(path.join(transcriptRoot, 'definitely-missing.jsonl')), null);
});
test('returns null for empty or non-string paths', () => {