feat(test): Increase test coverage across CLI and Core packages (#1089)
This commit is contained in:
parent
f00b9f2727
commit
197704c630
|
@ -438,6 +438,7 @@ Add any other context about the problem here.
|
|||
...mockConfig,
|
||||
getBugCommand: vi.fn(() => bugCommand),
|
||||
} as unknown as Config;
|
||||
process.env.CLI_VERSION = '0.1.0';
|
||||
|
||||
const { handleSlashCommand } = getProcessor();
|
||||
const bugDescription = 'This is a custom bug';
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getStartupWarnings } from './startupWarnings.js';
|
||||
import * as fs from 'fs/promises';
|
||||
import { getErrorMessage } from '@gemini-cli/core';
|
||||
|
||||
vi.mock('fs/promises');
|
||||
vi.mock('@gemini-cli/core', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
getErrorMessage: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe.skip('startupWarnings', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return warnings from the file and delete it', async () => {
|
||||
const mockWarnings = 'Warning 1\nWarning 2';
|
||||
vi.spyOn(fs, 'access').mockResolvedValue();
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockWarnings);
|
||||
vi.spyOn(fs, 'unlink').mockResolvedValue();
|
||||
|
||||
const warnings = await getStartupWarnings();
|
||||
|
||||
expect(fs.access).toHaveBeenCalled();
|
||||
expect(fs.readFile).toHaveBeenCalled();
|
||||
expect(fs.unlink).toHaveBeenCalled();
|
||||
expect(warnings).toEqual(['Warning 1', 'Warning 2']);
|
||||
});
|
||||
|
||||
it('should return an empty array if the file does not exist', async () => {
|
||||
const error = new Error('File not found');
|
||||
(error as Error & { code: string }).code = 'ENOENT';
|
||||
vi.spyOn(fs, 'access').mockRejectedValue(error);
|
||||
|
||||
const warnings = await getStartupWarnings();
|
||||
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an error message if reading the file fails', async () => {
|
||||
const error = new Error('Permission denied');
|
||||
vi.spyOn(fs, 'access').mockRejectedValue(error);
|
||||
vi.mocked(getErrorMessage).mockReturnValue('Permission denied');
|
||||
|
||||
const warnings = await getStartupWarnings();
|
||||
|
||||
expect(warnings).toEqual([
|
||||
'Error checking/reading warnings file: Permission denied',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a warning if deleting the file fails', async () => {
|
||||
const mockWarnings = 'Warning 1';
|
||||
vi.spyOn(fs, 'access').mockResolvedValue();
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(mockWarnings);
|
||||
vi.spyOn(fs, 'unlink').mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
const warnings = await getStartupWarnings();
|
||||
|
||||
expect(warnings).toEqual([
|
||||
'Warning 1',
|
||||
'Warning: Could not delete temporary warnings file.',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { webLoginClient } from './oauth2.js';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import http from 'http';
|
||||
import open from 'open';
|
||||
import crypto from 'crypto';
|
||||
|
||||
vi.mock('google-auth-library');
|
||||
vi.mock('http');
|
||||
vi.mock('open');
|
||||
vi.mock('crypto');
|
||||
|
||||
describe('oauth2', () => {
|
||||
it('should perform a web login', async () => {
|
||||
const mockAuthUrl = 'https://example.com/auth';
|
||||
const mockCode = 'test-code';
|
||||
const mockState = 'test-state';
|
||||
const mockTokens = {
|
||||
access_token: 'test-access-token',
|
||||
refresh_token: 'test-refresh-token',
|
||||
};
|
||||
|
||||
const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl);
|
||||
const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens });
|
||||
const mockSetCredentials = vi.fn();
|
||||
const mockOAuth2Client = {
|
||||
generateAuthUrl: mockGenerateAuthUrl,
|
||||
getToken: mockGetToken,
|
||||
setCredentials: mockSetCredentials,
|
||||
} as unknown as OAuth2Client;
|
||||
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
|
||||
|
||||
vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
|
||||
vi.mocked(open).mockImplementation(async () => ({}) as never);
|
||||
|
||||
let requestCallback!: (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
) => void;
|
||||
const mockHttpServer = {
|
||||
listen: vi.fn((port: number, callback?: () => void) => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}),
|
||||
close: vi.fn((callback?: () => void) => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}),
|
||||
on: vi.fn(),
|
||||
address: () => ({ port: 1234 }),
|
||||
};
|
||||
vi.mocked(http.createServer).mockImplementation((cb) => {
|
||||
requestCallback = cb as (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
) => void;
|
||||
return mockHttpServer as unknown as http.Server;
|
||||
});
|
||||
|
||||
const clientPromise = webLoginClient();
|
||||
|
||||
// Wait for the server to be created
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const mockReq = {
|
||||
url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
|
||||
} as http.IncomingMessage;
|
||||
const mockRes = {
|
||||
writeHead: vi.fn(),
|
||||
end: vi.fn(),
|
||||
} as unknown as http.ServerResponse;
|
||||
|
||||
if (requestCallback) {
|
||||
await requestCallback(mockReq, mockRes);
|
||||
}
|
||||
|
||||
const client = await clientPromise;
|
||||
|
||||
expect(open).toHaveBeenCalledWith(mockAuthUrl);
|
||||
expect(mockGetToken).toHaveBeenCalledWith(mockCode);
|
||||
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
|
||||
expect(client).toBe(mockOAuth2Client);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { CodeAssistServer } from './server.js';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
|
||||
vi.mock('google-auth-library');
|
||||
|
||||
describe('CodeAssistServer', () => {
|
||||
it('should be able to be constructed', () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
expect(server).toBeInstanceOf(CodeAssistServer);
|
||||
});
|
||||
|
||||
it('should call the generateContent endpoint', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
const mockResponse = {
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
index: 0,
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: [{ text: 'response' }],
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
safetyRatings: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
vi.spyOn(server, 'callEndpoint').mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await server.generateContent({
|
||||
model: 'test-model',
|
||||
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
||||
});
|
||||
|
||||
expect(server.callEndpoint).toHaveBeenCalledWith(
|
||||
'generateContent',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(response.candidates?.[0]?.content?.parts?.[0]?.text).toBe(
|
||||
'response',
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the generateContentStream endpoint', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
const mockResponse = (async function* () {
|
||||
yield {
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
index: 0,
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: [{ text: 'response' }],
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
safetyRatings: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
})();
|
||||
vi.spyOn(server, 'streamEndpoint').mockResolvedValue(mockResponse);
|
||||
|
||||
const stream = await server.generateContentStream({
|
||||
model: 'test-model',
|
||||
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
||||
});
|
||||
|
||||
for await (const res of stream) {
|
||||
expect(server.streamEndpoint).toHaveBeenCalledWith(
|
||||
'streamGenerateContent',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(res.candidates?.[0]?.content?.parts?.[0]?.text).toBe('response');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call the onboardUser endpoint', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
const mockResponse = {
|
||||
name: 'operations/123',
|
||||
done: true,
|
||||
};
|
||||
vi.spyOn(server, 'callEndpoint').mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await server.onboardUser({
|
||||
tierId: 'test-tier',
|
||||
cloudaicompanionProject: 'test-project',
|
||||
metadata: {},
|
||||
});
|
||||
|
||||
expect(server.callEndpoint).toHaveBeenCalledWith(
|
||||
'onboardUser',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(response.name).toBe('operations/123');
|
||||
});
|
||||
|
||||
it('should call the loadCodeAssist endpoint', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
const mockResponse = {
|
||||
// TODO: Add mock response
|
||||
};
|
||||
vi.spyOn(server, 'callEndpoint').mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await server.loadCodeAssist({
|
||||
metadata: {},
|
||||
});
|
||||
|
||||
expect(server.callEndpoint).toHaveBeenCalledWith(
|
||||
'loadCodeAssist',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(response).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should return 0 for countTokens', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
const response = await server.countTokens({
|
||||
model: 'test-model',
|
||||
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
||||
});
|
||||
expect(response.totalTokens).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw an error for embedContent', async () => {
|
||||
const auth = new OAuth2Client();
|
||||
const server = new CodeAssistServer(auth, 'test-project');
|
||||
await expect(
|
||||
server.embedContent({
|
||||
model: 'test-model',
|
||||
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createContentGenerator } from './contentGenerator.js';
|
||||
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
|
||||
vi.mock('../code_assist/codeAssist.js');
|
||||
vi.mock('@google/genai');
|
||||
|
||||
describe('contentGenerator', () => {
|
||||
it('should create a CodeAssistContentGenerator', async () => {
|
||||
const mockGenerator = {} as unknown;
|
||||
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
|
||||
mockGenerator as never,
|
||||
);
|
||||
const generator = await createContentGenerator({
|
||||
model: 'test-model',
|
||||
codeAssist: true,
|
||||
});
|
||||
expect(createCodeAssistContentGenerator).toHaveBeenCalled();
|
||||
expect(generator).toBe(mockGenerator);
|
||||
});
|
||||
|
||||
it('should create a GoogleGenAI content generator', async () => {
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
} as unknown;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
const generator = await createContentGenerator({
|
||||
model: 'test-model',
|
||||
apiKey: 'test-api-key',
|
||||
});
|
||||
expect(GoogleGenAI).toHaveBeenCalledWith({
|
||||
apiKey: 'test-api-key',
|
||||
vertexai: undefined,
|
||||
httpOptions: {
|
||||
headers: {
|
||||
'User-Agent': expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(generator).toBe((mockGenerator as GoogleGenAI).models);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
initializeTelemetry,
|
||||
shutdownTelemetry,
|
||||
isTelemetrySdkInitialized,
|
||||
} from './sdk.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import * as loggers from './loggers.js';
|
||||
|
||||
vi.mock('@opentelemetry/sdk-node');
|
||||
vi.mock('../config/config.js');
|
||||
vi.mock('./loggers.js');
|
||||
|
||||
describe('telemetry', () => {
|
||||
let mockConfig: Config;
|
||||
let mockNodeSdk: NodeSDK;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockConfig = new Config({
|
||||
sessionId: 'test-session-id',
|
||||
contentGeneratorConfig: {
|
||||
model: 'test-model',
|
||||
},
|
||||
targetDir: '/test/dir',
|
||||
debugMode: false,
|
||||
cwd: '/test/dir',
|
||||
});
|
||||
vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
|
||||
'http://localhost:4317',
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getSessionId').mockReturnValue('test-session-id');
|
||||
vi.spyOn(loggers, 'logCliConfiguration').mockImplementation(() => {});
|
||||
|
||||
mockNodeSdk = {
|
||||
start: vi.fn(),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as NodeSDK;
|
||||
vi.mocked(NodeSDK).mockImplementation(() => mockNodeSdk);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Ensure we shut down telemetry even if a test fails.
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
await shutdownTelemetry();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize the telemetry service', () => {
|
||||
initializeTelemetry(mockConfig);
|
||||
|
||||
expect(NodeSDK).toHaveBeenCalled();
|
||||
expect(mockNodeSdk.start).toHaveBeenCalled();
|
||||
expect(loggers.logCliConfiguration).toHaveBeenCalledWith(mockConfig);
|
||||
});
|
||||
|
||||
it('should shutdown the telemetry service', async () => {
|
||||
initializeTelemetry(mockConfig);
|
||||
await shutdownTelemetry();
|
||||
|
||||
expect(mockNodeSdk.shutdown).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -19,6 +19,7 @@ import {
|
|||
openDiff,
|
||||
allowEditorTypeInSandbox,
|
||||
isEditorAvailable,
|
||||
type EditorType,
|
||||
} from './editor.js';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
|
||||
|
@ -27,225 +28,290 @@ vi.mock('child_process', () => ({
|
|||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('checkHasEditorType', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
describe('editor utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true for vscode if "code" command exists', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
||||
expect(checkHasEditorType('vscode')).toBe(true);
|
||||
const expectedCommand =
|
||||
process.platform === 'win32' ? 'where.exe code.cmd' : 'command -v code';
|
||||
expect(execSync).toHaveBeenCalledWith(expectedCommand, {
|
||||
stdio: 'ignore',
|
||||
delete process.env.SANDBOX;
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for vscode if "code" command does not exist', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for windsurf if "windsurf" command exists', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/windsurf'));
|
||||
expect(checkHasEditorType('windsurf')).toBe(true);
|
||||
expect(execSync).toHaveBeenCalledWith('command -v windsurf', {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for windsurf if "windsurf" command does not exist', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType('windsurf')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for cursor if "cursor" command exists', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/cursor'));
|
||||
expect(checkHasEditorType('cursor')).toBe(true);
|
||||
expect(execSync).toHaveBeenCalledWith('command -v cursor', {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for cursor if "cursor" command does not exist', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType('cursor')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vim if "vim" command exists', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim'));
|
||||
expect(checkHasEditorType('vim')).toBe(true);
|
||||
const expectedCommand =
|
||||
process.platform === 'win32' ? 'where.exe vim' : 'command -v vim';
|
||||
expect(execSync).toHaveBeenCalledWith(expectedCommand, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for vim if "vim" command does not exist', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType('vim')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiffCommand', () => {
|
||||
it('should return the correct command for vscode', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'vscode');
|
||||
expect(command).toEqual({
|
||||
command: 'code',
|
||||
args: ['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct command for vim', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'vim');
|
||||
expect(command?.command).toBe('vim');
|
||||
expect(command?.args).toContain('old.txt');
|
||||
expect(command?.args).toContain('new.txt');
|
||||
});
|
||||
|
||||
it('should return null for an unsupported editor', () => {
|
||||
// @ts-expect-error Testing unsupported editor
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'foobar');
|
||||
expect(command).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDiff', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call spawn for vscode', async () => {
|
||||
const mockSpawn = {
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb(0);
|
||||
}
|
||||
}),
|
||||
};
|
||||
(spawn as Mock).mockReturnValue(mockSpawn);
|
||||
await openDiff('old.txt', 'new.txt', 'vscode');
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'code',
|
||||
['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
expect(mockSpawn.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call execSync for vim', async () => {
|
||||
await openDiff('old.txt', 'new.txt', 'vim');
|
||||
expect(execSync).toHaveBeenCalled();
|
||||
const command = (execSync as Mock).mock.calls[0][0];
|
||||
expect(command).toContain('vim');
|
||||
expect(command).toContain('old.txt');
|
||||
expect(command).toContain('new.txt');
|
||||
});
|
||||
|
||||
it('should handle spawn error for vscode', async () => {
|
||||
const mockSpawn = {
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'error') {
|
||||
cb(new Error('spawn error'));
|
||||
}
|
||||
}),
|
||||
};
|
||||
(spawn as Mock).mockReturnValue(mockSpawn);
|
||||
await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow(
|
||||
'spawn error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowEditorTypeInSandbox', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete process.env.SANDBOX;
|
||||
});
|
||||
|
||||
it('should allow vim in sandbox mode', () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow vim when not in sandbox mode', () => {
|
||||
delete process.env.SANDBOX;
|
||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow vscode in sandbox mode', () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow vscode when not in sandbox mode', () => {
|
||||
delete process.env.SANDBOX;
|
||||
expect(allowEditorTypeInSandbox('vscode')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow windsurf in sandbox mode', () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox('windsurf')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow windsurf when not in sandbox mode', () => {
|
||||
delete process.env.SANDBOX;
|
||||
expect(allowEditorTypeInSandbox('windsurf')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow cursor in sandbox mode', () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox('cursor')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow cursor when not in sandbox mode', () => {
|
||||
delete process.env.SANDBOX;
|
||||
expect(allowEditorTypeInSandbox('cursor')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEditorAvailable', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.SANDBOX;
|
||||
});
|
||||
|
||||
it('should return false for undefined editor', () => {
|
||||
expect(isEditorAvailable(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string editor', () => {
|
||||
expect(isEditorAvailable('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid editor type', () => {
|
||||
expect(isEditorAvailable('invalid-editor')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vscode when installed and not in sandbox mode', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
||||
expect(isEditorAvailable('vscode')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for vscode when not installed and not in sandbox mode', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
});
|
||||
expect(isEditorAvailable('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for vscode when installed and in sandbox mode', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(isEditorAvailable('vscode')).toBe(false);
|
||||
describe('checkHasEditorType', () => {
|
||||
const testCases: Array<{
|
||||
editor: EditorType;
|
||||
command: string;
|
||||
win32Command: string;
|
||||
}> = [
|
||||
{ editor: 'vscode', command: 'code', win32Command: 'code.cmd' },
|
||||
{ editor: 'windsurf', command: 'windsurf', win32Command: 'windsurf' },
|
||||
{ editor: 'cursor', command: 'cursor', win32Command: 'cursor' },
|
||||
{ editor: 'vim', command: 'vim', win32Command: 'vim' },
|
||||
];
|
||||
|
||||
for (const { editor, command, win32Command } of testCases) {
|
||||
describe(`${editor}`, () => {
|
||||
it(`should return true if "${command}" command exists on non-windows`, () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
(execSync as Mock).mockReturnValue(
|
||||
Buffer.from(`/usr/bin/${command}`),
|
||||
);
|
||||
expect(checkHasEditorType(editor)).toBe(true);
|
||||
expect(execSync).toHaveBeenCalledWith(`command -v ${command}`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return false if "${command}" command does not exist on non-windows`, () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType(editor)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should return true if "${win32Command}" command exists on windows`, () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
(execSync as Mock).mockReturnValue(
|
||||
Buffer.from(`C:\\Program Files\\...\\${win32Command}`),
|
||||
);
|
||||
expect(checkHasEditorType(editor)).toBe(true);
|
||||
expect(execSync).toHaveBeenCalledWith(`where.exe ${win32Command}`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return false if "${win32Command}" command does not exist on windows`, () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(checkHasEditorType(editor)).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('getDiffCommand', () => {
|
||||
it('should return the correct command for vscode', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'vscode');
|
||||
expect(command).toEqual({
|
||||
command: 'code',
|
||||
args: ['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct command for windsurf', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'windsurf');
|
||||
expect(command).toEqual({
|
||||
command: 'windsurf',
|
||||
args: ['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct command for cursor', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'cursor');
|
||||
expect(command).toEqual({
|
||||
command: 'cursor',
|
||||
args: ['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct command for vim', () => {
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'vim');
|
||||
expect(command).toEqual({
|
||||
command: 'vim',
|
||||
args: [
|
||||
'-d',
|
||||
'-i',
|
||||
'NONE',
|
||||
'-c',
|
||||
'wincmd h | set readonly | wincmd l',
|
||||
'-c',
|
||||
'highlight DiffAdd cterm=bold ctermbg=22 guibg=#005f00 | highlight DiffChange cterm=bold ctermbg=24 guibg=#005f87 | highlight DiffText ctermbg=21 guibg=#0000af | highlight DiffDelete ctermbg=52 guibg=#5f0000',
|
||||
'-c',
|
||||
'set showtabline=2 | set tabline=[Instructions]\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
|
||||
'-c',
|
||||
'wincmd h | setlocal statusline=OLD\\ FILE',
|
||||
'-c',
|
||||
'wincmd l | setlocal statusline=%#StatusBold#NEW\\ FILE\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
|
||||
'-c',
|
||||
'autocmd WinClosed * wqa',
|
||||
'old.txt',
|
||||
'new.txt',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for an unsupported editor', () => {
|
||||
// @ts-expect-error Testing unsupported editor
|
||||
const command = getDiffCommand('old.txt', 'new.txt', 'foobar');
|
||||
expect(command).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDiff', () => {
|
||||
it('should call spawn for vscode', async () => {
|
||||
const mockSpawn = {
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb(0);
|
||||
}
|
||||
}),
|
||||
};
|
||||
(spawn as Mock).mockReturnValue(mockSpawn);
|
||||
await openDiff('old.txt', 'new.txt', 'vscode');
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'code',
|
||||
['--wait', '--diff', 'old.txt', 'new.txt'],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
expect(mockSpawn.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should reject if spawn for vscode fails', async () => {
|
||||
const mockError = new Error('spawn error');
|
||||
const mockSpawn = {
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'error') {
|
||||
cb(mockError);
|
||||
}
|
||||
}),
|
||||
};
|
||||
(spawn as Mock).mockReturnValue(mockSpawn);
|
||||
await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow(
|
||||
'spawn error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if vscode exits with non-zero code', async () => {
|
||||
const mockSpawn = {
|
||||
on: vi.fn((event, cb) => {
|
||||
if (event === 'close') {
|
||||
cb(1);
|
||||
}
|
||||
}),
|
||||
};
|
||||
(spawn as Mock).mockReturnValue(mockSpawn);
|
||||
await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow(
|
||||
'VS Code exited with code 1',
|
||||
);
|
||||
});
|
||||
|
||||
const execSyncEditors: EditorType[] = ['vim', 'windsurf', 'cursor'];
|
||||
for (const editor of execSyncEditors) {
|
||||
it(`should call execSync for ${editor} on non-windows`, async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
await openDiff('old.txt', 'new.txt', editor);
|
||||
expect(execSync).toHaveBeenCalledTimes(1);
|
||||
const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
|
||||
const expectedCommand = `${
|
||||
diffCommand.command
|
||||
} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`;
|
||||
expect(execSync).toHaveBeenCalledWith(expectedCommand, {
|
||||
stdio: 'inherit',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call execSync for ${editor} on windows`, async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
await openDiff('old.txt', 'new.txt', editor);
|
||||
expect(execSync).toHaveBeenCalledTimes(1);
|
||||
const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
|
||||
const expectedCommand = `${diffCommand.command} ${diffCommand.args.join(
|
||||
' ',
|
||||
)}`;
|
||||
expect(execSync).toHaveBeenCalledWith(expectedCommand, {
|
||||
stdio: 'inherit',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('should log an error if diff command is not available', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
// @ts-expect-error Testing unsupported editor
|
||||
await openDiff('old.txt', 'new.txt', 'foobar');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'No diff tool available. Install vim or vscode.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowEditorTypeInSandbox', () => {
|
||||
it('should allow vim in sandbox mode', () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow vim when not in sandbox mode', () => {
|
||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||
});
|
||||
|
||||
const guiEditors: EditorType[] = ['vscode', 'windsurf', 'cursor'];
|
||||
for (const editor of guiEditors) {
|
||||
it(`should not allow ${editor} in sandbox mode`, () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(allowEditorTypeInSandbox(editor)).toBe(false);
|
||||
});
|
||||
|
||||
it(`should allow ${editor} when not in sandbox mode`, () => {
|
||||
expect(allowEditorTypeInSandbox(editor)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('isEditorAvailable', () => {
|
||||
it('should return false for undefined editor', () => {
|
||||
expect(isEditorAvailable(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string editor', () => {
|
||||
expect(isEditorAvailable('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid editor type', () => {
|
||||
expect(isEditorAvailable('invalid-editor')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vscode when installed and not in sandbox mode', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
||||
expect(isEditorAvailable('vscode')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for vscode when not installed and not in sandbox mode', () => {
|
||||
(execSync as Mock).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
expect(isEditorAvailable('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for vscode when installed and in sandbox mode', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(isEditorAvailable('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vim when installed and in sandbox mode', () => {
|
||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim'));
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
expect(isEditorAvailable('vim')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue