basant307/AI_Governance_Project
048
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 