CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
lsp.test.ts1278 linesDownload Raw Back to tools
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';8import path from 'node:path';9import { pathToFileURL } from 'node:url';10import type { Config } from '../config/config.js';11import type {12  LspCallHierarchyIncomingCall,13  LspCallHierarchyItem,14  LspCallHierarchyOutgoingCall,15  LspClient,16  LspDefinition,17  LspHoverResult,18  LspLocation,19  LspReference,20  LspSymbolInformation,21} from '../lsp/types.js';22import { LspTool, type LspToolParams, type LspOperation } from './lsp.js';23 24const abortSignal = new AbortController().signal;25const workspaceRoot = '/test/workspace';26 27/**28 * Helper to resolve a path relative to workspace root.29 */30const resolvePath = (...segments: string[]) =>31  path.join(workspaceRoot, ...segments);32 33/**34 * Helper to convert file path to URI.35 */36const toUri = (filePath: string) => pathToFileURL(filePath).toString();37 38/**39 * Helper to create a mock LspLocation.40 */41const createLocation = (42  filePath: string,43  line: number,44  character: number,45): LspLocation => ({46  uri: toUri(filePath),47  range: {48    start: { line, character },49    end: { line, character },50  },51});52 53/**54 * Create a mock LspClient with all methods mocked.55 */56const createMockClient = (): LspClient =>57  ({58    workspaceSymbols: vi.fn().mockResolvedValue([]),59    hover: vi.fn().mockResolvedValue(null),60    documentSymbols: vi.fn().mockResolvedValue([]),61    definitions: vi.fn().mockResolvedValue([]),62    implementations: vi.fn().mockResolvedValue([]),63    references: vi.fn().mockResolvedValue([]),64    prepareCallHierarchy: vi.fn().mockResolvedValue([]),65    incomingCalls: vi.fn().mockResolvedValue([]),66    outgoingCalls: vi.fn().mockResolvedValue([]),67  }) as unknown as LspClient;68 69/**70 * Create a mock Config for testing.71 */72const createMockConfig = (client?: LspClient, enabled = true): Config =>73  ({74    getLspClient: () => client,75    isLspEnabled: () => enabled,76    getProjectRoot: () => workspaceRoot,77  }) as unknown as Config;78 79/**80 * Create a LspTool with mock config.81 */82const createTool = (client?: LspClient, enabled = true) =>83  new LspTool(createMockConfig(client, enabled));84 85describe('LspTool', () => {86  describe('validateToolParams', () => {87    let tool: LspTool;88 89    beforeEach(() => {90      tool = createTool();91    });92 93    describe('location-based operations', () => {94      const locationOperations: LspOperation[] = [95        'goToDefinition',96        'findReferences',97        'hover',98        'goToImplementation',99        'prepareCallHierarchy',100      ];101 102      it.each(locationOperations)(103        'requires filePath for %s operation',104        (operation) => {105          const result = tool.validateToolParams({106            operation,107          } as LspToolParams);108          expect(result).toBe(`filePath is required for ${operation}.`);109        },110      );111 112      it.each(locationOperations)(113        'requires line for %s operation',114        (operation) => {115          const result = tool.validateToolParams({116            operation,117            filePath: 'src/app.ts',118          } as LspToolParams);119          expect(result).toBe(`line is required for ${operation}.`);120        },121      );122 123      it.each(locationOperations)(124        'passes validation with valid params for %s',125        (operation) => {126          const result = tool.validateToolParams({127            operation,128            filePath: 'src/app.ts',129            line: 10,130            character: 5,131          } as LspToolParams);132          expect(result).toBeNull();133        },134      );135    });136 137    describe('documentSymbol operation', () => {138      it('requires filePath for documentSymbol', () => {139        const result = tool.validateToolParams({140          operation: 'documentSymbol',141        } as LspToolParams);142        expect(result).toBe('filePath is required for documentSymbol.');143      });144 145      it('passes validation with filePath', () => {146        const result = tool.validateToolParams({147          operation: 'documentSymbol',148          filePath: 'src/app.ts',149        } as LspToolParams);150        expect(result).toBeNull();151      });152    });153 154    describe('workspaceSymbol operation', () => {155      it('requires query for workspaceSymbol', () => {156        const result = tool.validateToolParams({157          operation: 'workspaceSymbol',158        } as LspToolParams);159        expect(result).toBe('query is required for workspaceSymbol.');160      });161 162      it('rejects empty query', () => {163        const result = tool.validateToolParams({164          operation: 'workspaceSymbol',165          query: '   ',166        } as LspToolParams);167        expect(result).toBe('query is required for workspaceSymbol.');168      });169 170      it('passes validation with query', () => {171        const result = tool.validateToolParams({172          operation: 'workspaceSymbol',173          query: 'Widget',174        } as LspToolParams);175        expect(result).toBeNull();176      });177    });178 179    describe('call hierarchy operations', () => {180      it('requires callHierarchyItem for incomingCalls', () => {181        const result = tool.validateToolParams({182          operation: 'incomingCalls',183        } as LspToolParams);184        expect(result).toBe('callHierarchyItem is required for incomingCalls.');185      });186 187      it('requires callHierarchyItem for outgoingCalls', () => {188        const result = tool.validateToolParams({189          operation: 'outgoingCalls',190        } as LspToolParams);191        expect(result).toBe('callHierarchyItem is required for outgoingCalls.');192      });193 194      it('passes validation with callHierarchyItem', () => {195        const item: LspCallHierarchyItem = {196          name: 'testFunc',197          uri: 'file:///test.ts',198          range: {199            start: { line: 0, character: 0 },200            end: { line: 0, character: 10 },201          },202          selectionRange: {203            start: { line: 0, character: 0 },204            end: { line: 0, character: 10 },205          },206        };207        const result = tool.validateToolParams({208          operation: 'incomingCalls',209          callHierarchyItem: item,210        } as LspToolParams);211        expect(result).toBeNull();212      });213    });214 215    describe('numeric parameter validation', () => {216      it('rejects non-positive line', () => {217        const result = tool.validateToolParams({218          operation: 'goToDefinition',219          filePath: 'src/app.ts',220          line: 0,221        } as LspToolParams);222        expect(result).toBe('line must be a positive number.');223      });224 225      it('rejects negative line', () => {226        const result = tool.validateToolParams({227          operation: 'goToDefinition',228          filePath: 'src/app.ts',229          line: -1,230        } as LspToolParams);231        expect(result).toBe('line must be a positive number.');232      });233 234      it('rejects non-positive character', () => {235        const result = tool.validateToolParams({236          operation: 'goToDefinition',237          filePath: 'src/app.ts',238          line: 1,239          character: 0,240        } as LspToolParams);241        expect(result).toBe('character must be a positive number.');242      });243 244      it('rejects non-positive limit', () => {245        const result = tool.validateToolParams({246          operation: 'documentSymbol',247          filePath: 'src/app.ts',248          limit: 0,249        } as LspToolParams);250        expect(result).toBe('params/limit must be >= 1');251      });252 253      it('rejects negative integer limit', () => {254        const result = tool.validateToolParams({255          operation: 'documentSymbol',256          filePath: 'src/app.ts',257          limit: -1,258        } as LspToolParams);259        expect(result).toBe('params/limit must be >= 1');260      });261 262      it('rejects fractional limit', () => {263        const result = tool.validateToolParams({264          operation: 'documentSymbol',265          filePath: 'src/app.ts',266          limit: 1.5,267        } as LspToolParams);268        expect(result).toBe('params/limit must be integer');269      });270    });271 272    describe('edge case validation', () => {273      it('rejects empty filePath', () => {274        const result = tool.validateToolParams({275          operation: 'goToDefinition',276          filePath: '',277          line: 1,278        } as LspToolParams);279        expect(result).toBe('filePath is required for goToDefinition.');280      });281 282      it('rejects whitespace-only filePath', () => {283        const result = tool.validateToolParams({284          operation: 'goToDefinition',285          filePath: '   ',286          line: 1,287        } as LspToolParams);288        expect(result).toBe('filePath is required for goToDefinition.');289      });290 291      it('rejects whitespace-only query', () => {292        const result = tool.validateToolParams({293          operation: 'workspaceSymbol',294          query: '  \t\n  ',295        } as LspToolParams);296        expect(result).toBe('query is required for workspaceSymbol.');297      });298 299      it.skipIf(process.platform === 'win32')(300        'should unescape shell-escaped filePath',301        () => {302          const params: LspToolParams = {303            operation: 'goToDefinition',304            filePath: 'src/app\\ file.ts',305            line: 10,306            character: 5,307          };308          const result = tool.validateToolParams(params);309          expect(result).toBeNull();310          expect(params.filePath).toBe('src/app file.ts');311        },312      );313    });314  });315 316  describe('execute', () => {317    describe('LSP disabled or unavailable', () => {318      it('returns unavailable message when LSP is disabled', async () => {319        const tool = createTool(undefined, false);320        const invocation = tool.build({321          operation: 'hover',322          filePath: 'src/app.ts',323          line: 1,324          character: 1,325        });326        const result = await invocation.execute(abortSignal);327        expect(result.llmContent).toContain('LSP hover is unavailable');328        expect(result.llmContent).toContain('LSP disabled or not initialized');329      });330 331      it('returns unavailable message when no LSP client', async () => {332        const tool = createTool(undefined, true);333        const invocation = tool.build({334          operation: 'goToDefinition',335          filePath: 'src/app.ts',336          line: 1,337          character: 1,338        });339        const result = await invocation.execute(abortSignal);340        // Note: operation labels are formatted (e.g., "go-to-definition")341        expect(result.llmContent).toContain(342          'LSP go-to-definition is unavailable',343        );344      });345    });346 347    describe('goToDefinition operation', () => {348      it('dispatches to definitions and formats results', async () => {349        const client = createMockClient();350        const tool = createTool(client);351        const filePath = resolvePath('src', 'app.ts');352        const definition: LspDefinition = {353          ...createLocation(filePath, 10, 5),354          serverName: 'tsserver',355        };356        (client.definitions as Mock).mockResolvedValue([definition]);357 358        const invocation = tool.build({359          operation: 'goToDefinition',360          filePath: 'src/app.ts',361          line: 5,362          character: 10,363        });364        const result = await invocation.execute(abortSignal);365 366        expect(client.definitions).toHaveBeenCalledWith(367          expect.objectContaining({368            uri: toUri(filePath),369            range: expect.objectContaining({370              start: { line: 4, character: 9 }, // 1-based to 0-based conversion371            }),372          }),373          undefined,374          20,375        );376        expect(result.llmContent).toContain('Definitions for');377        expect(result.llmContent).toContain('1.');378      });379 380      it('handles empty results', async () => {381        const client = createMockClient();382        const tool = createTool(client);383        (client.definitions as Mock).mockResolvedValue([]);384 385        const invocation = tool.build({386          operation: 'goToDefinition',387          filePath: 'src/app.ts',388          line: 5,389          character: 10,390        });391        const result = await invocation.execute(abortSignal);392 393        expect(result.llmContent).toContain('No definitions found');394      });395    });396 397    describe('findReferences operation', () => {398      it('dispatches to references and formats results', async () => {399        const client = createMockClient();400        const tool = createTool(client);401        const filePath = resolvePath('src', 'app.ts');402        const refs: LspReference[] = [403          { ...createLocation(filePath, 10, 5), serverName: 'tsserver' },404          { ...createLocation(filePath, 20, 8) },405        ];406        (client.references as Mock).mockResolvedValue(refs);407 408        const invocation = tool.build({409          operation: 'findReferences',410          filePath: 'src/app.ts',411          line: 5,412          character: 10,413          includeDeclaration: true,414        });415        const result = await invocation.execute(abortSignal);416 417        // Default limit for references is 50418        expect(client.references).toHaveBeenCalledWith(419          expect.objectContaining({ uri: toUri(filePath) }),420          undefined,421          true,422          50,423        );424        expect(result.llmContent).toContain('References for');425        expect(result.llmContent).toContain('1.');426        expect(result.llmContent).toContain('2.');427      });428    });429 430    describe('hover operation', () => {431      it('dispatches to hover and formats results', async () => {432        const client = createMockClient();433        const tool = createTool(client);434        const hoverResult: LspHoverResult = {435          contents: '**Type**: string\n\nA sample variable.',436        };437        (client.hover as Mock).mockResolvedValue(hoverResult);438 439        const invocation = tool.build({440          operation: 'hover',441          filePath: 'src/app.ts',442          line: 10,443          character: 5,444        });445        const result = await invocation.execute(abortSignal);446 447        expect(client.hover).toHaveBeenCalled();448        expect(result.llmContent).toContain('Hover for');449        expect(result.llmContent).toContain('Type');450      });451 452      it('handles null hover result', async () => {453        const client = createMockClient();454        const tool = createTool(client);455        (client.hover as Mock).mockResolvedValue(null);456 457        const invocation = tool.build({458          operation: 'hover',459          filePath: 'src/app.ts',460          line: 10,461          character: 5,462        });463        const result = await invocation.execute(abortSignal);464 465        expect(result.llmContent).toContain('No hover information found');466      });467    });468 469    describe('documentSymbol operation', () => {470      it('dispatches to documentSymbols and formats results', async () => {471        const client = createMockClient();472        const tool = createTool(client);473        const filePath = resolvePath('src', 'app.ts');474        const symbols: LspSymbolInformation[] = [475          {476            name: 'MyClass',477            kind: 'Class',478            containerName: 'app',479            location: createLocation(filePath, 5, 0),480            serverName: 'tsserver',481          },482          {483            name: 'myFunction',484            kind: 'Function',485            location: createLocation(filePath, 20, 0),486          },487        ];488        (client.documentSymbols as Mock).mockResolvedValue(symbols);489 490        const invocation = tool.build({491          operation: 'documentSymbol',492          filePath: 'src/app.ts',493        });494        const result = await invocation.execute(abortSignal);495 496        // Default limit for documentSymbols is 50497        expect(client.documentSymbols).toHaveBeenCalledWith(498          toUri(filePath),499          undefined,500          50,501        );502        expect(result.llmContent).toContain('Document symbols for');503        expect(result.llmContent).toContain('MyClass');504        expect(result.llmContent).toContain('myFunction');505      });506    });507 508    describe('workspaceSymbol operation', () => {509      it('dispatches to workspaceSymbols and formats results', async () => {510        const client = createMockClient();511        const tool = createTool(client);512        const filePath = resolvePath('src', 'app.ts');513        const symbols: LspSymbolInformation[] = [514          {515            name: 'Widget',516            kind: 'Class',517            location: createLocation(filePath, 10, 0),518          },519        ];520        (client.workspaceSymbols as Mock).mockResolvedValue(symbols);521        (client.references as Mock).mockResolvedValue([]);522 523        const invocation = tool.build({524          operation: 'workspaceSymbol',525          query: 'Widget',526          limit: 10,527        });528        const result = await invocation.execute(abortSignal);529 530        expect(client.workspaceSymbols).toHaveBeenCalledWith('Widget', 10);531        expect(result.llmContent).toContain('symbols for query "Widget"');532        expect(result.llmContent).toContain('Widget');533      });534    });535 536    describe('goToImplementation operation', () => {537      it('dispatches to implementations and formats results', async () => {538        const client = createMockClient();539        const tool = createTool(client);540        const filePath = resolvePath('src', 'impl.ts');541        const impl: LspDefinition = {542          ...createLocation(filePath, 15, 2),543          serverName: 'tsserver',544        };545        (client.implementations as Mock).mockResolvedValue([impl]);546 547        const invocation = tool.build({548          operation: 'goToImplementation',549          filePath: 'src/interface.ts',550          line: 5,551          character: 10,552        });553        const result = await invocation.execute(abortSignal);554 555        expect(client.implementations).toHaveBeenCalled();556        expect(result.llmContent).toContain('Implementations for');557      });558    });559 560    describe('prepareCallHierarchy operation', () => {561      it('dispatches to prepareCallHierarchy and formats results with JSON', async () => {562        const client = createMockClient();563        const tool = createTool(client);564        const filePath = resolvePath('src', 'app.ts');565        const item: LspCallHierarchyItem = {566          name: 'myFunction',567          kind: 'Function',568          detail: '(param: string)',569          uri: toUri(filePath),570          range: {571            start: { line: 10, character: 0 },572            end: { line: 20, character: 1 },573          },574          selectionRange: {575            start: { line: 10, character: 9 },576            end: { line: 10, character: 19 },577          },578          serverName: 'tsserver',579        };580        (client.prepareCallHierarchy as Mock).mockResolvedValue([item]);581 582        const invocation = tool.build({583          operation: 'prepareCallHierarchy',584          filePath: 'src/app.ts',585          line: 11,586          character: 15,587        });588        const result = await invocation.execute(abortSignal);589 590        expect(client.prepareCallHierarchy).toHaveBeenCalled();591        expect(result.llmContent).toContain('Call hierarchy items for');592        expect(result.llmContent).toContain('myFunction');593        expect(result.llmContent).toContain('Call hierarchy items (JSON):');594        expect(result.llmContent).toContain('"name": "myFunction"');595      });596    });597 598    describe('incomingCalls operation', () => {599      it('dispatches to incomingCalls and formats results', async () => {600        const client = createMockClient();601        const tool = createTool(client);602        const targetPath = resolvePath('src', 'target.ts');603        const callerPath = resolvePath('src', 'caller.ts');604 605        const targetItem: LspCallHierarchyItem = {606          name: 'targetFunc',607          uri: toUri(targetPath),608          range: {609            start: { line: 5, character: 0 },610            end: { line: 10, character: 1 },611          },612          selectionRange: {613            start: { line: 5, character: 9 },614            end: { line: 5, character: 19 },615          },616          serverName: 'tsserver',617        };618 619        const callerItem: LspCallHierarchyItem = {620          name: 'callerFunc',621          kind: 'Function',622          uri: toUri(callerPath),623          range: {624            start: { line: 20, character: 0 },625            end: { line: 30, character: 1 },626          },627          selectionRange: {628            start: { line: 20, character: 9 },629            end: { line: 20, character: 19 },630          },631        };632 633        const incomingCall: LspCallHierarchyIncomingCall = {634          from: callerItem,635          fromRanges: [636            {637              start: { line: 25, character: 4 },638              end: { line: 25, character: 14 },639            },640          ],641        };642        (client.incomingCalls as Mock).mockResolvedValue([incomingCall]);643 644        const invocation = tool.build({645          operation: 'incomingCalls',646          callHierarchyItem: targetItem,647        });648        const result = await invocation.execute(abortSignal);649 650        expect(client.incomingCalls).toHaveBeenCalledWith(651          targetItem,652          'tsserver',653          20,654        );655        expect(result.llmContent).toContain('Incoming calls for targetFunc');656        expect(result.llmContent).toContain('callerFunc');657        expect(result.llmContent).toContain('Incoming calls (JSON):');658      });659    });660 661    describe('outgoingCalls operation', () => {662      it('dispatches to outgoingCalls and formats results', async () => {663        const client = createMockClient();664        const tool = createTool(client);665        const sourcePath = resolvePath('src', 'source.ts');666        const targetPath = resolvePath('src', 'target.ts');667 668        const sourceItem: LspCallHierarchyItem = {669          name: 'sourceFunc',670          uri: toUri(sourcePath),671          range: {672            start: { line: 5, character: 0 },673            end: { line: 15, character: 1 },674          },675          selectionRange: {676            start: { line: 5, character: 9 },677            end: { line: 5, character: 19 },678          },679        };680 681        const targetItem: LspCallHierarchyItem = {682          name: 'targetFunc',683          kind: 'Function',684          uri: toUri(targetPath),685          range: {686            start: { line: 20, character: 0 },687            end: { line: 30, character: 1 },688          },689          selectionRange: {690            start: { line: 20, character: 9 },691            end: { line: 20, character: 19 },692          },693          serverName: 'tsserver',694        };695 696        const outgoingCall: LspCallHierarchyOutgoingCall = {697          to: targetItem,698          fromRanges: [699            {700              start: { line: 10, character: 4 },701              end: { line: 10, character: 14 },702            },703          ],704        };705        (client.outgoingCalls as Mock).mockResolvedValue([outgoingCall]);706 707        const invocation = tool.build({708          operation: 'outgoingCalls',709          callHierarchyItem: sourceItem,710        });711        const result = await invocation.execute(abortSignal);712 713        expect(client.outgoingCalls).toHaveBeenCalled();714        expect(result.llmContent).toContain('Outgoing calls for sourceFunc');715        expect(result.llmContent).toContain('targetFunc');716        expect(result.llmContent).toContain('Outgoing calls (JSON):');717      });718    });719 720    describe('error handling', () => {721      it('handles LSP client errors gracefully', async () => {722        const client = createMockClient();723        const tool = createTool(client);724        (client.definitions as Mock).mockRejectedValue(725          new Error('Connection refused'),726        );727 728        const invocation = tool.build({729          operation: 'goToDefinition',730          filePath: 'src/app.ts',731          line: 5,732          character: 10,733        });734        const result = await invocation.execute(abortSignal);735 736        expect(result.llmContent).toContain('failed');737        expect(result.llmContent).toContain('Connection refused');738      });739 740      it('handles hover operation errors', async () => {741        const client = createMockClient();742        const tool = createTool(client);743        (client.hover as Mock).mockRejectedValue(new Error('Server timeout'));744 745        const invocation = tool.build({746          operation: 'hover',747          filePath: 'src/app.ts',748          line: 5,749          character: 10,750        });751        const result = await invocation.execute(abortSignal);752 753        expect(result.llmContent).toContain('failed');754        expect(result.llmContent).toContain('Server timeout');755      });756 757      it('handles call hierarchy errors', async () => {758        const client = createMockClient();759        const tool = createTool(client);760        (client.prepareCallHierarchy as Mock).mockRejectedValue(761          new Error('Not supported'),762        );763 764        const invocation = tool.build({765          operation: 'prepareCallHierarchy',766          filePath: 'src/app.ts',767          line: 5,768          character: 10,769        });770        const result = await invocation.execute(abortSignal);771 772        expect(result.llmContent).toContain('failed');773        expect(result.llmContent).toContain('Not supported');774      });775    });776 777    describe('workspaceSymbol with references', () => {778      it('fetches references for top match when available', async () => {779        const client = createMockClient();780        const tool = createTool(client);781        const filePath = resolvePath('src', 'app.ts');782        const refPath = resolvePath('src', 'other.ts');783        const symbols: LspSymbolInformation[] = [784          {785            name: 'TopWidget',786            kind: 'Class',787            location: createLocation(filePath, 10, 0),788            serverName: 'tsserver',789          },790        ];791        const references: LspReference[] = [792          { ...createLocation(refPath, 5, 10), serverName: 'tsserver' },793          { ...createLocation(refPath, 20, 5) },794        ];795        (client.workspaceSymbols as Mock).mockResolvedValue(symbols);796        (client.references as Mock).mockResolvedValue(references);797 798        const invocation = tool.build({799          operation: 'workspaceSymbol',800          query: 'TopWidget',801        });802        const result = await invocation.execute(abortSignal);803 804        // Should fetch references for top match805        expect(client.references).toHaveBeenCalledWith(806          symbols[0].location,807          'tsserver',808          false,809          expect.any(Number),810        );811        expect(result.llmContent).toContain('References for top match');812        expect(result.llmContent).toContain('TopWidget');813      });814 815      it('handles reference lookup failure gracefully', async () => {816        const client = createMockClient();817        const tool = createTool(client);818        const filePath = resolvePath('src', 'app.ts');819        const symbols: LspSymbolInformation[] = [820          {821            name: 'Widget',822            kind: 'Class',823            location: createLocation(filePath, 10, 0),824          },825        ];826        (client.workspaceSymbols as Mock).mockResolvedValue(symbols);827        (client.references as Mock).mockRejectedValue(828          new Error('References not supported'),829        );830 831        const invocation = tool.build({832          operation: 'workspaceSymbol',833          query: 'Widget',834        });835        const result = await invocation.execute(abortSignal);836 837        // Should still return symbols even if references fail838        expect(result.llmContent).toContain('Widget');839        expect(result.llmContent).toContain('References lookup failed');840      });841    });842 843    describe('returnDisplay verification', () => {844      it('returns formatted display for definitions', async () => {845        const client = createMockClient();846        const tool = createTool(client);847        const filePath = resolvePath('src', 'app.ts');848        const definition: LspDefinition = {849          ...createLocation(filePath, 10, 5),850          serverName: 'tsserver',851        };852        (client.definitions as Mock).mockResolvedValue([definition]);853 854        const invocation = tool.build({855          operation: 'goToDefinition',856          filePath: 'src/app.ts',857          line: 5,858          character: 10,859        });860        const result = await invocation.execute(abortSignal);861 862        // returnDisplay should be concise (without heading)863        expect(result.returnDisplay).toBeDefined();864        expect(result.returnDisplay).toContain('1.');865        expect(result.returnDisplay).toContain('[tsserver]');866      });867 868      it('returns formatted display for hover with trimmed content', async () => {869        const client = createMockClient();870        const tool = createTool(client);871        const hoverResult: LspHoverResult = {872          contents: '  \n  Type: string  \n  ',873        };874        (client.hover as Mock).mockResolvedValue(hoverResult);875 876        const invocation = tool.build({877          operation: 'hover',878          filePath: 'src/app.ts',879          line: 10,880          character: 5,881        });882        const result = await invocation.execute(abortSignal);883 884        // returnDisplay should be trimmed885        expect(result.returnDisplay).toBe('Type: string');886      });887    });888 889    describe('serverName and limit parameter passing', () => {890      it('passes serverName to client methods', async () => {891        const client = createMockClient();892        const tool = createTool(client);893        (client.definitions as Mock).mockResolvedValue([]);894 895        const invocation = tool.build({896          operation: 'goToDefinition',897          filePath: 'src/app.ts',898          line: 5,899          character: 10,900          serverName: 'pylsp',901        });902        await invocation.execute(abortSignal);903 904        expect(client.definitions).toHaveBeenCalledWith(905          expect.anything(),906          'pylsp',907          expect.any(Number),908        );909      });910 911      it('passes custom limit to client methods', async () => {912        const client = createMockClient();913        const tool = createTool(client);914        (client.definitions as Mock).mockResolvedValue([]);915 916        const invocation = tool.build({917          operation: 'goToDefinition',918          filePath: 'src/app.ts',919          line: 5,920          character: 10,921          limit: 5,922        });923        await invocation.execute(abortSignal);924 925        expect(client.definitions).toHaveBeenCalledWith(926          expect.anything(),927          undefined,928          5,929        );930      });931    });932  });933 934  describe('schema compatibility with Claude Code', () => {935    /**936     * Claude Code LSP tool schema reference:937     * {938     *   "name": "lsp",939     *   "input_schema": {940     *     "type": "object",941     *     "properties": {942     *       "operation": { "type": "string", "enum": [...] },943     *       "filePath": { "type": "string" },944     *       "line": { "type": "number" },945     *       "character": { "type": "number" },946     *       "includeDeclaration": { "type": "boolean" },947     *       "query": { "type": "string" },948     *       "callHierarchyItem": { ... }949     *     },950     *     "required": ["operation"]951     *   }952     * }953     */954 955    it('has correct tool name', () => {956      const tool = createTool();957      expect(tool.schema.name).toBe('lsp');958    });959 960    it('has operation as only required field', () => {961      const tool = createTool();962      const schema = tool.schema.parametersJsonSchema as {963        required?: string[];964      };965      expect(schema.required).toEqual(['operation']);966    });967 968    it('operation enum matches Claude Code exactly', () => {969      const tool = createTool();970      const schema = tool.schema.parametersJsonSchema as {971        properties?: {972          operation?: {973            enum?: string[];974          };975        };976      };977      const expectedOperations = [978        'goToDefinition',979        'findReferences',980        'hover',981        'documentSymbol',982        'workspaceSymbol',983        'goToImplementation',984        'prepareCallHierarchy',985        'incomingCalls',986        'outgoingCalls',987        'diagnostics',988        'workspaceDiagnostics',989        'codeActions',990      ];991      expect(schema.properties?.operation?.enum).toEqual(expectedOperations);992    });993 994    it('has all Claude Code core properties', () => {995      const tool = createTool();996      const schema = tool.schema.parametersJsonSchema as {997        properties?: Record<string, unknown>;998      };999      const properties = Object.keys(schema.properties ?? {});1000 1001      // Core properties that must match Claude Code1002      const coreProperties = [1003        'operation',1004        'filePath',1005        'line',1006        'character',1007        'includeDeclaration',1008        'query',1009        'callHierarchyItem',1010      ];1011 1012      for (const prop of coreProperties) {1013        expect(properties).toContain(prop);1014      }1015    });1016 1017    it('extension properties are documented', () => {1018      const tool = createTool();1019      const schema = tool.schema.parametersJsonSchema as {1020        properties?: Record<string, unknown>;1021      };1022      const properties = Object.keys(schema.properties ?? {});1023 1024      // Our extensions beyond Claude Code1025      const extensionProperties = [1026        'serverName',1027        'limit',1028        'endLine',1029        'endCharacter',1030        'diagnostics',1031        'codeActionKinds',1032      ];1033 1034      // All properties should be either core or documented extensions1035      const knownProperties = [1036        'operation',1037        'filePath',1038        'line',1039        'character',1040        'includeDeclaration',1041        'query',1042        'callHierarchyItem',1043        ...extensionProperties,1044      ];1045 1046      for (const prop of properties) {1047        expect(knownProperties).toContain(prop);1048      }1049    });1050 1051    it('filePath property has correct type', () => {1052      const tool = createTool();1053      const schema = tool.schema.parametersJsonSchema as {1054        properties?: {1055          filePath?: { type?: string };1056        };1057      };1058      expect(schema.properties?.filePath?.type).toBe('string');1059    });1060 1061    it('line and character properties have correct type', () => {1062      const tool = createTool();1063      const schema = tool.schema.parametersJsonSchema as {1064        properties?: {1065          line?: { type?: string };1066          character?: { type?: string };1067        };1068      };1069      expect(schema.properties?.line?.type).toBe('number');1070      expect(schema.properties?.character?.type).toBe('number');1071    });1072 1073    it('limit extension property has integer type', () => {1074      const tool = createTool();1075      const schema = tool.schema.parametersJsonSchema as {1076        properties?: {1077          limit?: { type?: string; minimum?: number };1078        };1079      };1080      expect(schema.properties?.limit?.type).toBe('integer');1081      expect(schema.properties?.limit?.minimum).toBe(1);1082    });1083 1084    it('includeDeclaration property has correct type', () => {1085      const tool = createTool();1086      const schema = tool.schema.parametersJsonSchema as {1087        properties?: {1088          includeDeclaration?: { type?: string };1089        };1090      };1091      expect(schema.properties?.includeDeclaration?.type).toBe('boolean');1092    });1093 1094    it('callHierarchyItem has required structure', () => {1095      const tool = createTool();1096      const schema = tool.schema.parametersJsonSchema as {1097        definitions?: {1098          LspCallHierarchyItem?: {1099            type?: string;1100            properties?: Record<string, unknown>;1101            required?: string[];1102          };1103        };1104      };1105      const itemDef = schema.definitions?.LspCallHierarchyItem;1106      expect(itemDef?.type).toBe('object');1107      expect(itemDef?.required).toEqual([1108        'name',1109        'uri',1110        'range',1111        'selectionRange',1112      ]);1113      expect(itemDef?.properties).toHaveProperty('name');1114      expect(itemDef?.properties).toHaveProperty('kind');1115      expect(itemDef?.properties).toHaveProperty('uri');1116      expect(itemDef?.properties).toHaveProperty('range');1117      expect(itemDef?.properties).toHaveProperty('selectionRange');1118    });1119 1120    it('supports rawKind for SymbolKind numeric preservation', () => {1121      const tool = createTool();1122      const schema = tool.schema.parametersJsonSchema as {1123        definitions?: {1124          LspCallHierarchyItem?: {1125            properties?: {1126              rawKind?: { type?: string };1127            };1128          };1129        };1130      };1131      const itemDef = schema.definitions?.LspCallHierarchyItem;1132      expect(itemDef?.properties?.rawKind?.type).toBe('number');1133    });1134 1135    describe('schema definitions deep validation', () => {1136      it('has LspPosition definition with correct structure', () => {1137        const tool = createTool();1138        const schema = tool.schema.parametersJsonSchema as {1139          definitions?: {1140            LspPosition?: {1141              type?: string;1142              properties?: {1143                line?: { type?: string };1144                character?: { type?: string };1145              };1146              required?: string[];1147            };1148          };1149        };1150        const posDef = schema.definitions?.LspPosition;1151        expect(posDef).toBeDefined();1152        expect(posDef?.type).toBe('object');1153        expect(posDef?.properties?.line?.type).toBe('number');1154        expect(posDef?.properties?.character?.type).toBe('number');1155        expect(posDef?.required).toEqual(['line', 'character']);1156      });1157 1158      it('has LspRange definition with correct structure', () => {1159        const tool = createTool();1160        const schema = tool.schema.parametersJsonSchema as {1161          definitions?: {1162            LspRange?: {1163              type?: string;1164              properties?: {1165                start?: { $ref?: string };1166                end?: { $ref?: string };1167              };1168              required?: string[];1169            };1170          };1171        };1172        const rangeDef = schema.definitions?.LspRange;1173        expect(rangeDef).toBeDefined();1174        expect(rangeDef?.type).toBe('object');1175        expect(rangeDef?.properties?.start?.$ref).toBe(1176          '#/definitions/LspPosition',1177        );1178        expect(rangeDef?.properties?.end?.$ref).toBe(1179          '#/definitions/LspPosition',1180        );1181        expect(rangeDef?.required).toEqual(['start', 'end']);1182      });1183 1184      it('callHierarchyItem uses $ref for range fields', () => {1185        const tool = createTool();1186        const schema = tool.schema.parametersJsonSchema as {1187          properties?: {1188            callHierarchyItem?: { $ref?: string };1189          };1190          definitions?: {1191            LspCallHierarchyItem?: {1192              properties?: {1193                range?: { $ref?: string };1194                selectionRange?: { $ref?: string };1195              };1196            };1197          };1198        };1199        // callHierarchyItem property should reference the definition1200        expect(schema.properties?.callHierarchyItem?.$ref).toBe(

Showing the first 1,200 of 1278 lines. Download the file for the rest.

basant307/AI_Governance_Project · CoolFace