diff --git a/tests/fixtures/session-start-alias-read-preload.cjs b/tests/fixtures/session-start-alias-read-preload.cjs new file mode 100644 index 000000000..d9cd848c5 --- /dev/null +++ b/tests/fixtures/session-start-alias-read-preload.cjs @@ -0,0 +1,31 @@ +'use strict'; + +const fs = require('fs'); +const atomic = require('../../scripts/lib/atomic-write'); +const aliasPath = process.env.ECC_TEST_ALIAS_PATH; +const countPath = process.env.ECC_TEST_ALIAS_FAULT_COUNT; +const errorCode = process.env.ECC_TEST_ALIAS_READ_ERROR; +if (!aliasPath || !countPath || !['EACCES', 'EPERM'].includes(errorCode)) { + throw new Error('Invalid startup alias-read fixture data'); +} + +const originalRead = fs.readFileSync; +let denied = 0; +fs.readFileSync = function (file) { + if (file === aliasPath) { + denied++; + throw Object.assign(new Error('injected unreadable aliases'), { code: errorCode }); + } + return originalRead.apply(this, arguments); +}; + +// Exercise Windows retry semantics only during this synchronous helper call. +// Restore the actual platform before project detection and other hook work. +const readWithRetry = atomic.readFileWithSharingRetry; +atomic.readFileWithSharingRetry = function (...args) { + const platform = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { ...platform, value: 'win32' }); + try { return readWithRetry(...args); } + finally { Object.defineProperty(process, 'platform', platform); } +}; +process.on('exit', () => fs.writeFileSync(countPath, JSON.stringify(denied))); diff --git a/tests/hooks/session-start-alias-unavailable.test.js b/tests/hooks/session-start-alias-unavailable.test.js index b088d6227..c2c7e480e 100644 --- a/tests/hooks/session-start-alias-unavailable.test.js +++ b/tests/hooks/session-start-alias-unavailable.test.js @@ -31,23 +31,11 @@ try { for (const code of [null, 'EACCES', 'EPERM']) { const faultCount = path.join(root, `fault-count-${code}.json`); const args = []; - if (code) { - const preload = path.join(root, `deny-${code}.cjs`); - fs.writeFileSync(preload, [ - "const fs=require('fs'), vm=require('vm');", - `const file=${JSON.stringify(aliasPath)}, helper=${JSON.stringify(path.join(repo, 'scripts/lib/atomic-write.js'))};`, - 'const original=fs.readFileSync; let denied=0;', - `fs.readFileSync=function(input){ if(input===file){ denied++; throw Object.assign(new Error('injected unreadable aliases'),{code:${JSON.stringify(code)}}); } return original.apply(this,arguments); };`, - 'const record={exports:{}};', - "vm.runInNewContext(original(helper,'utf8'),{module:record,exports:record.exports,require,process:{platform:'win32',pid:process.pid},performance,Atomics,SharedArrayBuffer,Int32Array},{filename:helper});", - 'require.cache[helper]={id:helper,filename:helper,loaded:true,exports:record.exports};', - `process.on('exit',()=>fs.writeFileSync(${JSON.stringify(faultCount)},JSON.stringify(denied)));` - ].join('\n')); - args.push('--require', preload); - } + if (code) args.push('--require', path.join(repo, 'tests/fixtures/session-start-alias-read-preload.cjs')); try { const result = spawnSync(process.execPath, [...args, path.join(repo, 'scripts/hooks/session-start.js')], { - cwd: project, env, input: JSON.stringify({ hook_event_name: 'SessionStart', source: 'startup', session_id: `startup-${code}`, cwd: project }), + cwd: project, env: { ...env, ECC_TEST_ALIAS_PATH: aliasPath, + ECC_TEST_ALIAS_FAULT_COUNT: faultCount, ECC_TEST_ALIAS_READ_ERROR: code || '' }, input: JSON.stringify({ hook_event_name: 'SessionStart', source: 'startup', session_id: `startup-${code}`, cwd: project }), encoding: 'utf8', timeout: 15000, }); assert.ifError(result.error); @@ -63,7 +51,8 @@ try { assert.match(result.stderr, /No package manager preference found/); if (code) { assert.match(result.stderr, /Session aliases unavailable/); - assert.ok(JSON.parse(fs.readFileSync(faultCount, 'utf8')) >= 1); + const denied = JSON.parse(fs.readFileSync(faultCount, 'utf8')); + assert.ok(denied >= 1 && denied <= 21, 'Actual sharing retries remain bounded'); } else assert.match(result.stderr, /session alias\(es\) available: kept/); assert.strictEqual(fs.readFileSync(aliasPath, 'utf8'), aliasBytes); passed++; console.log(`PASS actual startup preserves context with ${code || 'readable'} aliases`);