CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
NativeLspService.integration.test.ts770 linesDownload Raw Back to lsp
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';8import { EventEmitter } from 'events';9import { NativeLspService } from './NativeLspService.js';10import type { Config as CoreConfig } from '../config/config.js';11import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';12import type { IdeContextStore } from '../ide/ideContext.js';13import type { WorkspaceContext } from '../utils/workspaceContext.js';14import type { LspDiagnostic, LspLocation } from './types.js';15 16/**17 * Mock LSP server responses for integration testing.18 * This simulates real LSP server behavior without requiring an actual server.19 */20const MOCK_LSP_RESPONSES = {21  initialize: {22    capabilities: {23      textDocumentSync: 1,24      completionProvider: {},25      hoverProvider: true,26      definitionProvider: true,27      referencesProvider: true,28      documentSymbolProvider: true,29      workspaceSymbolProvider: true,30      codeActionProvider: true,31      diagnosticProvider: {32        interFileDependencies: true,33        workspaceDiagnostics: true,34      },35    },36    serverInfo: {37      name: 'mock-lsp-server',38      version: '1.0.0',39    },40  },41  'textDocument/definition': [42    {43      uri: 'file:///test/workspace/src/types.ts',44      range: {45        start: { line: 10, character: 0 },46        end: { line: 10, character: 20 },47      },48    },49  ],50  'textDocument/references': [51    {52      uri: 'file:///test/workspace/src/app.ts',53      range: {54        start: { line: 5, character: 10 },55        end: { line: 5, character: 20 },56      },57    },58    {59      uri: 'file:///test/workspace/src/utils.ts',60      range: {61        start: { line: 15, character: 5 },62        end: { line: 15, character: 15 },63      },64    },65  ],66  'textDocument/hover': {67    contents: {68      kind: 'markdown',69      value:70        '```typescript\nfunction testFunc(): void\n```\n\nA test function.',71    },72    range: {73      start: { line: 10, character: 0 },74      end: { line: 10, character: 8 },75    },76  },77  'textDocument/documentSymbol': [78    {79      name: 'TestClass',80      kind: 5, // Class81      range: {82        start: { line: 0, character: 0 },83        end: { line: 20, character: 1 },84      },85      selectionRange: {86        start: { line: 0, character: 6 },87        end: { line: 0, character: 15 },88      },89      children: [90        {91          name: 'constructor',92          kind: 9, // Constructor93          range: {94            start: { line: 2, character: 2 },95            end: { line: 4, character: 3 },96          },97          selectionRange: {98            start: { line: 2, character: 2 },99            end: { line: 2, character: 13 },100          },101        },102      ],103    },104  ],105  'workspace/symbol': [106    {107      name: 'TestClass',108      kind: 5, // Class109      location: {110        uri: 'file:///test/workspace/src/test.ts',111        range: {112          start: { line: 0, character: 0 },113          end: { line: 20, character: 1 },114        },115      },116    },117    {118      name: 'testFunction',119      kind: 12, // Function120      location: {121        uri: 'file:///test/workspace/src/utils.ts',122        range: {123          start: { line: 5, character: 0 },124          end: { line: 10, character: 1 },125        },126      },127      containerName: 'utils',128    },129  ],130  'textDocument/implementation': [131    {132      uri: 'file:///test/workspace/src/impl.ts',133      range: {134        start: { line: 20, character: 0 },135        end: { line: 40, character: 1 },136      },137    },138  ],139  'textDocument/prepareCallHierarchy': [140    {141      name: 'testFunction',142      kind: 12, // Function143      detail: '(param: string) => void',144      uri: 'file:///test/workspace/src/utils.ts',145      range: {146        start: { line: 5, character: 0 },147        end: { line: 10, character: 1 },148      },149      selectionRange: {150        start: { line: 5, character: 9 },151        end: { line: 5, character: 21 },152      },153    },154  ],155  'callHierarchy/incomingCalls': [156    {157      from: {158        name: 'callerFunction',159        kind: 12,160        uri: 'file:///test/workspace/src/caller.ts',161        range: {162          start: { line: 10, character: 0 },163          end: { line: 15, character: 1 },164        },165        selectionRange: {166          start: { line: 10, character: 9 },167          end: { line: 10, character: 23 },168        },169      },170      fromRanges: [171        {172          start: { line: 12, character: 2 },173          end: { line: 12, character: 16 },174        },175      ],176    },177  ],178  'callHierarchy/outgoingCalls': [179    {180      to: {181        name: 'helperFunction',182        kind: 12,183        uri: 'file:///test/workspace/src/helper.ts',184        range: {185          start: { line: 0, character: 0 },186          end: { line: 5, character: 1 },187        },188        selectionRange: {189          start: { line: 0, character: 9 },190          end: { line: 0, character: 23 },191        },192      },193      fromRanges: [194        {195          start: { line: 7, character: 2 },196          end: { line: 7, character: 16 },197        },198      ],199    },200  ],201  'textDocument/diagnostic': {202    kind: 'full',203    items: [204      {205        range: {206          start: { line: 5, character: 0 },207          end: { line: 5, character: 10 },208        },209        severity: 1, // Error210        code: 'TS2304',211        source: 'typescript',212        message: "Cannot find name 'undeclaredVar'.",213      },214      {215        range: {216          start: { line: 10, character: 0 },217          end: { line: 10, character: 15 },218        },219        severity: 2, // Warning220        code: 'TS6133',221        source: 'typescript',222        message: "'unusedVar' is declared but its value is never read.",223        tags: [1], // Unnecessary224      },225    ],226  },227  'workspace/diagnostic': {228    items: [229      {230        kind: 'full',231        uri: 'file:///test/workspace/src/app.ts',232        items: [233          {234            range: {235              start: { line: 5, character: 0 },236              end: { line: 5, character: 10 },237            },238            severity: 1,239            code: 'TS2304',240            source: 'typescript',241            message: "Cannot find name 'undeclaredVar'.",242          },243        ],244      },245      {246        kind: 'full',247        uri: 'file:///test/workspace/src/utils.ts',248        items: [249          {250            range: {251              start: { line: 10, character: 0 },252              end: { line: 10, character: 15 },253            },254            severity: 2,255            code: 'TS6133',256            source: 'typescript',257            message: "'unusedVar' is declared but its value is never read.",258          },259        ],260      },261    ],262  },263  'textDocument/codeAction': [264    {265      title: "Add missing import 'React'",266      kind: 'quickfix',267      diagnostics: [268        {269          range: {270            start: { line: 0, character: 0 },271            end: { line: 0, character: 5 },272          },273          severity: 1,274          message: "Cannot find name 'React'.",275        },276      ],277      edit: {278        changes: {279          'file:///test/workspace/src/app.tsx': [280            {281              range: {282                start: { line: 0, character: 0 },283                end: { line: 0, character: 0 },284              },285              newText: "import React from 'react';\n",286            },287          ],288        },289      },290      isPreferred: true,291    },292    {293      title: 'Organize imports',294      kind: 'source.organizeImports',295      edit: {296        changes: {297          'file:///test/workspace/src/app.tsx': [298            {299              range: {300                start: { line: 0, character: 0 },301                end: { line: 5, character: 0 },302              },303              newText:304                "import { Component } from 'react';\nimport { helper } from './utils';\n",305            },306          ],307        },308      },309    },310  ],311};312 313/**314 * Mock configuration for testing.315 */316class MockConfig {317  rootPath = '/test/workspace';318  private trusted = true;319 320  isTrustedFolder(): boolean {321    return this.trusted;322  }323 324  setTrusted(trusted: boolean): void {325    this.trusted = trusted;326  }327 328  get(_key: string) {329    return undefined;330  }331 332  getProjectRoot(): string {333    return this.rootPath;334  }335}336 337/**338 * Mock workspace context for testing.339 */340class MockWorkspaceContext {341  rootPath = '/test/workspace';342 343  async fileExists(filePath: string): Promise<boolean> {344    return (345      filePath.endsWith('.json') ||346      filePath.includes('package.json') ||347      filePath.includes('.ts')348    );349  }350 351  async readFile(filePath: string): Promise<string> {352    if (filePath.includes('.lsp.json')) {353      return JSON.stringify({354        'mock-lsp': {355          languages: ['typescript', 'javascript'],356          command: 'mock-lsp-server',357          args: ['--stdio'],358          transport: 'stdio',359        },360      });361    }362    return '{}';363  }364 365  resolvePath(relativePath: string): string {366    return this.rootPath + '/' + relativePath;367  }368 369  isPathWithinWorkspace(_path: string): boolean {370    return true;371  }372 373  getDirectories(): string[] {374    return [this.rootPath];375  }376}377 378/**379 * Mock file discovery service for testing.380 */381class MockFileDiscoveryService {382  async discoverFiles(_root: string, _options: unknown): Promise<string[]> {383    return [384      '/test/workspace/src/index.ts',385      '/test/workspace/src/app.ts',386      '/test/workspace/src/utils.ts',387      '/test/workspace/src/types.ts',388    ];389  }390 391  shouldIgnoreFile(file: string): boolean {392    return file.includes('node_modules') || file.includes('.git');393  }394}395 396/**397 * Mock IDE context store for testing.398 */399class MockIdeContextStore {}400 401describe('NativeLspService Integration Tests', () => {402  let lspService: NativeLspService;403  let mockConfig: MockConfig;404  let mockWorkspace: MockWorkspaceContext;405  let mockFileDiscovery: MockFileDiscoveryService;406  let mockIdeStore: MockIdeContextStore;407  let eventEmitter: EventEmitter;408 409  beforeEach(() => {410    mockConfig = new MockConfig();411    mockWorkspace = new MockWorkspaceContext();412    mockFileDiscovery = new MockFileDiscoveryService();413    mockIdeStore = new MockIdeContextStore();414    eventEmitter = new EventEmitter();415 416    lspService = new NativeLspService(417      mockConfig as unknown as CoreConfig,418      mockWorkspace as unknown as WorkspaceContext,419      eventEmitter,420      mockFileDiscovery as unknown as FileDiscoveryService,421      mockIdeStore as unknown as IdeContextStore,422      {423        workspaceRoot: mockWorkspace.rootPath,424      },425    );426  });427 428  afterEach(() => {429    vi.clearAllMocks();430  });431 432  describe('Service Lifecycle', () => {433    it('should initialize service correctly', () => {434      expect(lspService).toBeDefined();435    });436 437    it('should discover and prepare without errors', async () => {438      await expect(lspService.discoverAndPrepare()).resolves.not.toThrow();439    });440 441    it('should return status after discovery', async () => {442      await lspService.discoverAndPrepare();443      const status = lspService.getStatus();444      expect(status).toBeDefined();445      expect(status instanceof Map).toBe(true);446    });447 448    it('should skip discovery for untrusted workspace', async () => {449      mockConfig.setTrusted(false);450      const untrustedService = new NativeLspService(451        mockConfig as unknown as CoreConfig,452        mockWorkspace as unknown as WorkspaceContext,453        eventEmitter,454        mockFileDiscovery as unknown as FileDiscoveryService,455        mockIdeStore as unknown as IdeContextStore,456        {457          workspaceRoot: mockWorkspace.rootPath,458          requireTrustedWorkspace: true,459        },460      );461 462      await untrustedService.discoverAndPrepare();463      const status = untrustedService.getStatus();464      expect(status.size).toBe(0);465    });466  });467 468  describe('Configuration Merging', () => {469    it('should detect TypeScript/JavaScript in workspace', async () => {470      await lspService.discoverAndPrepare();471      const status = lspService.getStatus();472 473      // Should have detected TypeScript based on mock file discovery474      // The exact server name depends on built-in presets475      expect(status.size).toBeGreaterThanOrEqual(0);476    });477  });478 479  describe('LSP Operations - Mock Responses', () => {480    // Note: These tests verify the structure of expected responses481    // In a real integration test, you would mock the connection or use a real server482 483    it('should format definition response correctly', () => {484      const response = MOCK_LSP_RESPONSES['textDocument/definition'];485      expect(response).toHaveLength(1);486      expect(response[0]).toHaveProperty('uri');487      expect(response[0]).toHaveProperty('range');488      expect(response[0].range.start).toHaveProperty('line');489      expect(response[0].range.start).toHaveProperty('character');490    });491 492    it('should format references response correctly', () => {493      const response = MOCK_LSP_RESPONSES['textDocument/references'];494      expect(response).toHaveLength(2);495      for (const ref of response) {496        expect(ref).toHaveProperty('uri');497        expect(ref).toHaveProperty('range');498      }499    });500 501    it('should format hover response correctly', () => {502      const response = MOCK_LSP_RESPONSES['textDocument/hover'];503      expect(response).toHaveProperty('contents');504      expect(response.contents).toHaveProperty('value');505      expect(response.contents.value).toContain('testFunc');506    });507 508    it('should format document symbols correctly', () => {509      const response = MOCK_LSP_RESPONSES['textDocument/documentSymbol'];510      expect(response).toHaveLength(1);511      expect(response[0].name).toBe('TestClass');512      expect(response[0].kind).toBe(5); // Class513      expect(response[0].children).toHaveLength(1);514    });515 516    it('should format workspace symbols correctly', () => {517      const response = MOCK_LSP_RESPONSES['workspace/symbol'];518      expect(response).toHaveLength(2);519      expect(response[0].name).toBe('TestClass');520      expect(response[1].name).toBe('testFunction');521      expect(response[1].containerName).toBe('utils');522    });523 524    it('should format call hierarchy items correctly', () => {525      const response = MOCK_LSP_RESPONSES['textDocument/prepareCallHierarchy'];526      expect(response).toHaveLength(1);527      expect(response[0].name).toBe('testFunction');528      expect(response[0]).toHaveProperty('detail');529      expect(response[0]).toHaveProperty('range');530      expect(response[0]).toHaveProperty('selectionRange');531    });532 533    it('should format incoming calls correctly', () => {534      const response = MOCK_LSP_RESPONSES['callHierarchy/incomingCalls'];535      expect(response).toHaveLength(1);536      expect(response[0].from.name).toBe('callerFunction');537      expect(response[0].fromRanges).toHaveLength(1);538    });539 540    it('should format outgoing calls correctly', () => {541      const response = MOCK_LSP_RESPONSES['callHierarchy/outgoingCalls'];542      expect(response).toHaveLength(1);543      expect(response[0].to.name).toBe('helperFunction');544      expect(response[0].fromRanges).toHaveLength(1);545    });546 547    it('should format diagnostics correctly', () => {548      const response = MOCK_LSP_RESPONSES['textDocument/diagnostic'];549      expect(response.items).toHaveLength(2);550      expect(response.items[0].severity).toBe(1); // Error551      expect(response.items[0].code).toBe('TS2304');552      expect(response.items[1].severity).toBe(2); // Warning553      expect(response.items[1].tags).toContain(1); // Unnecessary554    });555 556    it('should format workspace diagnostics correctly', () => {557      const response = MOCK_LSP_RESPONSES['workspace/diagnostic'];558      expect(response.items).toHaveLength(2);559      expect(response.items[0].uri).toContain('app.ts');560      expect(response.items[1].uri).toContain('utils.ts');561    });562 563    it('should format code actions correctly', () => {564      const response = MOCK_LSP_RESPONSES['textDocument/codeAction'];565      expect(response).toHaveLength(2);566 567      const quickfix = response[0];568      expect(quickfix.title).toContain('import');569      expect(quickfix.kind).toBe('quickfix');570      expect(quickfix.isPreferred).toBe(true);571      expect(quickfix.edit).toHaveProperty('changes');572 573      const organizeImports = response[1];574      expect(organizeImports.kind).toBe('source.organizeImports');575    });576  });577 578  describe('Diagnostic Normalization', () => {579    it('should normalize severity levels correctly', () => {580      const severityMap: Record<number, string> = {581        1: 'error',582        2: 'warning',583        3: 'information',584        4: 'hint',585      };586 587      for (const [num, label] of Object.entries(severityMap)) {588        expect(severityMap[Number(num)]).toBe(label);589      }590    });591 592    it('should normalize diagnostic tags correctly', () => {593      const tagMap: Record<number, string> = {594        1: 'unnecessary',595        2: 'deprecated',596      };597 598      expect(tagMap[1]).toBe('unnecessary');599      expect(tagMap[2]).toBe('deprecated');600    });601  });602 603  describe('Code Action Context', () => {604    it('should support filtering by code action kind', () => {605      const kinds = ['quickfix', 'refactor', 'source.organizeImports'];606      const filteredActions = MOCK_LSP_RESPONSES[607        'textDocument/codeAction'608      ].filter((action) => kinds.includes(action.kind));609      expect(filteredActions).toHaveLength(2);610    });611 612    it('should support quick fix actions with diagnostics', () => {613      const quickfix = MOCK_LSP_RESPONSES['textDocument/codeAction'][0];614      expect(quickfix.diagnostics).toBeDefined();615      expect(quickfix.diagnostics).toHaveLength(1);616      expect(quickfix.edit).toBeDefined();617    });618  });619 620  describe('Workspace Edit Application', () => {621    it('should structure workspace edits correctly', () => {622      const codeAction = MOCK_LSP_RESPONSES['textDocument/codeAction'][0];623      const edit = codeAction.edit;624 625      expect(edit).toHaveProperty('changes');626      expect(edit?.changes).toBeDefined();627 628      const changes = edit?.changes as Record<string, unknown[]>;629      const uri = Object.keys(changes ?? {})[0];630      expect(uri).toContain('app.tsx');631 632      const edits = changes?.[uri];633      expect(edits).toHaveLength(1);634      expect(edits?.[0]).toHaveProperty('range');635      expect(edits?.[0]).toHaveProperty('newText');636    });637  });638 639  describe('Error Handling', () => {640    it('should handle missing workspace gracefully', async () => {641      const emptyWorkspace = new MockWorkspaceContext();642      emptyWorkspace.getDirectories = () => [];643 644      const service = new NativeLspService(645        mockConfig as unknown as CoreConfig,646        emptyWorkspace as unknown as WorkspaceContext,647        eventEmitter,648        mockFileDiscovery as unknown as FileDiscoveryService,649        mockIdeStore as unknown as IdeContextStore,650      );651 652      await expect(service.discoverAndPrepare()).resolves.not.toThrow();653    });654 655    it('should return empty results when no server is ready', async () => {656      // Before starting any servers, operations should return empty657      const results = await lspService.workspaceSymbols('test');658      expect(results).toEqual([]);659    });660 661    it('should return empty diagnostics when no server is ready', async () => {662      const uri = 'file:///test/workspace/src/app.ts';663      const results = await lspService.diagnostics(uri);664      expect(results).toEqual([]);665    });666 667    it('should return empty code actions when no server is ready', async () => {668      const uri = 'file:///test/workspace/src/app.ts';669      const range = {670        start: { line: 0, character: 0 },671        end: { line: 0, character: 10 },672      };673      const context = {674        diagnostics: [],675        only: undefined,676        triggerKind: 'invoked' as const,677      };678 679      const results = await lspService.codeActions(uri, range, context);680      expect(results).toEqual([]);681    });682  });683 684  describe('Security Controls', () => {685    it('should respect trust requirements', async () => {686      mockConfig.setTrusted(false);687 688      const strictService = new NativeLspService(689        mockConfig as unknown as CoreConfig,690        mockWorkspace as unknown as WorkspaceContext,691        eventEmitter,692        mockFileDiscovery as unknown as FileDiscoveryService,693        mockIdeStore as unknown as IdeContextStore,694        {695          requireTrustedWorkspace: true,696        },697      );698 699      await strictService.discoverAndPrepare();700      const status = strictService.getStatus();701 702      // No servers should be discovered in untrusted workspace703      expect(status.size).toBe(0);704    });705 706    it('should allow operations in trusted workspace', async () => {707      mockConfig.setTrusted(true);708 709      await lspService.discoverAndPrepare();710      // Service should be ready to accept operations (even if no real server)711      expect(lspService).toBeDefined();712    });713  });714});715 716describe('LSP Response Type Validation', () => {717  describe('LspDiagnostic', () => {718    it('should have correct structure', () => {719      const diagnostic: LspDiagnostic = {720        range: {721          start: { line: 0, character: 0 },722          end: { line: 0, character: 10 },723        },724        severity: 'error',725        code: 'TS2304',726        source: 'typescript',727        message: 'Cannot find name.',728      };729 730      expect(diagnostic.range).toBeDefined();731      expect(diagnostic.severity).toBe('error');732      expect(diagnostic.code).toBe('TS2304');733      expect(diagnostic.source).toBe('typescript');734      expect(diagnostic.message).toBeDefined();735    });736 737    it('should support optional fields', () => {738      const minimalDiagnostic: LspDiagnostic = {739        range: {740          start: { line: 0, character: 0 },741          end: { line: 0, character: 10 },742        },743        message: 'Error message',744      };745 746      expect(minimalDiagnostic.severity).toBeUndefined();747      expect(minimalDiagnostic.code).toBeUndefined();748      expect(minimalDiagnostic.source).toBeUndefined();749    });750  });751 752  describe('LspLocation', () => {753    it('should have correct structure', () => {754      const location: LspLocation = {755        uri: 'file:///test/file.ts',756        range: {757          start: { line: 10, character: 5 },758          end: { line: 10, character: 15 },759        },760      };761 762      expect(location.uri).toBe('file:///test/file.ts');763      expect(location.range.start.line).toBe(10);764      expect(location.range.start.character).toBe(5);765      expect(location.range.end.line).toBe(10);766      expect(location.range.end.character).toBe(15);767    });768  });769});770 
basant307/AI_Governance_Project · CoolFace