From d6dc05a12b1e4a081cbcd920dedaaf751fed0ca3 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:31:19 -0400 Subject: [PATCH] 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. --- tests/hooks/stop-format-typecheck.test.js | 22 +++++++++++++++++----- tests/lib/install-stale-operations.test.js | 7 ++++++- tests/lib/transcript-context.test.js | 8 +++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/tests/hooks/stop-format-typecheck.test.js b/tests/hooks/stop-format-typecheck.test.js index e0696de8f..944c546a1 100644 --- a/tests/hooks/stop-format-typecheck.test.js +++ b/tests/hooks/stop-format-typecheck.test.js @@ -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; diff --git a/tests/lib/install-stale-operations.test.js b/tests/lib/install-stale-operations.test.js index caf24802b..9714a03d9 100644 --- a/tests/lib/install-stale-operations.test.js +++ b/tests/lib/install-stale-operations.test.js @@ -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; diff --git a/tests/lib/transcript-context.test.js b/tests/lib/transcript-context.test.js index 003d0cc9a..cac68d1d0 100644 --- a/tests/lib/transcript-context.test.js +++ b/tests/lib/transcript-context.test.js @@ -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', () => {