refactor(core): extract and centralize sandbox path utilities (#25305)

Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
Emily Hedlund
2026-04-13 11:43:13 -07:00
committed by GitHub
parent b91d177bde
commit 0d6d5d90b9
6 changed files with 121 additions and 116 deletions
+60
View File
@@ -16,6 +16,8 @@ import {
normalizePath,
resolveToRealPath,
makeRelative,
deduplicateAbsolutePaths,
toPathKey,
} from './paths.js';
vi.mock('node:fs', async (importOriginal) => {
@@ -702,4 +704,62 @@ describe('normalizePath', () => {
expect(result).toBe('/usr/local/bin');
});
});
describe('deduplicateAbsolutePaths', () => {
it('should return an empty array if no paths are provided', () => {
expect(deduplicateAbsolutePaths(undefined)).toEqual([]);
expect(deduplicateAbsolutePaths(null)).toEqual([]);
expect(deduplicateAbsolutePaths([])).toEqual([]);
});
it('should deduplicate paths using their normalized identity', () => {
const paths = ['/workspace/foo', '/workspace/foo/'];
expect(deduplicateAbsolutePaths(paths)).toEqual(['/workspace/foo']);
});
it('should handle case-insensitivity on Windows and macOS', () => {
mockPlatform('win32');
const paths = ['/workspace/foo', '/Workspace/Foo'];
expect(deduplicateAbsolutePaths(paths)).toEqual(['/workspace/foo']);
mockPlatform('darwin');
const macPaths = ['/tmp/foo', '/Tmp/Foo'];
expect(deduplicateAbsolutePaths(macPaths)).toEqual(['/tmp/foo']);
mockPlatform('linux');
const linuxPaths = ['/tmp/foo', '/tmp/FOO'];
expect(deduplicateAbsolutePaths(linuxPaths)).toEqual([
'/tmp/foo',
'/tmp/FOO',
]);
});
it('should throw an error if a path is not absolute', () => {
const paths = ['relative/path'];
expect(() => deduplicateAbsolutePaths(paths)).toThrow(
'Path must be absolute: relative/path',
);
});
});
describe('toPathKey', () => {
it('should normalize paths and strip trailing slashes', () => {
expect(toPathKey('/foo/bar//baz/')).toBe(path.normalize('/foo/bar/baz'));
});
it('should convert paths to lowercase on Windows and macOS', () => {
mockPlatform('win32');
expect(toPathKey('/Workspace/Foo')).toBe(
path.normalize('/workspace/foo'),
);
// Ensure drive roots are preserved
expect(toPathKey('C:\\')).toBe('c:\\');
mockPlatform('darwin');
expect(toPathKey('/Tmp/Foo')).toBe(path.normalize('/tmp/foo'));
mockPlatform('linux');
expect(toPathKey('/Tmp/Foo')).toBe(path.normalize('/Tmp/Foo'));
});
});
});
+42
View File
@@ -454,3 +454,45 @@ function robustRealpath(p: string, visited = new Set<string>()): string {
throw e;
}
}
/**
* Deduplicates an array of paths and ensures all paths are absolute.
*/
export function deduplicateAbsolutePaths(paths?: string[] | null): string[] {
if (!paths || paths.length === 0) return [];
const uniquePathsMap = new Map<string, string>();
for (const p of paths) {
if (!path.isAbsolute(p)) {
throw new Error(`Path must be absolute: ${p}`);
}
const key = toPathKey(p);
if (!uniquePathsMap.has(key)) {
uniquePathsMap.set(key, p);
}
}
return Array.from(uniquePathsMap.values());
}
/**
* Returns a stable string key for a path to be used in comparisons or Map lookups.
*/
export function toPathKey(p: string): string {
// Normalize path segments
let norm = path.normalize(p);
// Strip trailing slashes (except for root paths)
if (norm.length > 1 && (norm.endsWith('/') || norm.endsWith('\\'))) {
// On Windows, don't strip the slash from a drive root (e.g., "C:\\")
if (!/^[a-zA-Z]:[\\/]$/.test(norm)) {
norm = norm.slice(0, -1);
}
}
// Convert to lowercase on case-insensitive platforms
const platform = process.platform;
const isCaseInsensitive = platform === 'win32' || platform === 'darwin';
return isCaseInsensitive ? norm.toLowerCase() : norm;
}