Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
/**
|
|
* Windows shell quoting utilities tests
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
// We test the pure functions directly by dynamically importing after
|
|
// patching process.platform, since the functions check it at call time.
|
|
const originalPlatform = process.platform;
|
|
|
|
function setPlatform(platform: string) {
|
|
Object.defineProperty(process, 'platform', { value: platform, writable: true });
|
|
}
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform, writable: true });
|
|
});
|
|
|
|
describe('quoteForCmd', () => {
|
|
let quoteForCmd: (value: string) => string;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('@electron/utils/win-shell');
|
|
quoteForCmd = mod.quoteForCmd;
|
|
});
|
|
|
|
it('returns value unchanged on non-Windows', () => {
|
|
setPlatform('linux');
|
|
expect(quoteForCmd('C:\\Program Files\\uv.exe')).toBe('C:\\Program Files\\uv.exe');
|
|
});
|
|
|
|
it('returns value unchanged on macOS', () => {
|
|
setPlatform('darwin');
|
|
expect(quoteForCmd('/Applications/My App/bin')).toBe('/Applications/My App/bin');
|
|
});
|
|
|
|
it('returns value unchanged on Windows when no spaces', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('C:\\tools\\uv.exe')).toBe('C:\\tools\\uv.exe');
|
|
});
|
|
|
|
it('wraps in double quotes on Windows when path has spaces', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('C:\\Program Files\\uv.exe')).toBe('"C:\\Program Files\\uv.exe"');
|
|
});
|
|
|
|
it('wraps user home paths with spaces', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('C:\\Users\\John Doe\\AppData\\Local\\uv.exe'))
|
|
.toBe('"C:\\Users\\John Doe\\AppData\\Local\\uv.exe"');
|
|
});
|
|
|
|
it('does not double-quote already quoted values', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('"C:\\Program Files\\uv.exe"')).toBe('"C:\\Program Files\\uv.exe"');
|
|
});
|
|
|
|
it('handles simple command names without spaces', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('uv')).toBe('uv');
|
|
expect(quoteForCmd('node')).toBe('node');
|
|
expect(quoteForCmd('pnpm')).toBe('pnpm');
|
|
});
|
|
|
|
it('handles empty string', () => {
|
|
setPlatform('win32');
|
|
expect(quoteForCmd('')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('needsWinShell', () => {
|
|
let needsWinShell: (bin: string) => boolean;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('@electron/utils/win-shell');
|
|
needsWinShell = mod.needsWinShell;
|
|
});
|
|
|
|
it('returns false on non-Windows', () => {
|
|
setPlatform('linux');
|
|
expect(needsWinShell('uv')).toBe(false);
|
|
expect(needsWinShell('/usr/bin/uv')).toBe(false);
|
|
});
|
|
|
|
it('returns true on Windows for simple command names', () => {
|
|
setPlatform('win32');
|
|
expect(needsWinShell('uv')).toBe(true);
|
|
expect(needsWinShell('node')).toBe(true);
|
|
expect(needsWinShell('pnpm')).toBe(true);
|
|
});
|
|
|
|
it('returns false on Windows for absolute paths', () => {
|
|
setPlatform('win32');
|
|
expect(needsWinShell('C:\\Program Files\\uv.exe')).toBe(false);
|
|
expect(needsWinShell('D:\\tools\\bin\\uv.exe')).toBe(false);
|
|
});
|
|
|
|
it('returns true on Windows for relative paths', () => {
|
|
setPlatform('win32');
|
|
expect(needsWinShell('bin\\uv.exe')).toBe(true);
|
|
expect(needsWinShell('.\\uv.exe')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('prepareWinSpawn', () => {
|
|
let prepareWinSpawn: (
|
|
command: string,
|
|
args: string[],
|
|
forceShell?: boolean,
|
|
) => { shell: boolean; command: string; args: string[] };
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('@electron/utils/win-shell');
|
|
prepareWinSpawn = mod.prepareWinSpawn;
|
|
});
|
|
|
|
it('does not quote on non-Windows', () => {
|
|
setPlatform('linux');
|
|
const result = prepareWinSpawn('/usr/bin/uv', ['python', 'install', '3.12']);
|
|
expect(result.shell).toBe(false);
|
|
expect(result.command).toBe('/usr/bin/uv');
|
|
expect(result.args).toEqual(['python', 'install', '3.12']);
|
|
});
|
|
|
|
it('quotes command and args with spaces on Windows with shell', () => {
|
|
setPlatform('win32');
|
|
const result = prepareWinSpawn(
|
|
'C:\\Program Files\\uv.exe',
|
|
['python', 'install', '3.12'],
|
|
true,
|
|
);
|
|
expect(result.shell).toBe(true);
|
|
expect(result.command).toBe('"C:\\Program Files\\uv.exe"');
|
|
expect(result.args).toEqual(['python', 'install', '3.12']);
|
|
});
|
|
|
|
it('quotes args that contain spaces on Windows with shell', () => {
|
|
setPlatform('win32');
|
|
const result = prepareWinSpawn(
|
|
'node',
|
|
['C:\\Users\\John Doe\\script.js', '--port', '18789'],
|
|
true,
|
|
);
|
|
expect(result.shell).toBe(true);
|
|
expect(result.command).toBe('node');
|
|
expect(result.args).toEqual(['"C:\\Users\\John Doe\\script.js"', '--port', '18789']);
|
|
});
|
|
|
|
it('auto-detects shell need based on absolute path on Windows', () => {
|
|
setPlatform('win32');
|
|
const absResult = prepareWinSpawn(
|
|
'C:\\tools\\uv.exe',
|
|
['python', 'find', '3.12'],
|
|
);
|
|
expect(absResult.shell).toBe(false);
|
|
|
|
const relResult = prepareWinSpawn(
|
|
'uv',
|
|
['python', 'find', '3.12'],
|
|
);
|
|
expect(relResult.shell).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('normalizeNodeRequirePathForNodeOptions', () => {
|
|
let normalizeNodeRequirePathForNodeOptions: (modulePath: string) => string;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('@electron/utils/win-shell');
|
|
normalizeNodeRequirePathForNodeOptions = mod.normalizeNodeRequirePathForNodeOptions;
|
|
});
|
|
|
|
it('returns path unchanged on non-Windows', () => {
|
|
setPlatform('linux');
|
|
expect(normalizeNodeRequirePathForNodeOptions('/home/user/.config/app/preload.cjs'))
|
|
.toBe('/home/user/.config/app/preload.cjs');
|
|
});
|
|
|
|
it('converts backslashes to forward slashes on Windows', () => {
|
|
setPlatform('win32');
|
|
expect(normalizeNodeRequirePathForNodeOptions('C:\\Users\\70954\\AppData\\Roaming\\clawx\\gateway-fetch-preload.cjs'))
|
|
.toBe('C:/Users/70954/AppData/Roaming/clawx/gateway-fetch-preload.cjs');
|
|
});
|
|
|
|
it('leaves forward slashes intact on Windows', () => {
|
|
setPlatform('win32');
|
|
expect(normalizeNodeRequirePathForNodeOptions('C:/already/forward/slashes.cjs'))
|
|
.toBe('C:/already/forward/slashes.cjs');
|
|
});
|
|
});
|
|
|
|
describe('appendNodeRequireToNodeOptions', () => {
|
|
let appendNodeRequireToNodeOptions: (nodeOptions: string | undefined, modulePath: string) => string;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('@electron/utils/win-shell');
|
|
appendNodeRequireToNodeOptions = mod.appendNodeRequireToNodeOptions;
|
|
});
|
|
|
|
it('creates NODE_OPTIONS from undefined', () => {
|
|
setPlatform('linux');
|
|
expect(appendNodeRequireToNodeOptions(undefined, '/tmp/preload.cjs'))
|
|
.toBe('--require "/tmp/preload.cjs"');
|
|
});
|
|
|
|
it('appends to existing NODE_OPTIONS', () => {
|
|
setPlatform('linux');
|
|
expect(appendNodeRequireToNodeOptions('--disable-warning=ExperimentalWarning', '/tmp/preload.cjs'))
|
|
.toBe('--disable-warning=ExperimentalWarning --require "/tmp/preload.cjs"');
|
|
});
|
|
|
|
it('normalizes Windows backslashes in the module path', () => {
|
|
setPlatform('win32');
|
|
expect(appendNodeRequireToNodeOptions(undefined, 'C:\\Users\\test\\preload.cjs'))
|
|
.toBe('--require "C:/Users/test/preload.cjs"');
|
|
});
|
|
|
|
it('appends to existing NODE_OPTIONS on Windows with normalized path', () => {
|
|
setPlatform('win32');
|
|
expect(appendNodeRequireToNodeOptions('--max-old-space-size=4096', 'D:\\app\\data\\preload.cjs'))
|
|
.toBe('--max-old-space-size=4096 --require "D:/app/data/preload.cjs"');
|
|
});
|
|
});
|