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,
|
...mockConfig,
|
||||||
getBugCommand: vi.fn(() => bugCommand),
|
getBugCommand: vi.fn(() => bugCommand),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
process.env.CLI_VERSION = '0.1.0';
|
||||||
|
|
||||||
const { handleSlashCommand } = getProcessor();
|
const { handleSlashCommand } = getProcessor();
|
||||||
const bugDescription = 'This is a custom bug';
|
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,
|
openDiff,
|
||||||
allowEditorTypeInSandbox,
|
allowEditorTypeInSandbox,
|
||||||
isEditorAvailable,
|
isEditorAvailable,
|
||||||
|
type EditorType,
|
||||||
} from './editor.js';
|
} from './editor.js';
|
||||||
import { execSync, spawn } from 'child_process';
|
import { execSync, spawn } from 'child_process';
|
||||||
|
|
||||||
|
@ -27,74 +28,80 @@ vi.mock('child_process', () => ({
|
||||||
spawn: vi.fn(),
|
spawn: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('checkHasEditorType', () => {
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
describe('editor utils', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
delete process.env.SANDBOX;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for vscode if "code" command exists', () => {
|
afterEach(() => {
|
||||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
|
vi.restoreAllMocks();
|
||||||
expect(checkHasEditorType('vscode')).toBe(true);
|
delete process.env.SANDBOX;
|
||||||
const expectedCommand =
|
Object.defineProperty(process, 'platform', {
|
||||||
process.platform === 'win32' ? 'where.exe code.cmd' : 'command -v code';
|
value: originalPlatform,
|
||||||
expect(execSync).toHaveBeenCalledWith(expectedCommand, {
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for vscode if "code" command does not exist', () => {
|
it(`should return false if "${command}" command does not exist on non-windows`, () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||||
(execSync as Mock).mockImplementation(() => {
|
(execSync as Mock).mockImplementation(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
});
|
});
|
||||||
expect(checkHasEditorType('vscode')).toBe(false);
|
expect(checkHasEditorType(editor)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for windsurf if "windsurf" command exists', () => {
|
it(`should return true if "${win32Command}" command exists on windows`, () => {
|
||||||
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/windsurf'));
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||||
expect(checkHasEditorType('windsurf')).toBe(true);
|
(execSync as Mock).mockReturnValue(
|
||||||
expect(execSync).toHaveBeenCalledWith('command -v windsurf', {
|
Buffer.from(`C:\\Program Files\\...\\${win32Command}`),
|
||||||
|
);
|
||||||
|
expect(checkHasEditorType(editor)).toBe(true);
|
||||||
|
expect(execSync).toHaveBeenCalledWith(`where.exe ${win32Command}`, {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for windsurf if "windsurf" command does not exist', () => {
|
it(`should return false if "${win32Command}" command does not exist on windows`, () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||||
(execSync as Mock).mockImplementation(() => {
|
(execSync as Mock).mockImplementation(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
});
|
});
|
||||||
expect(checkHasEditorType('windsurf')).toBe(false);
|
expect(checkHasEditorType(editor)).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', () => {
|
describe('getDiffCommand', () => {
|
||||||
|
@ -106,11 +113,46 @@ describe('getDiffCommand', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
it('should return the correct command for vim', () => {
|
||||||
const command = getDiffCommand('old.txt', 'new.txt', 'vim');
|
const command = getDiffCommand('old.txt', 'new.txt', 'vim');
|
||||||
expect(command?.command).toBe('vim');
|
expect(command).toEqual({
|
||||||
expect(command?.args).toContain('old.txt');
|
command: 'vim',
|
||||||
expect(command?.args).toContain('new.txt');
|
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', () => {
|
it('should return null for an unsupported editor', () => {
|
||||||
|
@ -121,10 +163,6 @@ describe('getDiffCommand', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('openDiff', () => {
|
describe('openDiff', () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call spawn for vscode', async () => {
|
it('should call spawn for vscode', async () => {
|
||||||
const mockSpawn = {
|
const mockSpawn = {
|
||||||
on: vi.fn((event, cb) => {
|
on: vi.fn((event, cb) => {
|
||||||
|
@ -144,20 +182,12 @@ describe('openDiff', () => {
|
||||||
expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function));
|
expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call execSync for vim', async () => {
|
it('should reject if spawn for vscode fails', async () => {
|
||||||
await openDiff('old.txt', 'new.txt', 'vim');
|
const mockError = new Error('spawn error');
|
||||||
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 = {
|
const mockSpawn = {
|
||||||
on: vi.fn((event, cb) => {
|
on: vi.fn((event, cb) => {
|
||||||
if (event === 'error') {
|
if (event === 'error') {
|
||||||
cb(new Error('spawn error'));
|
cb(mockError);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -166,59 +196,88 @@ describe('openDiff', () => {
|
||||||
'spawn error',
|
'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', () => {
|
describe('allowEditorTypeInSandbox', () => {
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.SANDBOX;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow vim in sandbox mode', () => {
|
it('should allow vim in sandbox mode', () => {
|
||||||
process.env.SANDBOX = 'sandbox';
|
process.env.SANDBOX = 'sandbox';
|
||||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow vim when not in sandbox mode', () => {
|
it('should allow vim when not in sandbox mode', () => {
|
||||||
delete process.env.SANDBOX;
|
|
||||||
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
expect(allowEditorTypeInSandbox('vim')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow vscode in sandbox mode', () => {
|
const guiEditors: EditorType[] = ['vscode', 'windsurf', 'cursor'];
|
||||||
|
for (const editor of guiEditors) {
|
||||||
|
it(`should not allow ${editor} in sandbox mode`, () => {
|
||||||
process.env.SANDBOX = 'sandbox';
|
process.env.SANDBOX = 'sandbox';
|
||||||
expect(allowEditorTypeInSandbox('vscode')).toBe(false);
|
expect(allowEditorTypeInSandbox(editor)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow vscode when not in sandbox mode', () => {
|
it(`should allow ${editor} when not in sandbox mode`, () => {
|
||||||
delete process.env.SANDBOX;
|
expect(allowEditorTypeInSandbox(editor)).toBe(true);
|
||||||
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', () => {
|
describe('isEditorAvailable', () => {
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.SANDBOX;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for undefined editor', () => {
|
it('should return false for undefined editor', () => {
|
||||||
expect(isEditorAvailable(undefined)).toBe(false);
|
expect(isEditorAvailable(undefined)).toBe(false);
|
||||||
});
|
});
|
||||||
|
@ -248,4 +307,11 @@ describe('isEditorAvailable', () => {
|
||||||
process.env.SANDBOX = 'sandbox';
|
process.env.SANDBOX = 'sandbox';
|
||||||
expect(isEditorAvailable('vscode')).toBe(false);
|
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