diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 7768bea3f..d5232ffd5 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -5794,7 +5794,9 @@ function runTests() { }); // --- Same-turn sibling creations and counters --- - const projectRoot = fs.realpathSync(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-proj-'))); + // Native realpath expands Windows8.3 temp aliases; expected directory keys + // use physical fixture roots independently of the production path helpers. + const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-proj-'))); const projectEnv = { CLAUDE_PROJECT_DIR: projectRoot, CLAUDE_TRANSCRIPT_PATH: '' }; const siblingOutputs = []; const projectRun = (toolName, toolInput, transcriptPath, env = {}) => { @@ -6196,7 +6198,7 @@ function runTests() { }); // --- Prior-search credit: scope and matching rules --- - const scopeRoot = fs.realpathSync(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-scope-proj-'))); + const scopeRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-scope-proj-'))); // Shell fixtures use forward slashes: bash treats unquoted backslashes as escapes. const scopeRootSh = scopeRoot.replace(/\\/g, '/'); for (const dir of ['src/handlers', 'handlers', 'f']) fs.mkdirSync(path.join(scopeRoot, dir), { recursive: true }); @@ -6426,7 +6428,7 @@ function runTests() { // --- Sibling collapse keyed by target class --- // The project root sits under an ancestor `tests/` on purpose: `src/a.py` must still be code. - const classBase = fs.realpathSync(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-class-'))); + const classBase = fs.realpathSync.native(fs.mkdtempSync(path.join(tmpRoot, 'gateguard-class-'))); const classRoot = path.join(classBase, 'tests', 'proj'); fs.mkdirSync(classRoot, { recursive: true }); const classEnv = { CLAUDE_PROJECT_DIR: classRoot, CLAUDE_TRANSCRIPT_PATH: '' }; diff --git a/tests/lib/gateguard-target-class.test.js b/tests/lib/gateguard-target-class.test.js index 47a2b2602..06a3cf7c0 100644 --- a/tests/lib/gateguard-target-class.test.js +++ b/tests/lib/gateguard-target-class.test.js @@ -224,8 +224,10 @@ test('Windows name tricks: trailing dots/spaces, streams and 8.3 short names', ( // ── collapseGateDir (real directories) ── console.log('\ncollapseGateDir:'); +// Native paths give the OS identity independently of collapseGateDir, including +// Windows8.3 aliases that legacy realpath may retain in a temporary root. function tempProject() { - const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-collapse-'))); + const root = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-collapse-'))); fs.mkdirSync(path.join(root, 'src', 'real'), { recursive: true }); fs.mkdirSync(path.join(root, '.claude', 'hooks'), { recursive: true }); return root; @@ -261,6 +263,27 @@ test('the worktree prefix is stripped only for a real worktree (.git present)', } }); +test('native canonical directory keys match raw aliases and missing descendants', () => { + const rawRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-native-key-')); + const nativeRoot = fs.realpathSync.native(rawRoot); + const expectedKey = value => process.platform === 'win32' ? value.replace(/\\/g, '/').toLowerCase() : value; + try { + fs.mkdirSync(path.join(rawRoot, 'src', 'real'), { recursive: true }); + fs.mkdirSync(path.join(rawRoot, '.claude', 'hooks'), { recursive: true }); + withProjectDir(rawRoot, () => { + const data = { cwd: rawRoot }; + for (const root of [rawRoot, nativeRoot]) { + assert.strictEqual(collapseGateDir(path.join(root, 'src', 'real', 'a.js'), data, 'code'), expectedKey(path.join(nativeRoot, 'src', 'real'))); + assert.strictEqual(collapseGateDir(path.join(root, 'src', 'missing', 'deeper', 'a.js'), data, 'code'), expectedKey(path.join(nativeRoot, 'src', 'missing', 'deeper'))); + assert.strictEqual(collapseGateDir(path.join(root, '.claude', 'hooks', 'a.js'), data, 'code'), null, 'native alias cannot collapse an instruction directory'); + } + if (process.platform === 'win32') { + assert.strictEqual(collapseGateDir(path.join(rawRoot.toUpperCase(), 'SRC', 'REAL', 'a.js'), data, 'code'), expectedKey(path.join(nativeRoot, 'src', 'real')), 'case aliases share the same native directory'); + } + }); + } finally { fs.rmSync(rawRoot, { recursive: true, force: true }); } +}); + test('collapse is screened on the real directory (symlinks, missing parents, fs errors)', () => { const root = tempProject(); try { @@ -282,7 +305,7 @@ test('collapse is screened on the real directory (symlinks, missing parents, fs trySymlink(at('tests'), at('src/t-alias')); fs.mkdirSync(at('tests'), { recursive: true }); assert.strictEqual(collapseGateDir(at('src/t-alias/b.js'), data, 'code'), null, 'real location has another class'); - const outside = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-outside-'))); + const outside = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-outside-'))); try { trySymlink(outside, at('src/out')); assert.strictEqual(collapseGateDir(at('src/out/b.js'), data, 'code'), null, 'symlink out of the project'); @@ -478,7 +501,7 @@ test('isSensitiveTarget fails safe (any error => sensitive)', () => { }); test('isSensitiveTargetFor uses the project-relative class path', () => { - const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-sensitive-'))); + const root = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-sensitive-'))); try { // The project itself sits under an ancestor named `security`: only project-relative segments count. const proj = path.join(root, 'security', 'proj'); @@ -502,7 +525,7 @@ test('isSensitiveTargetFor uses the project-relative class path', () => { test('isSensitiveTargetFor also judges the real (symlink-resolved) path', () => { const root = tempProject(); - const outside = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-outside-'))); + const outside = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-outside-'))); try { withProjectDir(root, () => { const data = { cwd: root };