Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/lockFile.spec.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';6import * as fs from 'fs/promises';7import * as os from 'os';8import * as path from 'path';9import { TestLogService } from '../../../../../platform/testing/common/testLogService';1011vi.mock('vscode', async (importOriginal) => {12const original = await importOriginal<typeof import('vscode')>();13return {14...original,15workspace: {16workspaceFolders: [{ uri: { fsPath: '/test/workspace' } }],17isTrusted: true,18},19env: {20appName: 'Visual Studio Code',21},22};23});2425import { LockFileHandle, createLockFile, isProcessRunning, cleanupStaleLockFiles } from '../lockFile';2627const logger = new TestLogService();2829describe('LockFileHandle', () => {30const testDir = path.join(os.tmpdir(), 'lockfile-test-' + Date.now());31const testLockFilePath = path.join(testDir, 'test.lock');32const mockServerUri = { path: '/tmp/test.sock', scheme: 'http' } as any;33const mockHeaders = { Authorization: 'Bearer test-token' };34const testTimestamp = Date.now();3536beforeEach(async () => {37await fs.mkdir(testDir, { recursive: true });38});3940afterEach(async () => {41await fs.rm(testDir, { recursive: true, force: true }).catch(() => { });42});4344describe('constructor and path getter', () => {45it('should store the lock file path', () => {46const handle = new LockFileHandle(testLockFilePath, mockServerUri, mockHeaders, testTimestamp, logger);47expect(handle.path).toBe(testLockFilePath);48});49});5051describe('update', () => {52it('should write lock file with correct content', async () => {53const handle = new LockFileHandle(testLockFilePath, mockServerUri, mockHeaders, testTimestamp, logger);54await handle.update();5556const stat = await fs.stat(testLockFilePath);57expect(stat.isFile()).toBe(true);5859const content = JSON.parse(await fs.readFile(testLockFilePath, 'utf-8'));60expect(content.socketPath).toBe('/tmp/test.sock');61expect(content.scheme).toBe('http');62expect(content.headers).toEqual(mockHeaders);63expect(content.pid).toBe(process.pid);64expect(content.timestamp).toBe(testTimestamp);65expect(content.isTrusted).toBe(true);66});6768it.skipIf(process.platform === 'win32')('should set restrictive file permissions (0o600)', async () => {69const handle = new LockFileHandle(testLockFilePath, mockServerUri, mockHeaders, testTimestamp, logger);70await handle.update();7172const stats = await fs.stat(testLockFilePath);73const mode = stats.mode & 0o777;74expect(mode).toBe(0o600);75});76});7778describe('remove', () => {79it('should delete the lock file if it exists', async () => {80await fs.writeFile(testLockFilePath, '{}');81const stat = await fs.stat(testLockFilePath);82expect(stat.isFile()).toBe(true);8384const handle = new LockFileHandle(testLockFilePath, mockServerUri, mockHeaders, testTimestamp, logger);85await handle.remove();8687await expect(fs.stat(testLockFilePath)).rejects.toThrow();88});8990it('should not throw if lock file does not exist', async () => {91const handle = new LockFileHandle(testLockFilePath, mockServerUri, mockHeaders, testTimestamp, logger);92await expect(handle.remove()).resolves.not.toThrow();93});94});95});9697describe('createLockFile', () => {98const testDir = path.join(os.tmpdir(), 'lockfile-create-test-' + Date.now());99let originalEnv: string | undefined;100let createdLockFile: string | null = null;101102beforeEach(() => {103originalEnv = process.env.XDG_STATE_HOME;104process.env.XDG_STATE_HOME = testDir;105});106107afterEach(async () => {108if (createdLockFile) {109await fs.unlink(createdLockFile).catch(() => { });110createdLockFile = null;111}112if (originalEnv !== undefined) {113process.env.XDG_STATE_HOME = originalEnv;114} else {115delete process.env.XDG_STATE_HOME;116}117await fs.rm(testDir, { recursive: true, force: true }).catch(() => { });118});119120it('should create lock file in .copilot directory', async () => {121const mockServerUri = { path: '/tmp/server.sock', scheme: 'http' } as any;122const mockHeaders = { 'X-Test': 'value' };123124const handle = await createLockFile(mockServerUri, mockHeaders, logger);125createdLockFile = handle.path;126127expect(handle.path).toMatch(/\.copilot[/\\]ide.*\.lock$/);128const stat = await fs.stat(handle.path);129expect(stat.isFile()).toBe(true);130131const content = JSON.parse(await fs.readFile(handle.path, 'utf-8'));132expect(content.socketPath).toBe('/tmp/server.sock');133expect(content.scheme).toBe('http');134expect(content.headers).toEqual(mockHeaders);135expect(content.pid).toBe(process.pid);136expect(typeof content.timestamp).toBe('number');137expect(content.isTrusted).toBe(true);138});139140it('should create .copilot directory if it does not exist', async () => {141const mockServerUri = { path: '/tmp/server.sock', scheme: 'http' } as any;142const handle = await createLockFile(mockServerUri, {}, logger);143createdLockFile = handle.path;144145const copilotDir = path.dirname(handle.path);146const stat = await fs.stat(copilotDir);147expect(stat.isDirectory()).toBe(true);148});149150it('should generate unique lock file names', async () => {151const mockServerUri = { path: '/tmp/server.sock', scheme: 'http' } as any;152153const handle1 = await createLockFile(mockServerUri, {}, logger);154const handle2 = await createLockFile(mockServerUri, {}, logger);155156expect(handle1.path).not.toBe(handle2.path);157158await handle1.remove();159await handle2.remove();160});161});162163describe('isProcessRunning', () => {164it('should return true for current process', () => {165expect(isProcessRunning(process.pid)).toBe(true);166});167168it('should return false for non-existent process', () => {169expect(isProcessRunning(999999999)).toBe(false);170});171});172173describe('cleanupStaleLockFiles', () => {174const testDir = path.join(os.tmpdir(), 'lockfile-cleanup-test-' + Date.now());175const copilotDir = path.join(testDir, '.copilot', 'ide');176let originalEnv: string | undefined;177178beforeEach(async () => {179originalEnv = process.env.XDG_STATE_HOME;180process.env.XDG_STATE_HOME = testDir;181await fs.mkdir(copilotDir, { recursive: true });182});183184afterEach(async () => {185if (originalEnv !== undefined) {186process.env.XDG_STATE_HOME = originalEnv;187} else {188delete process.env.XDG_STATE_HOME;189}190await fs.rm(testDir, { recursive: true, force: true }).catch(() => { });191});192193it('should remove lockfiles for non-running processes', async () => {194const staleLockFile = path.join(copilotDir, 'stale.lock');195const staleLockInfo = {196socketPath: '/tmp/test.sock',197scheme: 'http',198headers: {},199pid: 999999999,200ideName: 'Test',201timestamp: Date.now(),202workspaceFolders: [],203};204await fs.writeFile(staleLockFile, JSON.stringify(staleLockInfo));205206const stat = await fs.stat(staleLockFile);207expect(stat.isFile()).toBe(true);208const cleaned = await cleanupStaleLockFiles(logger);209expect(cleaned).toBe(1);210await expect(fs.stat(staleLockFile)).rejects.toThrow();211});212213it('should keep lockfiles for running processes', async () => {214const activeLockFile = path.join(copilotDir, 'active.lock');215const activeLockInfo = {216socketPath: '/tmp/test.sock',217scheme: 'http',218headers: {},219pid: process.pid,220ideName: 'Test',221timestamp: Date.now(),222workspaceFolders: [],223};224await fs.writeFile(activeLockFile, JSON.stringify(activeLockInfo));225226const stat = await fs.stat(activeLockFile);227expect(stat.isFile()).toBe(true);228const cleaned = await cleanupStaleLockFiles(logger);229expect(cleaned).toBe(0);230const stat2 = await fs.stat(activeLockFile);231expect(stat2.isFile()).toBe(true);232});233234it('should return 0 when copilot directory does not exist', async () => {235await fs.rm(copilotDir, { recursive: true, force: true });236237const cleaned = await cleanupStaleLockFiles(logger);238expect(cleaned).toBe(0);239});240});241242243