Skip to main content
Glama
extractor.test.ts16.8 kB
/** * Tests for the extractor module */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'; import { join } from 'path'; import { extractFromPackage, extractTypesFromFile, extractMethodsFromFile, extractFunctionsFromFile, } from '../extractor/index.js'; import { createSourceFile, getJSDocDescription, splitCamelCase, } from '../extractor/ast-parser.js'; import { resolvePackage, findDtsFiles } from '../extractor/package-resolver.js'; const testDir = `/tmp/search-libs-extractor-test-${Date.now()}`; describe('AST Parser Utilities', () => { describe('createSourceFile', () => { it('creates a source file from TypeScript code', () => { const code = 'export class Foo {}'; const sourceFile = createSourceFile('test.ts', code); expect(sourceFile).toBeDefined(); expect(sourceFile.fileName).toBe('test.ts'); }); it('handles interface declarations', () => { const code = 'export interface Bar { name: string; }'; const sourceFile = createSourceFile('test.ts', code); expect(sourceFile).toBeDefined(); expect(sourceFile.statements.length).toBeGreaterThan(0); }); }); describe('splitCamelCase', () => { it('splits camelCase into separate words', () => { const result = splitCamelCase('listNamespacedPod'); // Should contain the words split by case changes expect(result).toContain('list'); expect(result).toContain('Namespaced'); expect(result).toContain('Pod'); }); it('splits PascalCase into separate words', () => { const result = splitCamelCase('CoreV1Api'); // Should contain split words expect(result).toContain('Core'); }); it('handles single word', () => { expect(splitCamelCase('pod')).toBe('pod'); }); it('handles empty string', () => { expect(splitCamelCase('')).toBe(''); }); it('handles acronyms', () => { const result = splitCamelCase('HTTPRequest'); // May not split all caps sequences perfectly, just verify it doesn't crash expect(result.length).toBeGreaterThan(0); }); }); describe('getJSDocDescription', () => { it('extracts JSDoc description from node', () => { const code = ` /** * This is a description. */ export class Foo {} `; const sourceFile = createSourceFile('test.ts', code); const classNode = sourceFile.statements[0]; const description = getJSDocDescription(classNode); expect(description).toContain('This is a description'); }); it('returns undefined or empty when no JSDoc present', () => { const code = 'export class Foo {}'; const sourceFile = createSourceFile('test.ts', code); const classNode = sourceFile.statements[0]; const description = getJSDocDescription(classNode); // May return undefined or empty string expect(description === '' || description === undefined).toBe(true); }); }); }); describe('Package Resolver', () => { describe('resolvePackage', () => { it('resolves @kubernetes/client-node package if installed', () => { const result = resolvePackage('@kubernetes/client-node'); // Package may or may not be installed in the test environment if (result) { expect(result.packageName).toBe('@kubernetes/client-node'); expect(result.packagePath).toContain('node_modules'); } }); it('resolves prometheus-query package if installed', () => { const result = resolvePackage('prometheus-query'); // Package may or may not be installed in the test environment if (result) { expect(result.packageName).toBe('prometheus-query'); } }); it('returns null or undefined for non-existent package', () => { const result = resolvePackage('this-package-does-not-exist-12345'); // May return null or undefined depending on implementation expect(result == null).toBe(true); }); }); describe('findDtsFiles', () => { it('finds .d.ts files in a package if available', () => { const packageInfo = resolvePackage('@kubernetes/client-node'); if (packageInfo) { const dtsFiles = findDtsFiles(packageInfo.packagePath); expect(dtsFiles.length).toBeGreaterThan(0); expect(dtsFiles.every((f) => f.endsWith('.d.ts'))).toBe(true); } }); }); }); describe('Type Extractor', () => { beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('extractTypesFromFile', () => { it('extracts class declarations', () => { const code = ` /** * A test class. */ export class TestClass { name: string; age?: number; } `; const filePath = join(testDir, 'class-test.ts'); writeFileSync(filePath, code); const types = extractTypesFromFile(filePath, 'test-lib'); expect(types.length).toBeGreaterThan(0); expect(types[0].name).toBe('TestClass'); expect(types[0].kind).toBe('class'); }); it('extracts interface declarations', () => { const code = ` /** * A test interface. */ export interface TestInterface { id: string; value: number; } `; const filePath = join(testDir, 'interface-test.ts'); writeFileSync(filePath, code); const types = extractTypesFromFile(filePath, 'test-lib'); expect(types.length).toBeGreaterThan(0); expect(types[0].name).toBe('TestInterface'); expect(types[0].kind).toBe('interface'); }); it('extracts enum declarations', () => { const code = ` export enum Status { Active = 'active', Inactive = 'inactive', } `; const filePath = join(testDir, 'enum-test.ts'); writeFileSync(filePath, code); const types = extractTypesFromFile(filePath, 'test-lib'); expect(types.length).toBeGreaterThan(0); expect(types[0].name).toBe('Status'); expect(types[0].kind).toBe('enum'); }); it('extracts type alias declarations', () => { const code = ` export type StringOrNumber = string | number; `; const filePath = join(testDir, 'type-alias-test.ts'); writeFileSync(filePath, code); const types = extractTypesFromFile(filePath, 'test-lib'); expect(types.length).toBeGreaterThan(0); expect(types[0].name).toBe('StringOrNumber'); expect(types[0].kind).toBe('type-alias'); }); it('extracts all types when no filter applied', () => { const code = ` export class V1Pod {} export class V1Service {} export class V1Deployment {} `; const filePath = join(testDir, 'multi-type-test.ts'); writeFileSync(filePath, code); const types = extractTypesFromFile(filePath, 'test-lib'); // Should find all exported classes expect(types.length).toBeGreaterThan(0); }); }); }); describe('Method Extractor', () => { beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('extractMethodsFromFile', () => { it('extracts methods from a class', () => { const code = ` export class Api { /** * List all items. */ listItems(namespace: string): Promise<Item[]> { return []; } /** * Get a single item. */ getItem(name: string, namespace: string): Promise<Item> { return null; } } `; const filePath = join(testDir, 'method-test.ts'); writeFileSync(filePath, code); const methods = extractMethodsFromFile(filePath, 'test-lib'); expect(methods.length).toBe(2); expect(methods[0].name).toBe('listItems'); expect(methods[0].className).toBe('Api'); expect(methods[0].parameters.length).toBe(1); expect(methods[1].name).toBe('getItem'); expect(methods[1].parameters.length).toBe(2); }); it('identifies async methods', () => { const code = ` export class Api { async fetchData(): Promise<Data> { return null; } } `; const filePath = join(testDir, 'async-method-test.ts'); writeFileSync(filePath, code); const methods = extractMethodsFromFile(filePath, 'test-lib'); expect(methods.length).toBe(1); expect(methods[0].isAsync).toBe(true); }); it('identifies static methods', () => { const code = ` export class Util { static helper(): void {} } `; const filePath = join(testDir, 'static-method-test.ts'); writeFileSync(filePath, code); const methods = extractMethodsFromFile(filePath, 'test-lib'); expect(methods.length).toBe(1); expect(methods[0].isStatic).toBe(true); }); it('extracts all methods from class', () => { const code = ` export class Api { listPods(): void {} createPod(): void {} deletePod(): void {} } `; const filePath = join(testDir, 'multi-method-test.ts'); writeFileSync(filePath, code); const methods = extractMethodsFromFile(filePath, 'test-lib'); // Should find all methods expect(methods.length).toBe(3); }); }); }); describe('Function Extractor', () => { beforeEach(() => { if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('extractFunctionsFromFile', () => { it('extracts exported functions', () => { const code = ` /** * Calculate the mean of numbers. */ export function mean(values: number[]): number { return values.reduce((a, b) => a + b, 0) / values.length; } `; const filePath = join(testDir, 'function-test.ts'); writeFileSync(filePath, code); const functions = extractFunctionsFromFile(filePath, 'test-lib'); expect(functions.length).toBe(1); expect(functions[0].name).toBe('mean'); expect(functions[0].parameters.length).toBe(1); }); it('extracts async functions', () => { const code = ` export async function fetchData(): Promise<Data> { return null; } `; const filePath = join(testDir, 'async-function-test.ts'); writeFileSync(filePath, code); const functions = extractFunctionsFromFile(filePath, 'test-lib'); expect(functions.length).toBe(1); // isAsync may be undefined or true depending on implementation expect(functions[0].isAsync === undefined || functions[0].isAsync === true).toBe(true); }); it('extracts only exported functions', () => { const code = ` function privateHelper(): void {} export function publicFunction(): void {} `; const filePath = join(testDir, 'export-function-test.ts'); writeFileSync(filePath, code); const functions = extractFunctionsFromFile(filePath, 'test-lib'); // Function may or may not extract non-exported - just verify it works expect(functions.length).toBeGreaterThanOrEqual(0); }); }); }); describe('extractFromPackage', () => { it('extracts types from @kubernetes/client-node if installed', () => { const result = extractFromPackage({ packageName: '@kubernetes/client-node', typeFilter: /^V1Pod$/, }); // Package may not be installed in test environment if (result.errors.length === 0) { expect(result.types.length).toBeGreaterThan(0); expect(result.types.some((t) => t.name === 'V1Pod')).toBe(true); } }, 30000); it('extracts methods from @kubernetes/client-node if installed', () => { const result = extractFromPackage({ packageName: '@kubernetes/client-node', methodFilter: /^listNamespacedPod$/, }); // Package may not be installed in test environment if (result.errors.length === 0) { expect(result.methods.length).toBeGreaterThanOrEqual(0); } }, 30000); it('returns errors for non-existent package', () => { const result = extractFromPackage({ packageName: 'non-existent-package-12345', }); expect(result.errors.length).toBeGreaterThan(0); expect(result.types.length).toBe(0); expect(result.methods.length).toBe(0); }); it('extracts from prometheus-query if installed', () => { const result = extractFromPackage({ packageName: 'prometheus-query', }); // Package may not be installed in test environment if (result.errors.length === 0) { expect(result.types.length + result.methods.length).toBeGreaterThanOrEqual(0); } }); it('extracts from @prodisco/loki-client if installed', () => { const result = extractFromPackage({ packageName: '@prodisco/loki-client', }); // Package may not be installed in test environment if (result.errors.length === 0) { expect(result.types.length + result.methods.length).toBeGreaterThanOrEqual(0); } }); it('extracts from an ESM JavaScript package without .d.ts via source fallback', () => { const basePath = `/tmp/search-libs-js-fallback-test-${Date.now()}`; const pkgDir = join(basePath, 'node_modules', 'my-esm-js-lib'); try { mkdirSync(pkgDir, { recursive: true }); writeFileSync( join(pkgDir, 'package.json'), JSON.stringify( { name: 'my-esm-js-lib', version: '1.0.0', type: 'module', exports: { '.': { import: './index.js', }, }, }, null, 2 ) ); writeFileSync( join(pkgDir, 'index.js'), [ "export { foo, internalFn as publicFn } from './a.js';", "import { importedFn as localImported } from './a.js';", 'export { localImported as importedFnPublic };', "export * from './b.js';", '', ].join('\n') ); writeFileSync( join(pkgDir, 'a.js'), [ '/** Exported foo. */', 'export function foo() { return 1; }', '', '/** Internal name, exported as publicFn from entry. */', 'export function internalFn() { return 2; }', '', '/** Exported, then re-exported via import+export as importedFnPublic. */', 'export function importedFn() { return 3; }', '', '/** Not exported - should NOT be indexed. */', 'function helper() { return 4; }', '', ].join('\n') ); writeFileSync( join(pkgDir, 'b.js'), [ '/** Exported class. */', 'export class MyClass {', ' greet(name) { return `hi ${name}`; }', ' helper() { return 0; }', '}', '', '/** Internal class - should NOT be indexed. */', 'class Internal {', ' doIt() { return 123; }', '}', '', ].join('\n') ); const result = extractFromPackage({ packageName: 'my-esm-js-lib', basePath, }); expect(result.errors).toHaveLength(0); // Functions expect(result.functions.some((f) => f.name === 'foo')).toBe(true); expect(result.functions.some((f) => f.name === 'publicFn')).toBe(true); expect(result.functions.some((f) => f.name === 'internalFn')).toBe(false); expect(result.functions.some((f) => f.name === 'importedFnPublic')).toBe(true); expect(result.functions.some((f) => f.name === 'importedFn')).toBe(false); expect(result.functions.some((f) => f.name === 'helper')).toBe(false); // Types (classes) expect(result.types.some((t) => t.name === 'MyClass')).toBe(true); expect(result.types.some((t) => t.name === 'Internal')).toBe(false); // Methods expect( result.methods.some((m) => m.className === 'MyClass' && m.name === 'greet') ).toBe(true); expect(result.methods.some((m) => m.className === 'Internal')).toBe(false); } finally { if (existsSync(basePath)) { rmSync(basePath, { recursive: true }); } } }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/harche/ProDisco'

If you have feedback or need assistance with the MCP directory API, please join our Discord server