mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
800 lines
22 KiB
JavaScript
800 lines
22 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import * as path from 'node:path';
|
|
import { Buffer } from 'node:buffer';
|
|
import process from 'node:process';
|
|
import {
|
|
sanitizeArgv,
|
|
getSafeName,
|
|
verifyIntegrity,
|
|
prepareRuntime,
|
|
main,
|
|
} from './sea-launch.cjs';
|
|
|
|
// Mocking fs and os
|
|
// We need to use vi.mock factory for ESM mocking of built-in modules in Vitest
|
|
vi.mock('node:fs', async () => {
|
|
const fsMock = {
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
existsSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
lstatSync: vi.fn(),
|
|
statSync: vi.fn(),
|
|
openSync: vi.fn(),
|
|
readSync: vi.fn(),
|
|
closeSync: vi.fn(),
|
|
};
|
|
return {
|
|
default: fsMock,
|
|
...fsMock,
|
|
};
|
|
});
|
|
vi.mock('fs', async () => {
|
|
const fsMock = {
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
existsSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
lstatSync: vi.fn(),
|
|
statSync: vi.fn(),
|
|
openSync: vi.fn(),
|
|
readSync: vi.fn(),
|
|
closeSync: vi.fn(),
|
|
};
|
|
return {
|
|
default: fsMock,
|
|
...fsMock,
|
|
};
|
|
});
|
|
|
|
vi.mock('node:os', async () => {
|
|
const osMock = {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
};
|
|
return {
|
|
default: osMock,
|
|
...osMock,
|
|
};
|
|
});
|
|
vi.mock('os', async () => {
|
|
const osMock = {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
};
|
|
return {
|
|
default: osMock,
|
|
...osMock,
|
|
};
|
|
});
|
|
|
|
describe('sea-launch', () => {
|
|
describe('main', () => {
|
|
it('executes main logic', async () => {
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
const consoleSpy = vi
|
|
.spyOn(globalThis.console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
const mockGetAsset = vi.fn((key) => {
|
|
if (key === 'manifest.json')
|
|
return JSON.stringify({ version: '1.0.0', mainHash: 'h1' });
|
|
return Buffer.from('content');
|
|
});
|
|
|
|
await main(mockGetAsset);
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
expect(exitSpy).toHaveBeenCalled();
|
|
|
|
exitSpy.mockRestore();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('sanitizeArgv', () => {
|
|
it('removes ghost argument when argv[2] matches execPath', () => {
|
|
const execPath = '/bin/node';
|
|
const argv = ['/bin/node', '/app/script.js', '/bin/node', 'arg1'];
|
|
const resolveFn = (p) => p;
|
|
const removed = sanitizeArgv(argv, execPath, resolveFn);
|
|
expect(removed).toBe(true);
|
|
expect(argv).toEqual(['/bin/node', '/app/script.js', 'arg1']);
|
|
});
|
|
|
|
it('does nothing if argv[2] does not match execPath', () => {
|
|
const execPath = '/bin/node';
|
|
const argv = ['/bin/node', '/app/script.js', 'command', 'arg1'];
|
|
const resolveFn = (p) => p;
|
|
const removed = sanitizeArgv(argv, execPath, resolveFn);
|
|
expect(removed).toBe(false);
|
|
expect(argv).toHaveLength(4);
|
|
});
|
|
|
|
it('handles resolving relative paths', () => {
|
|
const execPath = '/bin/node';
|
|
const argv = ['/bin/node', '/app/script.js', './node', 'arg1'];
|
|
const resolveFn = (p) => (p === './node' ? '/bin/node' : p);
|
|
const removed = sanitizeArgv(argv, execPath, resolveFn);
|
|
expect(removed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getSafeName', () => {
|
|
it('sanitizes strings', () => {
|
|
expect(getSafeName('user@name')).toBe('user_name');
|
|
expect(getSafeName('../path')).toBe('.._path');
|
|
expect(getSafeName('valid-1.2')).toBe('valid-1.2');
|
|
expect(getSafeName(undefined)).toBe('unknown');
|
|
});
|
|
});
|
|
|
|
describe('verifyIntegrity', () => {
|
|
it('returns true for matching hashes', () => {
|
|
const dir = '/tmp/test';
|
|
const manifest = {
|
|
mainHash: 'hash1',
|
|
files: [{ path: 'file.txt', hash: 'hash2' }],
|
|
};
|
|
|
|
const mockFs = {
|
|
openSync: vi.fn((p) => {
|
|
if (p.endsWith('gemini.mjs')) return 10;
|
|
if (p.endsWith('file.txt')) return 20;
|
|
throw new Error('Not found');
|
|
}),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
let content = '';
|
|
if (fd === 10) content = 'content1';
|
|
if (fd === 20) content = 'content2';
|
|
|
|
// Simulate simple read: write content to buffer and return length once, then return 0
|
|
if (!buffer._readDone) {
|
|
const buf = Buffer.from(content);
|
|
buf.copy(buffer);
|
|
buffer._readDone = true;
|
|
return buf.length;
|
|
} else {
|
|
buffer._readDone = false; // Reset for next file
|
|
return 0;
|
|
}
|
|
}),
|
|
closeSync: vi.fn(),
|
|
};
|
|
|
|
const mockCrypto = {
|
|
createHash: vi.fn(() => ({
|
|
update: vi.fn(function (content) {
|
|
this._content =
|
|
(this._content || '') + Buffer.from(content).toString();
|
|
return this;
|
|
}),
|
|
digest: vi.fn(function () {
|
|
if (this._content === 'content1') return 'hash1';
|
|
if (this._content === 'content2') return 'hash2';
|
|
return 'wrong';
|
|
}),
|
|
})),
|
|
};
|
|
|
|
expect(verifyIntegrity(dir, manifest, mockFs, mockCrypto)).toBe(true);
|
|
});
|
|
|
|
it('returns false for mismatched hashes', () => {
|
|
const dir = '/tmp/test';
|
|
const manifest = { mainHash: 'hash1' };
|
|
|
|
const mockFs = {
|
|
openSync: vi.fn(() => 10),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
const buf = Buffer.from('content_wrong');
|
|
buf.copy(buffer);
|
|
buffer._readDone = true;
|
|
return buf.length;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
};
|
|
|
|
const mockCrypto = {
|
|
createHash: vi.fn(() => ({
|
|
update: vi.fn(function (content) {
|
|
this._content =
|
|
(this._content || '') + Buffer.from(content).toString();
|
|
return this;
|
|
}),
|
|
digest: vi.fn(function () {
|
|
return 'hash_wrong';
|
|
}),
|
|
})),
|
|
};
|
|
|
|
expect(verifyIntegrity(dir, manifest, mockFs, mockCrypto)).toBe(false);
|
|
});
|
|
|
|
it('returns false when fs throws error', () => {
|
|
const dir = '/tmp/test';
|
|
const manifest = { mainHash: 'hash1' };
|
|
const mockFs = {
|
|
openSync: vi.fn(() => {
|
|
throw new Error('FS Error');
|
|
}),
|
|
};
|
|
const mockCrypto = { createHash: vi.fn() };
|
|
expect(verifyIntegrity(dir, manifest, mockFs, mockCrypto)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('prepareRuntime', () => {
|
|
const mockManifest = {
|
|
version: '1.0.0',
|
|
mainHash: 'h1',
|
|
files: [{ key: 'f1', path: 'p1', hash: 'h1' }],
|
|
};
|
|
const mockGetAsset = vi.fn();
|
|
const S_IFDIR = 0o40000;
|
|
const MODE_700 = 0o700;
|
|
|
|
it('reuses existing runtime if secure and valid', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn(() => true),
|
|
rmSync: vi.fn(),
|
|
readFileSync: vi.fn(),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 1000,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
};
|
|
|
|
deps.fs.readFileSync.mockReturnValue('content');
|
|
|
|
const runtime = prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
expect(runtime).toContain('gemini-runtime-1.0.0-user');
|
|
expect(deps.fs.rmSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('recreates runtime if existing has wrong owner', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
|
|
rmSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 999, // Wrong UID
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
processPid: 123,
|
|
};
|
|
|
|
mockGetAsset.mockReturnValue(Buffer.from('asset_content'));
|
|
|
|
prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(deps.fs.rmSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-runtime'),
|
|
expect.anything(),
|
|
);
|
|
expect(deps.fs.mkdirSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-setup'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('recreates runtime if existing has wrong permissions', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
|
|
rmSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 1000,
|
|
mode: S_IFDIR | 0o777, // Too open
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
processPid: 123,
|
|
};
|
|
|
|
mockGetAsset.mockReturnValue(Buffer.from('asset_content'));
|
|
|
|
prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(deps.fs.rmSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-runtime'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('creates new runtime if existing is invalid (integrity check)', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValueOnce(true).mockReturnValue(false),
|
|
rmSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('wrong_content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 1000,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'hash_calculated'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
processPid: 123,
|
|
};
|
|
|
|
mockGetAsset.mockReturnValue(Buffer.from('asset_content'));
|
|
|
|
prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(deps.fs.rmSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-runtime'),
|
|
expect.anything(),
|
|
);
|
|
expect(deps.fs.mkdirSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-setup'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('handles rename race condition: uses target if secure and valid', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(() => {
|
|
throw new Error('Rename failed');
|
|
}),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 1000,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
processPid: 123,
|
|
};
|
|
|
|
// 1. Initial exists check -> false
|
|
// 2. mkdir checks (destDir) -> false
|
|
// 3. renameSync -> throws
|
|
// 4. existsSync (race check) -> true
|
|
deps.fs.existsSync
|
|
.mockReturnValueOnce(false)
|
|
.mockReturnValueOnce(false)
|
|
.mockReturnValue(true);
|
|
|
|
mockGetAsset.mockReturnValue(Buffer.from('asset_content'));
|
|
|
|
const runtime = prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(deps.fs.renameSync).toHaveBeenCalled();
|
|
expect(runtime).toContain('gemini-runtime');
|
|
expect(deps.fs.rmSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('gemini-setup'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('handles rename race condition: fails if target is insecure', () => {
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(() => {
|
|
throw new Error('Rename failed');
|
|
}),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 999, // Wrong UID
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => '/tmp',
|
|
},
|
|
path: path,
|
|
processEnv: {},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 1000,
|
|
processPid: 123,
|
|
};
|
|
|
|
deps.fs.existsSync
|
|
.mockReturnValueOnce(false)
|
|
.mockReturnValueOnce(false)
|
|
.mockReturnValue(true);
|
|
|
|
mockGetAsset.mockReturnValue(Buffer.from('asset_content'));
|
|
|
|
// Mock process.exit and console.error
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
const consoleSpy = vi
|
|
.spyOn(globalThis.console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
|
|
exitSpy.mockRestore();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('uses LOCALAPPDATA on Windows if available', () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', {
|
|
value: 'win32',
|
|
configurable: true,
|
|
});
|
|
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValue(false),
|
|
mkdirSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 0,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => 'C:\\Temp',
|
|
},
|
|
path: {
|
|
join: (...args) => args.join('\\'),
|
|
dirname: (p) => p.split('\\').slice(0, -1).join('\\'),
|
|
resolve: (p) => p,
|
|
},
|
|
processEnv: {
|
|
LOCALAPPDATA: 'C:\\Users\\User\\AppData\\Local',
|
|
},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 'unknown',
|
|
};
|
|
|
|
prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
expect(deps.fs.mkdirSync).toHaveBeenCalledWith(
|
|
'C:\\Users\\User\\AppData\\Local\\Google\\GeminiCLI',
|
|
expect.objectContaining({ recursive: true }),
|
|
);
|
|
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it('falls back to tmpdir on Windows if LOCALAPPDATA is missing', () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', {
|
|
value: 'win32',
|
|
configurable: true,
|
|
});
|
|
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValue(false),
|
|
mkdirSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 0,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => 'C:\\Temp',
|
|
},
|
|
path: {
|
|
join: (...args) => args.join('\\'),
|
|
dirname: (p) => p.split('\\').slice(0, -1).join('\\'),
|
|
resolve: (p) => p,
|
|
},
|
|
processEnv: {}, // Missing LOCALAPPDATA
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 'unknown',
|
|
};
|
|
|
|
const runtime = prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
// Should use tmpdir
|
|
expect(runtime).toContain('C:\\Temp');
|
|
expect(runtime).not.toContain('Google\\GeminiCLI');
|
|
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it('falls back to tmpdir on Windows if mkdir fails', () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', {
|
|
value: 'win32',
|
|
configurable: true,
|
|
});
|
|
|
|
const deps = {
|
|
fs: {
|
|
existsSync: vi.fn().mockReturnValue(false),
|
|
mkdirSync: vi.fn((p) => {
|
|
if (typeof p === 'string' && p.includes('Google\\GeminiCLI')) {
|
|
throw new Error('Permission denied');
|
|
}
|
|
}),
|
|
rmSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
renameSync: vi.fn(),
|
|
readFileSync: vi.fn().mockReturnValue('content'),
|
|
openSync: vi.fn(() => 1),
|
|
readSync: vi.fn((fd, buffer) => {
|
|
if (!buffer._readDone) {
|
|
buffer._readDone = true;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}),
|
|
closeSync: vi.fn(),
|
|
lstatSync: vi.fn(() => ({
|
|
isDirectory: () => true,
|
|
uid: 0,
|
|
mode: S_IFDIR | MODE_700,
|
|
})),
|
|
},
|
|
os: {
|
|
userInfo: () => ({ username: 'user' }),
|
|
tmpdir: () => 'C:\\Temp',
|
|
},
|
|
path: {
|
|
join: (...args) => args.join('\\'),
|
|
dirname: (p) => p.split('\\').slice(0, -1).join('\\'),
|
|
resolve: (p) => p,
|
|
},
|
|
processEnv: {
|
|
LOCALAPPDATA: 'C:\\Users\\User\\AppData\\Local',
|
|
},
|
|
crypto: {
|
|
createHash: vi.fn(() => {
|
|
const hash = {
|
|
update: vi.fn().mockReturnThis(),
|
|
digest: vi.fn(() => 'h1'),
|
|
};
|
|
return hash;
|
|
}),
|
|
},
|
|
processUid: 'unknown',
|
|
};
|
|
|
|
const runtime = prepareRuntime(mockManifest, mockGetAsset, deps);
|
|
|
|
// Should use tmpdir
|
|
expect(runtime).toContain('C:\\Temp');
|
|
expect(deps.fs.mkdirSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('Google\\GeminiCLI'),
|
|
expect.anything(),
|
|
);
|
|
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
});
|
|
});
|
|
});
|