/** * #2667 — the Windows extended-length (`\\?\`) prefix must never survive into a * path GitNexus compares or keys on. * * `stripWindowsLongPathPrefix` is a POSIX no-op, so these assertions only bite on * windows-latest; the file is registered in `scripts/cross-platform-tests.ts` for * exactly that reason. Like `analyzer-identity-path-normalization.test.ts`, it holds * ONLY pure-function assertions with an explicit `platform` argument — no fixture, * no filesystem — so it stays green on every runner. */ import { describe, it, expect } from 'vitest'; import path from 'path'; import { stripWindowsLongPathPrefix } from '../../src/lib/utils.js'; describe('stripWindowsLongPathPrefix (#2667)', () => { it('strips the prefix from a drive path', () => { expect(stripWindowsLongPathPrefix('\\\\?\\D:\\Projects\\repo', 'win32')).toBe( 'D:\\Projects\\repo', ); }); it('rewrites the UNC form back to its `\\\\server\\share` shape', () => { expect(stripWindowsLongPathPrefix('\\\\?\\UNC\\server\\share\\repo', 'win32')).toBe( '\\\\server\\share\\repo', ); }); // The namespace `\\?\` addresses is case-insensitive, so a caller can spell // the token in any case. Matching only `UNC` left `\\?\unc\…` prefixed, which // is the #2667 registry mismatch all over again on a network share. it('rewrites the UNC form whatever case the token is spelled in', () => { expect(stripWindowsLongPathPrefix('\\\\?\\unc\\server\\share\\repo', 'win32')).toBe( '\\\\server\\share\\repo', ); expect(stripWindowsLongPathPrefix('\\\\?\\Unc\\server\\share\\repo', 'win32')).toBe( '\\\\server\\share\\repo', ); }); it('leaves a volume-GUID path untouched — its remainder is not a usable path', () => { expect(stripWindowsLongPathPrefix('\\\\?\\Volume{1a2b3c4d}\\repo', 'win32')).toBe( '\\\\?\\Volume{1a2b3c4d}\\repo', ); }); it('leaves the `\\\\.\\` device namespace untouched', () => { // Most of what it addresses is not a filesystem path at all. expect(stripWindowsLongPathPrefix('\\\\.\\D:\\repo', 'win32')).toBe('\\\\.\\D:\\repo'); expect(stripWindowsLongPathPrefix('\\\\.\\PhysicalDrive0', 'win32')).toBe( '\\\\.\\PhysicalDrive0', ); }); // Degenerate extended paths: stripping these would emit something worse than // the input. `\\?\UNC` has no share to keep, so a blind slice yields the bare // root `\\`; `\\?\D:foo` is drive-RELATIVE, so a blind slice yields `D:foo`, // which is not absolute and would resolve against the process cwd. Both are // left untouched so they simply fail to match a registry entry. it('leaves a UNC prefix with no share component untouched', () => { expect(stripWindowsLongPathPrefix('\\\\?\\UNC\\', 'win32')).toBe('\\\\?\\UNC\\'); expect(stripWindowsLongPathPrefix('\\\\?\\UNC', 'win32')).toBe('\\\\?\\UNC'); }); it('leaves a drive-relative extended path untouched, so output stays absolute', () => { expect(stripWindowsLongPathPrefix('\\\\?\\D:foo', 'win32')).toBe('\\\\?\\D:foo'); expect(path.win32.isAbsolute(stripWindowsLongPathPrefix('\\\\?\\D:\\foo', 'win32'))).toBe(true); }); it('is a no-op on already-canonical drive and UNC paths', () => { expect(stripWindowsLongPathPrefix('D:\\Projects\\repo', 'win32')).toBe('D:\\Projects\\repo'); expect(stripWindowsLongPathPrefix('\\\\server\\share\\repo', 'win32')).toBe( '\\\\server\\share\\repo', ); }); it('is idempotent — it runs wherever a comparison key is built', () => { const once = stripWindowsLongPathPrefix('\\\\?\\D:\\Projects\\repo', 'win32'); expect(stripWindowsLongPathPrefix(once, 'win32')).toBe(once); const uncOnce = stripWindowsLongPathPrefix('\\\\?\\UNC\\server\\share\\repo', 'win32'); expect(stripWindowsLongPathPrefix(uncOnce, 'win32')).toBe(uncOnce); }); // Near-miss spellings must fail closed rather than be half-normalized: none of // these is the extended-length prefix, so none may be sliced. it('leaves near-miss namespace spellings untouched', () => { expect(stripWindowsLongPathPrefix('\\\\??\\D:\\repo', 'win32')).toBe('\\\\??\\D:\\repo'); expect(stripWindowsLongPathPrefix('\\\\?\\\\D:\\repo', 'win32')).toBe('\\\\?\\\\D:\\repo'); expect(stripWindowsLongPathPrefix('\\?\\D:\\repo', 'win32')).toBe('\\?\\D:\\repo'); expect(stripWindowsLongPathPrefix('\\\\?\\GLOBALROOT\\Device\\X', 'win32')).toBe( '\\\\?\\GLOBALROOT\\Device\\X', ); }); it('handles degenerate and empty input without throwing', () => { expect(stripWindowsLongPathPrefix('', 'win32')).toBe(''); expect(stripWindowsLongPathPrefix('\\\\?\\', 'win32')).toBe('\\\\?\\'); expect(stripWindowsLongPathPrefix('\\\\', 'win32')).toBe('\\\\'); expect(stripWindowsLongPathPrefix('D:', 'win32')).toBe('D:'); }); // The helper matches the backslash spelling only, by design — `path.resolve` // folds `//?/` into it first. A half-converted path is left alone rather than // sliced on one separator convention and rejoined on the other. it('leaves a forward-slash or mixed-separator prefix untouched', () => { expect(stripWindowsLongPathPrefix('//?/D:/repo', 'win32')).toBe('//?/D:/repo'); expect(stripWindowsLongPathPrefix('//?/UNC/server/share', 'win32')).toBe( '//?/UNC/server/share', ); // Backslash prefix with a forward-slash body IS sliced — the prefix matched. expect(stripWindowsLongPathPrefix('\\\\?\\D:\\a/b/c', 'win32')).toBe('D:\\a/b/c'); }); it('is a no-op off Windows, where `\\\\?\\…` is an ordinary filename', () => { expect(stripWindowsLongPathPrefix('\\\\?\\D:\\repo', 'linux')).toBe('\\\\?\\D:\\repo'); expect(stripWindowsLongPathPrefix('/home/node/repo', 'linux')).toBe('/home/node/repo'); }); // The leak this normalization exists to prevent. `path.win32.relative` cannot // express a relative path between a prefixed and an un-prefixed form of the SAME // directory — they share no root — so it returns the absolute target instead. // That absolute string is exactly what #2667 reported inside node IDs // (`Function:\\?\D:\…\market.move:…`), and it is the same defect class as the // cross-drive `isInside` bug fixed in #2688. it('makes a mixed-prefix relativization relative again', () => { const prefixed = '\\\\?\\D:\\repo'; const child = 'D:\\repo\\a\\b.move'; expect(path.win32.relative(prefixed, child)).toBe(child); expect(path.win32.relative(stripWindowsLongPathPrefix(prefixed, 'win32'), child)).toBe( 'a\\b.move', ); }); // Why `canonicalizePath`'s `catch` branch leaked and its realpath branch did // not. The repo-manager regression tests for that branch can only run on // windows-latest, so pin the underlying platform fact here, where it runs // everywhere: `path.resolve` carries the prefix through untouched, which is // all the fallback branch used to do. Also pins the forward-slash spelling // that the helper deliberately does not match, because `resolve` folds it // into the backslash form first. it('pins that path.resolve preserves the prefix (the fallback branch #2667 leaked through)', () => { expect(path.win32.resolve('\\\\?\\D:\\repo\\sub')).toBe('\\\\?\\D:\\repo\\sub'); expect(path.win32.resolve('//?/D:/repo/sub')).toBe('\\\\?\\D:\\repo\\sub'); expect(stripWindowsLongPathPrefix(path.win32.resolve('\\\\?\\D:\\repo\\sub'), 'win32')).toBe( 'D:\\repo\\sub', ); }); });