Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192import { describe, expect, it, beforeEach, afterEach } from 'bun:test';import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';import { tmpdir } from 'node:os';import { join } from 'node:path';import { TreeSitterCodeIndex, createCodeIndexExtension, createCodeIndexTools } from '@/codeindex';import type { CodeIntelligenceProvider } from '@/contracts/code-intelligence';import type { ExtensionApi } from '@/contracts/extension-api';import type { Tool } from '@/contracts/tool';
let root: string;const files: string[] = [];
beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'shard-codeindex-')); });afterEach(async () => { await rm(root, { recursive: true, force: true }); });
async function fixture(name: string, source: string): Promise<string> { const path = join(root, name); await mkdir(join(path, '..'), { recursive: true }); await writeFile(path, source); files.push(path); return path;}
async function provider(): Promise<TreeSitterCodeIndex> { const index = new TreeSitterCodeIndex(root); await index.initialize(); return index;}
function symbol(index: CodeIntelligenceProvider, path: string, name: string) { const found = index.symbol(path, name); expect(found).toHaveLength(1); return found[0]!;}
function toolContext() { return { agentId: 'test', cwd: root }; }
describe('WASM CodeIndex declarations', () => { it('initializes the real WASM provider and parses TypeScript and TSX', async () => { const index = await provider(); const ts = await fixture('sample.ts', `export interface User {\n id: number;\n}\nexport function greet(user: User): string {\n return user.id.toString();\n}\nexport const answer = 42;\n`); const tsx = await fixture('view.tsx', `export function View(): JSX.Element {\n return <div />;\n}\n`); expect(index.outline(ts)).toEqual([ { name: 'User', kind: 'interface', range: [0, 2], signature: 'interface User' }, { name: 'greet', kind: 'function', range: [3, 5], signature: 'function greet(user: User): string' }, { name: 'answer', kind: 'variable', range: [6, 6], signature: 'const answer = 42;' }, ]); expect(symbol(index, tsx, 'View')).toMatchObject({ kind: 'function', range: [0, 2], signature: 'function View(): JSX.Element' }); });
it('parses JavaScript and JSX using underlying declaration kinds', async () => { const index = await provider(); const path = await fixture('sample.jsx', `export function App() {\n return <main />;\n}\nexport class Store {\n getValue() { return 1; }\n}\nexport const value = 1;\n`); expect(index.outline(path)).toEqual([ { name: 'App', kind: 'function', range: [0, 2], signature: 'function App()' }, { name: 'Store', kind: 'class', range: [3, 5], signature: 'class Store' }, { name: 'getValue', kind: 'method', range: [4, 4], signature: 'getValue()' }, { name: 'value', kind: 'variable', range: [6, 6], signature: 'const value = 1;' }, ]); });
it('parses decorated Python declarations with the underlying kind and signature', async () => { const index = await provider(); const path = await fixture('sample.py', `@dataclass\nclass User:\n id: int\n\n@route('/')\ndef home(name: str) -> str:\n return name\n`); expect(index.outline(path)).toEqual([ { name: 'User', kind: 'class', range: [1, 2], signature: 'class User:' }, { name: 'home', kind: 'function', range: [5, 6], signature: 'def home(name: str) -> str:' }, ]); });
it('extracts Go type and const declarations', async () => { const index = await provider(); const path = await fixture('sample.go', `package sample\n\ntype User struct {\n ID int\n}\n\nconst Answer = 42\n\nfunc Run() {}\n`); expect(index.outline(path)).toEqual([ { name: 'User', kind: 'type', range: [2, 4], signature: 'type User struct' }, { name: 'Answer', kind: 'constant', range: [6, 6], signature: 'const Answer = 42' }, { name: 'Run', kind: 'function', range: [8, 8], signature: 'func Run()' }, ]); });
it('extracts Svelte script declarations and component symbols without indexing markup', async () => { const index = await provider(); const path = await fixture('Counter.svelte', `<script lang="ts">\n export let initial = 0;\n const label: string = 'count';\n function increment() { }\n</script>\n\n<section>\n <h1>{label}</h1>\n</section>\n`); expect(index.outline(path)).toEqual([ { name: 'initial', kind: 'variable', range: [1, 1], signature: 'export let initial = 0;' }, { name: 'label', kind: 'variable', range: [2, 2], signature: "const label: string = 'count';" }, { name: 'increment', kind: 'function', range: [3, 3], signature: 'function increment()' }, ]); expect(index.symbol(path, 'section')).toEqual([]); });
it('extracts Rust struct, enum, trait, type, const, static, and impl', async () => { const index = await provider(); const path = await fixture('sample.rs', `struct User { id: i32 }\nenum State { Ready }\ntrait Displayable {}\ntype Id = i32;\nconst ANSWER: i32 = 42;\nstatic GLOBAL: i32 = 1;\nimpl User { fn id(&self) -> i32 { self.id } }\n`); expect(index.outline(path)).toEqual([ { name: 'User', kind: 'struct', range: [0, 0], signature: 'struct User' }, { name: 'State', kind: 'enum', range: [1, 1], signature: 'enum State' }, { name: 'Displayable', kind: 'trait', range: [2, 2], signature: 'trait Displayable' }, { name: 'Id', kind: 'type', range: [3, 3], signature: 'type Id = i32;' }, { name: 'ANSWER', kind: 'constant', range: [4, 4], signature: 'const ANSWER: i32 = 42;' }, { name: 'GLOBAL', kind: 'static', range: [5, 5], signature: 'static GLOBAL: i32 = 1;' }, { name: 'User', kind: 'impl', range: [6, 6], signature: 'impl User' }, { name: 'id', kind: 'method', range: [6, 6], signature: 'fn id(&self) -> i32' }, ]); });});
describe('CodeIndex cache and tools', () => { it('resolves class methods directly without reading the enclosing class', async () => { const index = await provider(); const path = await fixture('class.ts', `class AgentSession {\n runCompaction(): void {\n const internalBody = true;\n }\n}\n`); expect(index.symbol(path, 'runCompaction')).toEqual([ { name: 'runCompaction', kind: 'method', range: [1, 3], signature: 'runCompaction(): void' }, ]); expect(index.signatures(path)).toEqual(['class AgentSession']); });
it('returns every exact duplicate match in source order and signatures', async () => { const index = await provider(); const path = await fixture('dupes.ts', `function same(): void {}\n\nfunction same(value: number): void { return; }\n`); expect(index.symbol(path, 'same')).toHaveLength(2); expect(index.symbol(path, 'same').map((item) => item.range)).toEqual([[0, 0], [2, 2]]); expect(index.signatures(path)).toEqual(['function same(): void', 'function same(value: number): void']); });
it('converts inclusive 0-based ranges to 1-based read hints', async () => { const index = await provider(); await fixture('tool.ts', `function multi() {\n return 1;\n}\n`); const tool = createCodeIndexTools(index).find((item) => item.name === 'code_symbol')!; const result = await tool.execute({ path: 'tool.ts', name: 'multi' }, toolContext()); expect(result.isError).toBe(false); expect(result.content).toMatchObject({ kind: 'text', text: expect.stringContaining('read { path: tool.ts, offset: 1, limit: 3 }') }); });
it('validates tool input and reports missing, unsupported, and absent symbols', async () => { const index = await provider(); const tools = createCodeIndexTools(index); const symbolTool = tools.find((item) => item.name === 'code_symbol')!; const outlineTool = tools.find((item) => item.name === 'code_outline')!; expect((await symbolTool.execute({ path: '', name: 'x' }, toolContext())).isError).toBe(true); expect((await outlineTool.execute({ path: 'missing.ts' }, toolContext())).isError).toBe(true); expect((await outlineTool.execute({ path: 'unknown.txt' }, toolContext())).isError).toBe(true); const path = await fixture('empty.ts', 'const present = true;\n'); const result = await symbolTool.execute({ path, name: 'absent' }, toolContext()); expect(result).toMatchObject({ isError: false, content: { kind: 'text', text: 'No exact symbol matches for absent' } }); });
it('marks global outline truncation and excludes function bodies', async () => { const index = await provider(); const source = Array.from({ length: 201 }, (_, i) => `function fn${i}() {\n const body${i} = true;\n}`).join('\n'); await fixture('large.ts', source); const tool = createCodeIndexTools(index).find((item) => item.name === 'code_outline')!; const result = await tool.execute({ path: 'large.ts' }, toolContext()); const text = String((result.content as { text: string }).text); expect(text).toContain('[truncated: additional symbols omitted]'); expect(text).not.toContain('body0'); expect(Buffer.byteLength(text)).toBeLessThanOrEqual(8_000); });
it('refreshes fingerprints, honors invalidate, and safely disposes repeatedly', async () => { const index = await provider(); const path = await fixture('refresh.ts', 'const first = 1;\n'); expect(index.outline(path).map((item) => item.name)).toEqual(['first']); await writeFile(path, 'const second = 2;\n'); expect(index.outline(path).map((item) => item.name)).toEqual(['second']); await writeFile(path, 'const third = 3;\n'); index.invalidate(path); expect(index.outline(path).map((item) => item.name)).toEqual(['third']); index.dispose(); index.dispose(); });});
describe('CodeIndex extension installation', () => { it('registers exactly one provider and two tools, and its disposer is safe', async () => { const registeredProviders: CodeIntelligenceProvider[] = []; const registeredTools: Tool[] = []; const extension = createCodeIndexExtension(root); const api = { registerCodeIntelligence(provider: CodeIntelligenceProvider) { registeredProviders.push(provider); }, registerTool(tool: Tool) { registeredTools.push(tool); }, } as unknown as ExtensionApi; const disposer = await extension.install(api); expect(registeredProviders).toHaveLength(1); expect(registeredTools.map((tool) => tool.name)).toEqual(['code_outline', 'code_symbol']); expect(disposer).toBeFunction(); await disposer?.(); await disposer?.(); expect(() => registeredProviders[0]!.invalidate('none.ts')).not.toThrow(); });});