test(gateguard): expect native directory keys on Windows

This commit is contained in:
haelyra
2026-10-10 18:56:03 -04:00
parent 19a1b62f35
commit d34d022bc5
2 changed files with 32 additions and 7 deletions
+5 -3
View File
@@ -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: '' };
+27 -4
View File
@@ -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 };