diff options
Diffstat (limited to 'packages/vscode-ide-companion/src/ide-server.test.ts')
| -rw-r--r-- | packages/vscode-ide-companion/src/ide-server.test.ts | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/packages/vscode-ide-companion/src/ide-server.test.ts b/packages/vscode-ide-companion/src/ide-server.test.ts new file mode 100644 index 00000000..726259e4 --- /dev/null +++ b/packages/vscode-ide-companion/src/ide-server.test.ts @@ -0,0 +1,287 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as vscode from 'vscode'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { IDEServer } from './ide-server.js'; +import { DiffManager } from './diff-manager.js'; + +const mocks = vi.hoisted(() => ({ + diffManager: { + onDidChange: vi.fn(() => ({ dispose: vi.fn() })), + } as unknown as DiffManager, +})); + +vi.mock('node:fs/promises', () => ({ + writeFile: vi.fn(() => Promise.resolve(undefined)), + unlink: vi.fn(() => Promise.resolve(undefined)), +})); + +vi.mock('node:os', async (importOriginal) => { + const actual = await importOriginal<typeof os>(); + return { + ...actual, + tmpdir: vi.fn(() => '/tmp'), + }; +}); + +const vscodeMock = vi.hoisted(() => ({ + workspace: { + workspaceFolders: [ + { + uri: { + fsPath: '/test/workspace1', + }, + }, + { + uri: { + fsPath: '/test/workspace2', + }, + }, + ], + }, +})); + +vi.mock('vscode', () => vscodeMock); + +vi.mock('./open-files-manager', () => { + const OpenFilesManager = vi.fn(); + OpenFilesManager.prototype.onDidChange = vi.fn(() => ({ dispose: vi.fn() })); + return { OpenFilesManager }; +}); + +describe('IDEServer', () => { + let ideServer: IDEServer; + let mockContext: vscode.ExtensionContext; + let mockLog: (message: string) => void; + + const getPortFromMock = ( + replaceMock: ReturnType< + () => vscode.ExtensionContext['environmentVariableCollection']['replace'] + >, + ) => { + const port = vi + .mocked(replaceMock) + .mock.calls.find((call) => call[0] === 'GEMINI_CLI_IDE_SERVER_PORT')?.[1]; + + if (port === undefined) { + expect.fail('Port was not set'); + } + return port; + }; + + beforeEach(() => { + mockLog = vi.fn(); + ideServer = new IDEServer(mockLog, mocks.diffManager); + mockContext = { + subscriptions: [], + environmentVariableCollection: { + replace: vi.fn(), + clear: vi.fn(), + }, + } as unknown as vscode.ExtensionContext; + }); + + afterEach(async () => { + await ideServer.stop(); + vi.restoreAllMocks(); + vscodeMock.workspace.workspaceFolders = [ + { uri: { fsPath: '/test/workspace1' } }, + { uri: { fsPath: '/test/workspace2' } }, + ]; + }); + + it('should set environment variables and workspace path on start with multiple folders', async () => { + await ideServer.start(mockContext); + + const replaceMock = mockContext.environmentVariableCollection.replace; + expect(replaceMock).toHaveBeenCalledTimes(2); + + expect(replaceMock).toHaveBeenNthCalledWith( + 1, + 'GEMINI_CLI_IDE_SERVER_PORT', + expect.any(String), // port is a number as a string + ); + + const expectedWorkspacePaths = [ + '/test/workspace1', + '/test/workspace2', + ].join(path.delimiter); + + expect(replaceMock).toHaveBeenNthCalledWith( + 2, + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + expectedWorkspacePaths, + ); + + const port = getPortFromMock(replaceMock); + const expectedPortFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: expectedWorkspacePaths, + }), + ); + }); + + it('should set a single folder path', async () => { + vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/foo/bar' } }]; + + await ideServer.start(mockContext); + const replaceMock = mockContext.environmentVariableCollection.replace; + + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + '/foo/bar', + ); + + const port = getPortFromMock(replaceMock); + const expectedPortFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: '/foo/bar', + }), + ); + }); + + it('should set an empty string if no folders are open', async () => { + vscodeMock.workspace.workspaceFolders = []; + + await ideServer.start(mockContext); + const replaceMock = mockContext.environmentVariableCollection.replace; + + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + '', + ); + + const port = getPortFromMock(replaceMock); + const expectedPortFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: '', + }), + ); + }); + + it('should update the path when workspace folders change', async () => { + vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/foo/bar' } }]; + await ideServer.start(mockContext); + const replaceMock = mockContext.environmentVariableCollection.replace; + + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + '/foo/bar', + ); + + // Simulate adding a folder + vscodeMock.workspace.workspaceFolders = [ + { uri: { fsPath: '/foo/bar' } }, + { uri: { fsPath: '/baz/qux' } }, + ]; + await ideServer.updateWorkspacePath(); + + const expectedWorkspacePaths = ['/foo/bar', '/baz/qux'].join( + path.delimiter, + ); + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + expectedWorkspacePaths, + ); + + const port = getPortFromMock(replaceMock); + const expectedPortFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: expectedWorkspacePaths, + }), + ); + + // Simulate removing a folder + vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/baz/qux' } }]; + await ideServer.updateWorkspacePath(); + + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + '/baz/qux', + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: '/baz/qux', + }), + ); + }); + + it('should clear env vars and delete port file on stop', async () => { + await ideServer.start(mockContext); + const portFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith(portFile, expect.any(String)); + + await ideServer.stop(); + + expect(mockContext.environmentVariableCollection.clear).toHaveBeenCalled(); + expect(fs.unlink).toHaveBeenCalledWith(portFile); + }); + + it.skipIf(process.platform !== 'win32')( + 'should handle windows paths', + async () => { + vscodeMock.workspace.workspaceFolders = [ + { uri: { fsPath: 'c:\\foo\\bar' } }, + { uri: { fsPath: 'd:\\baz\\qux' } }, + ]; + + await ideServer.start(mockContext); + const replaceMock = mockContext.environmentVariableCollection.replace; + const expectedWorkspacePaths = 'c:\\foo\\bar;d:\\baz\\qux'; + + expect(replaceMock).toHaveBeenCalledWith( + 'GEMINI_CLI_IDE_WORKSPACE_PATH', + expectedWorkspacePaths, + ); + + const port = getPortFromMock(replaceMock); + const expectedPortFile = path.join( + '/tmp', + `gemini-ide-server-${process.ppid}.json`, + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedPortFile, + JSON.stringify({ + port: parseInt(port, 10), + workspacePath: expectedWorkspacePaths, + }), + ); + }, + ); +}); |
