Skip to main content
Glama
rest_client.test.ts11.5 kB
import { describe, it, expect, beforeAll, afterAll } from 'bun:test' import { startMockRest } from './mockServer.js' import { restSearch, restGetChunk, restGetFileSlice, restListRepos } from '../rest/client.js' let stop: () => Promise<void> beforeAll(async () => { stop = await startMockRest(7777) // Set environment variable to point to mock server process.env.KB_REST_BASE_URL = 'http://localhost:7777' }) afterAll(async () => { await stop?.() delete process.env.KB_REST_BASE_URL }) describe('REST Client - Search', () => { it('should make successful search request', async () => { const result = await restSearch({ query: 'test query', top_k: 10 }) expect(result).toBeDefined() expect(result.hits).toBeDefined() expect(Array.isArray(result.hits)).toBe(true) expect(result.meta).toBeDefined() }) it('should pass all search parameters correctly', async () => { const result = await restSearch({ query: 'test', top_k: 20, repos: ['repoa', 'repob'], path_prefix: ['src/'], score_cutoff: 0.5, embed_model: 'large' }) expect(result).toBeDefined() expect(result.hits).toBeDefined() }) it('should handle search with cursor', async () => { const result = await restSearch({ query: 'test', cursor: 'cursor-token-123' }) expect(result).toBeDefined() expect(result.hits).toBeDefined() }) it('should handle search with MMR enabled', async () => { const result = await restSearch({ query: 'test', mmr_enabled: true, mmr_lambda: 0.7 }) expect(result).toBeDefined() }) it('should handle search with graph context', async () => { const result = await restSearch({ query: 'test', include_graph_context: true }) expect(result).toBeDefined() }) it('should handle search with ANN strategy parameters', async () => { const result = await restSearch({ query: 'test', ann_strategy: 'adaptive', ann_nprobes: 10, ann_refine_factor: 2.0 }) expect(result).toBeDefined() }) it('should handle search with path filters', async () => { const result = await restSearch({ query: 'test', path_prefix: ['src/', 'lib/'], exclude_paths: ['test/', 'dist/'], exclude_patterns: ['*.test.ts', '*.spec.ts'] }) expect(result).toBeDefined() }) it('should handle search with deadline', async () => { const result = await restSearch({ query: 'test', deadline_ms: 5000 }) expect(result).toBeDefined() }) it('should handle repo not found error', async () => { await expect(async () => { await restSearch({ query: 'test', repos: ['nonexistent-repo'] }) }).toThrow() }) it('should handle invalid JSON response', async () => { // Mock server can return invalid JSON for specific queries await expect(async () => { await restSearch({ query: 'trigger-invalid-json' }) }).toThrow() }) it('should support AbortSignal for cancellation', async () => { const controller = new AbortController() // Start search then immediately cancel const searchPromise = restSearch({ query: 'test' }, controller.signal) controller.abort() await expect(searchPromise).rejects.toThrow() }) }) describe('REST Client - Get Chunk', () => { it('should fetch chunk by ID successfully', async () => { const chunk = await restGetChunk('abc123') expect(chunk).toBeDefined() expect(chunk.chunk_id).toBe('abc123') expect(chunk.content).toBeDefined() expect(chunk.repo).toBeDefined() expect(chunk.path).toBeDefined() expect(chunk.start_line).toBeGreaterThan(0) expect(chunk.end_line).toBeGreaterThanOrEqual(chunk.start_line) }) it('should include metadata in chunk response', async () => { const chunk = await restGetChunk('test-chunk-id') expect(chunk.lang).toBeDefined() expect(chunk.resource_link).toBeDefined() }) it('should handle chunk not found error', async () => { await expect(async () => { await restGetChunk('not-found') }).toThrow() }) it('should properly encode chunk ID in URL', async () => { // Test with special characters that need URL encoding const specialId = 'chunk/with/slashes' // Should not crash, mock server will handle it const result = await restGetChunk(specialId) expect(result).toBeDefined() }) it('should support AbortSignal for chunk fetch', async () => { const controller = new AbortController() const chunkPromise = restGetChunk('test-id', controller.signal) controller.abort() await expect(chunkPromise).rejects.toThrow() }) }) describe('REST Client - Get File Slice', () => { it('should fetch file slice successfully', async () => { const slice = await restGetFileSlice( 'repoa', 'src/test.ts', 1, 10 ) expect(slice).toBeDefined() expect(slice.repo).toBe('repoa') expect(slice.path).toBe('src/test.ts') expect(slice.start_line).toBe(1) expect(slice.end_line).toBe(10) expect(slice.content).toBeDefined() }) it('should include language in response', async () => { const slice = await restGetFileSlice( 'repoa', 'src/app.ts', 5, 15 ) expect(slice.lang).toBeDefined() }) it('should handle Python files', async () => { const slice = await restGetFileSlice( 'repoa', 'main.py', 1, 20 ) expect(slice).toBeDefined() expect(slice.lang).toBe('python') }) it('should handle file not found error', async () => { await expect(async () => { await restGetFileSlice( 'repoa', 'nonexistent.ts', 1, 10 ) }).toThrow() }) it('should handle invalid line range error', async () => { await expect(async () => { await restGetFileSlice( 'repoa', 'src/test.ts', 10, // start > end 1 ) }).toThrow() }) it('should properly encode path in URL', async () => { const slice = await restGetFileSlice( 'repoa', 'src/path with spaces/file.ts', 1, 5 ) expect(slice).toBeDefined() }) it('should support AbortSignal for file slice', async () => { const controller = new AbortController() const slicePromise = restGetFileSlice('repoa', 'test.ts', 1, 10, controller.signal) controller.abort() await expect(slicePromise).rejects.toThrow() }) }) describe('REST Client - List Repos', () => { it('should fetch list of repositories', async () => { const result = await restListRepos() expect(result).toBeDefined() expect(result.repos).toBeDefined() expect(Array.isArray(result.repos)).toBe(true) expect(result.repos.length).toBeGreaterThan(0) }) it('should include repo metadata', async () => { const result = await restListRepos() const repo = result.repos[0] expect(repo.name).toBeDefined() expect(repo.path).toBeDefined() }) it('should include optional repo statistics', async () => { const result = await restListRepos() const repo = result.repos[0] // These fields are optional but should be present in mock expect(repo.default_embed_model).toBeDefined() expect(repo.files).toBeDefined() expect(repo.chunks).toBeDefined() }) it('should support AbortSignal for listing repos', async () => { const controller = new AbortController() const reposPromise = restListRepos(controller.signal) controller.abort() await expect(reposPromise).rejects.toThrow() }) }) describe('REST Client - Error Handling', () => { it('should handle HTTP 500 errors gracefully', async () => { await expect(async () => { await restSearch({ query: 'trigger-500' }) }).toThrow() }) it('should handle network errors', async () => { // Temporarily change to invalid URL const originalUrl = process.env.KB_REST_BASE_URL process.env.KB_REST_BASE_URL = 'http://localhost:9999' await expect(async () => { await restSearch({ query: 'test' }) }).toThrow() // Restore process.env.KB_REST_BASE_URL = originalUrl }) it('should include error details in thrown errors', async () => { try { await restSearch({ query: 'test', repos: ['nonexistent-repo'] }) expect(false).toBe(true) // Should not reach here } catch (error: any) { expect(error.error).toBeDefined() expect(error.error.code).toBeDefined() expect(error.error.message).toBeDefined() } }) it('should handle malformed JSON responses', async () => { await expect(async () => { await restSearch({ query: 'trigger-invalid-json' }) }).toThrow() }) it('should provide remediation hints in errors', async () => { try { await restSearch({ query: 'trigger-invalid-json' }) expect(false).toBe(true) } catch (error: any) { // Error should have remediation field if (error.error?.remediation) { expect(typeof error.error.remediation).toBe('string') } } }) }) describe('REST Client - Headers', () => { it('should send correct Content-Type header', async () => { // This is implicitly tested by successful requests const result = await restSearch({ query: 'test' }) expect(result).toBeDefined() }) it('should send X-Client header for tracking', async () => { // This is implicitly tested by successful requests const result = await restSearch({ query: 'test' }) expect(result).toBeDefined() }) }) describe('REST Client - Edge Cases', () => { it('should handle empty search query', async () => { const result = await restSearch({ query: '' }) expect(result).toBeDefined() }) it('should handle very large top_k value', async () => { const result = await restSearch({ query: 'test', top_k: 10000 }) expect(result).toBeDefined() }) it('should handle top_k of 1', async () => { const result = await restSearch({ query: 'test', top_k: 1 }) expect(result).toBeDefined() }) it('should handle empty repos array', async () => { const result = await restSearch({ query: 'test', repos: [] }) expect(result).toBeDefined() }) it('should handle repos with special characters', async () => { const result = await restSearch({ query: 'test', repos: ['repo-with-dash', 'repo_with_underscore'] }) expect(result).toBeDefined() }) it('should handle path_prefix with various patterns', async () => { const result = await restSearch({ query: 'test', path_prefix: ['src/', 'lib/', 'packages/*/src/'] }) expect(result).toBeDefined() }) it('should handle score_cutoff edge values', async () => { const result1 = await restSearch({ query: 'test', score_cutoff: 0.0 }) expect(result1).toBeDefined() const result2 = await restSearch({ query: 'test', score_cutoff: 1.0 }) expect(result2).toBeDefined() }) it('should handle mmr_lambda edge values', async () => { const result1 = await restSearch({ query: 'test', mmr_enabled: true, mmr_lambda: 0.0 }) expect(result1).toBeDefined() const result2 = await restSearch({ query: 'test', mmr_enabled: true, mmr_lambda: 1.0 }) expect(result2).toBeDefined() }) })

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/plasticbeachllc/dolphin-mcp'

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