lance-context 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -23
- package/dist/__tests__/ast-chunker.test.d.ts +2 -0
- package/dist/__tests__/ast-chunker.test.d.ts.map +1 -0
- package/dist/__tests__/ast-chunker.test.js +307 -0
- package/dist/__tests__/ast-chunker.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +242 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/dashboard/beads.test.d.ts +2 -0
- package/dist/__tests__/dashboard/beads.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/beads.test.js +151 -0
- package/dist/__tests__/dashboard/beads.test.js.map +1 -0
- package/dist/__tests__/dashboard/index.test.d.ts +2 -0
- package/dist/__tests__/dashboard/index.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/index.test.js +116 -0
- package/dist/__tests__/dashboard/index.test.js.map +1 -0
- package/dist/__tests__/dashboard/routes.test.d.ts +2 -0
- package/dist/__tests__/dashboard/routes.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/routes.test.js +125 -0
- package/dist/__tests__/dashboard/routes.test.js.map +1 -0
- package/dist/__tests__/dashboard/server.test.d.ts +2 -0
- package/dist/__tests__/dashboard/server.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/server.test.js +75 -0
- package/dist/__tests__/dashboard/server.test.js.map +1 -0
- package/dist/__tests__/dashboard/state.test.d.ts +2 -0
- package/dist/__tests__/dashboard/state.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/state.test.js +124 -0
- package/dist/__tests__/dashboard/state.test.js.map +1 -0
- package/dist/__tests__/embeddings/factory.test.d.ts +2 -0
- package/dist/__tests__/embeddings/factory.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/factory.test.js +100 -0
- package/dist/__tests__/embeddings/factory.test.js.map +1 -0
- package/dist/__tests__/embeddings/jina.test.d.ts +2 -0
- package/dist/__tests__/embeddings/jina.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/jina.test.js +156 -0
- package/dist/__tests__/embeddings/jina.test.js.map +1 -0
- package/dist/__tests__/embeddings/ollama.test.d.ts +2 -0
- package/dist/__tests__/embeddings/ollama.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/ollama.test.js +172 -0
- package/dist/__tests__/embeddings/ollama.test.js.map +1 -0
- package/dist/__tests__/embeddings/rate-limiter.test.d.ts +2 -0
- package/dist/__tests__/embeddings/rate-limiter.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/rate-limiter.test.js +163 -0
- package/dist/__tests__/embeddings/rate-limiter.test.js.map +1 -0
- package/dist/__tests__/embeddings/retry.test.d.ts +2 -0
- package/dist/__tests__/embeddings/retry.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/retry.test.js +260 -0
- package/dist/__tests__/embeddings/retry.test.js.map +1 -0
- package/dist/__tests__/embeddings/types.test.d.ts +2 -0
- package/dist/__tests__/embeddings/types.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/types.test.js +31 -0
- package/dist/__tests__/embeddings/types.test.js.map +1 -0
- package/dist/__tests__/mocks/embedding-backend.mock.d.ts +10 -0
- package/dist/__tests__/mocks/embedding-backend.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/embedding-backend.mock.js +39 -0
- package/dist/__tests__/mocks/embedding-backend.mock.js.map +1 -0
- package/dist/__tests__/mocks/fetch.mock.d.ts +38 -0
- package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/fetch.mock.js +74 -0
- package/dist/__tests__/mocks/fetch.mock.js.map +1 -0
- package/dist/__tests__/mocks/lancedb.mock.d.ts +38 -0
- package/dist/__tests__/mocks/lancedb.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/lancedb.mock.js +63 -0
- package/dist/__tests__/mocks/lancedb.mock.js.map +1 -0
- package/dist/__tests__/search/clustering.test.d.ts +2 -0
- package/dist/__tests__/search/clustering.test.d.ts.map +1 -0
- package/dist/__tests__/search/clustering.test.js +230 -0
- package/dist/__tests__/search/clustering.test.js.map +1 -0
- package/dist/__tests__/search/hybrid-search.test.d.ts +2 -0
- package/dist/__tests__/search/hybrid-search.test.d.ts.map +1 -0
- package/dist/__tests__/search/hybrid-search.test.js +186 -0
- package/dist/__tests__/search/hybrid-search.test.js.map +1 -0
- package/dist/__tests__/search/indexer.test.d.ts +2 -0
- package/dist/__tests__/search/indexer.test.d.ts.map +1 -0
- package/dist/__tests__/search/indexer.test.js +878 -0
- package/dist/__tests__/search/indexer.test.js.map +1 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.d.ts +2 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.d.ts.map +1 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.js +228 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.js.map +1 -0
- package/dist/__tests__/setup.d.ts +2 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +11 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/__tests__/utils/concurrency.test.d.ts +2 -0
- package/dist/__tests__/utils/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/utils/concurrency.test.js +83 -0
- package/dist/__tests__/utils/concurrency.test.js.map +1 -0
- package/dist/__tests__/utils/errors.test.d.ts +2 -0
- package/dist/__tests__/utils/errors.test.d.ts.map +1 -0
- package/dist/__tests__/utils/errors.test.js +136 -0
- package/dist/__tests__/utils/errors.test.js.map +1 -0
- package/dist/__tests__/utils/type-guards.test.d.ts +2 -0
- package/dist/__tests__/utils/type-guards.test.d.ts.map +1 -0
- package/dist/__tests__/utils/type-guards.test.js +80 -0
- package/dist/__tests__/utils/type-guards.test.js.map +1 -0
- package/dist/__tests__/worktree/worktree-manager.test.d.ts +2 -0
- package/dist/__tests__/worktree/worktree-manager.test.d.ts.map +1 -0
- package/dist/__tests__/worktree/worktree-manager.test.js +403 -0
- package/dist/__tests__/worktree/worktree-manager.test.js.map +1 -0
- package/dist/config.d.ts +122 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +508 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/beads.d.ts +35 -0
- package/dist/dashboard/beads.d.ts.map +1 -0
- package/dist/dashboard/beads.js +102 -0
- package/dist/dashboard/beads.js.map +1 -0
- package/dist/dashboard/events.d.ts +46 -0
- package/dist/dashboard/events.d.ts.map +1 -0
- package/dist/dashboard/events.js +141 -0
- package/dist/dashboard/events.js.map +1 -0
- package/dist/dashboard/index.d.ts +67 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +90 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/routes.d.ts +6 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +244 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard/server.d.ts +27 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +72 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/state.d.ts +116 -0
- package/dist/dashboard/state.d.ts.map +1 -0
- package/dist/dashboard/state.js +251 -0
- package/dist/dashboard/state.js.map +1 -0
- package/dist/dashboard/ui.d.ts +6 -0
- package/dist/dashboard/ui.d.ts.map +1 -0
- package/dist/dashboard/ui.js +1407 -0
- package/dist/dashboard/ui.js.map +1 -0
- package/dist/embeddings/index.d.ts +20 -2
- package/dist/embeddings/index.d.ts.map +1 -1
- package/dist/embeddings/index.js +49 -6
- package/dist/embeddings/index.js.map +1 -1
- package/dist/embeddings/jina.d.ts +9 -0
- package/dist/embeddings/jina.d.ts.map +1 -1
- package/dist/embeddings/jina.js +42 -2
- package/dist/embeddings/jina.js.map +1 -1
- package/dist/embeddings/ollama.d.ts +2 -0
- package/dist/embeddings/ollama.d.ts.map +1 -1
- package/dist/embeddings/ollama.js +21 -5
- package/dist/embeddings/ollama.js.map +1 -1
- package/dist/embeddings/rate-limiter.d.ts +75 -0
- package/dist/embeddings/rate-limiter.d.ts.map +1 -0
- package/dist/embeddings/rate-limiter.js +145 -0
- package/dist/embeddings/rate-limiter.js.map +1 -0
- package/dist/embeddings/retry.d.ts +14 -0
- package/dist/embeddings/retry.d.ts.map +1 -0
- package/dist/embeddings/retry.js +89 -0
- package/dist/embeddings/retry.js.map +1 -0
- package/dist/embeddings/types.d.ts +56 -2
- package/dist/embeddings/types.d.ts.map +1 -1
- package/dist/embeddings/types.js +16 -0
- package/dist/embeddings/types.js.map +1 -1
- package/dist/index.js +1870 -44
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.ts +63 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +168 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/search/ast-chunker.d.ts +34 -0
- package/dist/search/ast-chunker.d.ts.map +1 -0
- package/dist/search/ast-chunker.js +261 -0
- package/dist/search/ast-chunker.js.map +1 -0
- package/dist/search/clustering.d.ts +77 -0
- package/dist/search/clustering.d.ts.map +1 -0
- package/dist/search/clustering.js +455 -0
- package/dist/search/clustering.js.map +1 -0
- package/dist/search/indexer.d.ts +239 -3
- package/dist/search/indexer.d.ts.map +1 -1
- package/dist/search/indexer.js +941 -45
- package/dist/search/indexer.js.map +1 -1
- package/dist/search/tree-sitter-chunker.d.ts +69 -0
- package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
- package/dist/search/tree-sitter-chunker.js +436 -0
- package/dist/search/tree-sitter-chunker.js.map +1 -0
- package/dist/symbols/index.d.ts +14 -0
- package/dist/symbols/index.d.ts.map +1 -0
- package/dist/symbols/index.js +19 -0
- package/dist/symbols/index.js.map +1 -0
- package/dist/symbols/name-path.d.ts +113 -0
- package/dist/symbols/name-path.d.ts.map +1 -0
- package/dist/symbols/name-path.js +194 -0
- package/dist/symbols/name-path.js.map +1 -0
- package/dist/symbols/pattern-search.d.ts +14 -0
- package/dist/symbols/pattern-search.d.ts.map +1 -0
- package/dist/symbols/pattern-search.js +224 -0
- package/dist/symbols/pattern-search.js.map +1 -0
- package/dist/symbols/reference-finder.d.ts +38 -0
- package/dist/symbols/reference-finder.d.ts.map +1 -0
- package/dist/symbols/reference-finder.js +376 -0
- package/dist/symbols/reference-finder.js.map +1 -0
- package/dist/symbols/symbol-editor.d.ts +81 -0
- package/dist/symbols/symbol-editor.d.ts.map +1 -0
- package/dist/symbols/symbol-editor.js +257 -0
- package/dist/symbols/symbol-editor.js.map +1 -0
- package/dist/symbols/symbol-extractor.d.ts +49 -0
- package/dist/symbols/symbol-extractor.d.ts.map +1 -0
- package/dist/symbols/symbol-extractor.js +593 -0
- package/dist/symbols/symbol-extractor.js.map +1 -0
- package/dist/symbols/symbol-renamer.d.ts +81 -0
- package/dist/symbols/symbol-renamer.d.ts.map +1 -0
- package/dist/symbols/symbol-renamer.js +204 -0
- package/dist/symbols/symbol-renamer.js.map +1 -0
- package/dist/symbols/types.d.ts +234 -0
- package/dist/symbols/types.d.ts.map +1 -0
- package/dist/symbols/types.js +106 -0
- package/dist/symbols/types.js.map +1 -0
- package/dist/utils/concurrency.d.ts +32 -0
- package/dist/utils/concurrency.d.ts.map +1 -0
- package/dist/utils/concurrency.js +57 -0
- package/dist/utils/concurrency.js.map +1 -0
- package/dist/utils/errors.d.ts +36 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +91 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/type-guards.d.ts +17 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +25 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/dist/worktree/index.d.ts +6 -0
- package/dist/worktree/index.d.ts.map +1 -0
- package/dist/worktree/index.js +6 -0
- package/dist/worktree/index.js.map +1 -0
- package/dist/worktree/types.d.ts +101 -0
- package/dist/worktree/types.d.ts.map +1 -0
- package/dist/worktree/types.js +6 -0
- package/dist/worktree/types.js.map +1 -0
- package/dist/worktree/worktree-manager.d.ts +80 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.js +407 -0
- package/dist/worktree/worktree-manager.js.map +1 -0
- package/package.json +39 -5
- package/scripts/postinstall.js +48 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CodeIndexer } from '../../search/indexer.js';
|
|
3
|
+
import { createMockEmbeddingBackend } from '../mocks/embedding-backend.mock.js';
|
|
4
|
+
import { createMockConnection, createMockTable } from '../mocks/lancedb.mock.js';
|
|
5
|
+
// Mock the lancedb module
|
|
6
|
+
vi.mock('@lancedb/lancedb', () => ({
|
|
7
|
+
connect: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Mock fs/promises for file operations
|
|
10
|
+
vi.mock('fs/promises', () => ({
|
|
11
|
+
readFile: vi.fn(),
|
|
12
|
+
writeFile: vi.fn(),
|
|
13
|
+
stat: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
// Mock config
|
|
16
|
+
vi.mock('../../config.js', async (_importOriginal) => {
|
|
17
|
+
return {
|
|
18
|
+
loadConfig: vi
|
|
19
|
+
.fn()
|
|
20
|
+
.mockResolvedValue({ patterns: ['**/*.ts'], excludePatterns: ['**/node_modules/**'] }),
|
|
21
|
+
getDefaultPatterns: vi.fn().mockReturnValue(['**/*.ts', '**/*.js']),
|
|
22
|
+
getDefaultExcludePatterns: vi.fn().mockReturnValue(['**/node_modules/**']),
|
|
23
|
+
getChunkingConfig: vi.fn().mockReturnValue({ maxLines: 100, overlap: 20 }),
|
|
24
|
+
getSearchConfig: vi.fn().mockReturnValue({ semanticWeight: 0.7, keywordWeight: 0.3 }),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
describe('CodeIndexer', () => {
|
|
28
|
+
let mockBackend;
|
|
29
|
+
let mockConnection;
|
|
30
|
+
let lancedb;
|
|
31
|
+
let fsPromises;
|
|
32
|
+
let configModule;
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
mockBackend = createMockEmbeddingBackend();
|
|
35
|
+
mockConnection = createMockConnection();
|
|
36
|
+
lancedb = await import('@lancedb/lancedb');
|
|
37
|
+
fsPromises = await import('fs/promises');
|
|
38
|
+
configModule = await import('../../config.js');
|
|
39
|
+
vi.mocked(lancedb.connect).mockResolvedValue(mockConnection);
|
|
40
|
+
// Re-establish the config mock after resetAllMocks
|
|
41
|
+
vi.mocked(configModule.loadConfig).mockResolvedValue({
|
|
42
|
+
patterns: ['**/*.ts'],
|
|
43
|
+
excludePatterns: ['**/node_modules/**'],
|
|
44
|
+
chunking: { maxLines: 100, overlap: 20 },
|
|
45
|
+
search: { semanticWeight: 0.7, keywordWeight: 0.3 },
|
|
46
|
+
});
|
|
47
|
+
vi.mocked(configModule.getDefaultPatterns).mockReturnValue(['**/*.ts', '**/*.js']);
|
|
48
|
+
vi.mocked(configModule.getDefaultExcludePatterns).mockReturnValue(['**/node_modules/**']);
|
|
49
|
+
vi.mocked(configModule.getChunkingConfig).mockReturnValue({ maxLines: 100, overlap: 20 });
|
|
50
|
+
vi.mocked(configModule.getSearchConfig).mockReturnValue({
|
|
51
|
+
semanticWeight: 0.7,
|
|
52
|
+
keywordWeight: 0.3,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.resetAllMocks();
|
|
57
|
+
});
|
|
58
|
+
describe('constructor', () => {
|
|
59
|
+
it('should set correct index path', () => {
|
|
60
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
61
|
+
// Access private property for testing
|
|
62
|
+
expect(indexer.projectPath).toBe('/project');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('initialize', () => {
|
|
66
|
+
it('should connect to LanceDB', async () => {
|
|
67
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
68
|
+
await indexer.initialize();
|
|
69
|
+
expect(lancedb.connect).toHaveBeenCalledWith('/project/.lance-context');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('getStatus', () => {
|
|
73
|
+
it('should return indexed:false when no table exists', async () => {
|
|
74
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
75
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
76
|
+
await indexer.initialize();
|
|
77
|
+
const status = await indexer.getStatus();
|
|
78
|
+
expect(status.indexed).toBe(false);
|
|
79
|
+
expect(status.fileCount).toBe(0);
|
|
80
|
+
expect(status.chunkCount).toBe(0);
|
|
81
|
+
expect(status.lastUpdated).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('should return correct counts when indexed', async () => {
|
|
84
|
+
const mockTable = createMockTable([
|
|
85
|
+
{
|
|
86
|
+
id: '1',
|
|
87
|
+
filepath: 'test.ts',
|
|
88
|
+
content: 'code',
|
|
89
|
+
startLine: 1,
|
|
90
|
+
endLine: 10,
|
|
91
|
+
language: 'typescript',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: '2',
|
|
95
|
+
filepath: 'test.ts',
|
|
96
|
+
content: 'more',
|
|
97
|
+
startLine: 11,
|
|
98
|
+
endLine: 20,
|
|
99
|
+
language: 'typescript',
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
103
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
104
|
+
// Mock metadata file
|
|
105
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
106
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
107
|
+
fileCount: 5,
|
|
108
|
+
chunkCount: 10,
|
|
109
|
+
embeddingBackend: 'mock',
|
|
110
|
+
embeddingDimensions: 1536,
|
|
111
|
+
}));
|
|
112
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
113
|
+
await indexer.initialize();
|
|
114
|
+
const status = await indexer.getStatus();
|
|
115
|
+
expect(status.indexed).toBe(true);
|
|
116
|
+
expect(status.fileCount).toBe(5);
|
|
117
|
+
expect(status.chunkCount).toBe(2); // From table.countRows
|
|
118
|
+
expect(status.lastUpdated).toBe('2024-01-01T00:00:00Z');
|
|
119
|
+
expect(status.embeddingBackend).toBe('mock');
|
|
120
|
+
});
|
|
121
|
+
it('should handle missing metadata gracefully', async () => {
|
|
122
|
+
const mockTable = createMockTable([
|
|
123
|
+
{
|
|
124
|
+
id: '1',
|
|
125
|
+
filepath: 'test.ts',
|
|
126
|
+
content: 'code',
|
|
127
|
+
startLine: 1,
|
|
128
|
+
endLine: 10,
|
|
129
|
+
language: 'typescript',
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
133
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
134
|
+
// Metadata file doesn't exist
|
|
135
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
136
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
137
|
+
await indexer.initialize();
|
|
138
|
+
const status = await indexer.getStatus();
|
|
139
|
+
expect(status.indexed).toBe(true);
|
|
140
|
+
expect(status.fileCount).toBe(0);
|
|
141
|
+
expect(status.lastUpdated).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('search', () => {
|
|
145
|
+
it('should throw when not indexed', async () => {
|
|
146
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
147
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
148
|
+
await indexer.initialize();
|
|
149
|
+
await expect(indexer.search('query')).rejects.toThrow('Codebase not indexed');
|
|
150
|
+
});
|
|
151
|
+
it('should embed query via backend', async () => {
|
|
152
|
+
const mockTable = createMockTable([
|
|
153
|
+
{
|
|
154
|
+
id: '1',
|
|
155
|
+
filepath: 'test.ts',
|
|
156
|
+
content: 'function test',
|
|
157
|
+
startLine: 1,
|
|
158
|
+
endLine: 10,
|
|
159
|
+
language: 'typescript',
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
163
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
164
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
165
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
166
|
+
await indexer.initialize();
|
|
167
|
+
await indexer.search('find function');
|
|
168
|
+
expect(mockBackend.embed).toHaveBeenCalledWith('find function');
|
|
169
|
+
});
|
|
170
|
+
it('should respect limit parameter', async () => {
|
|
171
|
+
const chunks = Array(10)
|
|
172
|
+
.fill(null)
|
|
173
|
+
.map((_, i) => ({
|
|
174
|
+
id: `${i}`,
|
|
175
|
+
filepath: `test${i}.ts`,
|
|
176
|
+
content: `content ${i}`,
|
|
177
|
+
startLine: 1,
|
|
178
|
+
endLine: 10,
|
|
179
|
+
language: 'typescript',
|
|
180
|
+
}));
|
|
181
|
+
const mockTable = createMockTable(chunks);
|
|
182
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
183
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
184
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
185
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
186
|
+
await indexer.initialize();
|
|
187
|
+
const results = await indexer.search('query', 3);
|
|
188
|
+
expect(results.length).toBe(3);
|
|
189
|
+
});
|
|
190
|
+
it('should return results with correct structure', async () => {
|
|
191
|
+
const mockTable = createMockTable([
|
|
192
|
+
{
|
|
193
|
+
id: 'test.ts:1-10',
|
|
194
|
+
filepath: 'test.ts',
|
|
195
|
+
content: 'function hello() {}',
|
|
196
|
+
startLine: 1,
|
|
197
|
+
endLine: 10,
|
|
198
|
+
language: 'typescript',
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
202
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
203
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
204
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
205
|
+
await indexer.initialize();
|
|
206
|
+
const results = await indexer.search('hello');
|
|
207
|
+
expect(results[0]).toMatchObject({
|
|
208
|
+
id: 'test.ts:1-10',
|
|
209
|
+
filepath: 'test.ts',
|
|
210
|
+
content: 'function hello() {}',
|
|
211
|
+
startLine: 1,
|
|
212
|
+
endLine: 10,
|
|
213
|
+
language: 'typescript',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
it('should include symbol context when available', async () => {
|
|
217
|
+
const mockTable = createMockTable([
|
|
218
|
+
{
|
|
219
|
+
id: 'test.ts:1-10:hello',
|
|
220
|
+
filepath: 'test.ts',
|
|
221
|
+
content: 'function hello() {}',
|
|
222
|
+
startLine: 1,
|
|
223
|
+
endLine: 10,
|
|
224
|
+
language: 'typescript',
|
|
225
|
+
symbolType: 'function',
|
|
226
|
+
symbolName: 'hello',
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
230
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
231
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
232
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
233
|
+
await indexer.initialize();
|
|
234
|
+
const results = await indexer.search('hello');
|
|
235
|
+
expect(results[0]).toMatchObject({
|
|
236
|
+
id: 'test.ts:1-10:hello',
|
|
237
|
+
filepath: 'test.ts',
|
|
238
|
+
symbolType: 'function',
|
|
239
|
+
symbolName: 'hello',
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('query embedding cache', () => {
|
|
244
|
+
it('should cache query embeddings and not recompute for identical queries', async () => {
|
|
245
|
+
const mockTable = createMockTable([
|
|
246
|
+
{
|
|
247
|
+
id: 'test.ts:1-10',
|
|
248
|
+
filepath: 'test.ts',
|
|
249
|
+
content: 'function test() {}',
|
|
250
|
+
startLine: 1,
|
|
251
|
+
endLine: 10,
|
|
252
|
+
language: 'typescript',
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
256
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
257
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
258
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
259
|
+
await indexer.initialize();
|
|
260
|
+
// First search - should call embed
|
|
261
|
+
await indexer.search('test query');
|
|
262
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1);
|
|
263
|
+
// Second search with same query - should use cache
|
|
264
|
+
await indexer.search('test query');
|
|
265
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1); // Still 1, not 2
|
|
266
|
+
// Different query - should call embed again
|
|
267
|
+
await indexer.search('different query');
|
|
268
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(2);
|
|
269
|
+
});
|
|
270
|
+
it('should clear cache when index is cleared', async () => {
|
|
271
|
+
const mockTable = createMockTable([
|
|
272
|
+
{
|
|
273
|
+
id: 'test.ts:1-10',
|
|
274
|
+
filepath: 'test.ts',
|
|
275
|
+
content: 'function test() {}',
|
|
276
|
+
startLine: 1,
|
|
277
|
+
endLine: 10,
|
|
278
|
+
language: 'typescript',
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
282
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
283
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
284
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
285
|
+
await indexer.initialize();
|
|
286
|
+
// First search - populates cache
|
|
287
|
+
await indexer.search('cached query');
|
|
288
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1);
|
|
289
|
+
// Clear index - should clear cache
|
|
290
|
+
await indexer.clearIndex();
|
|
291
|
+
// Re-setup the mock table for next search
|
|
292
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
293
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
294
|
+
// Same query after clear - should recompute
|
|
295
|
+
await indexer.search('cached query');
|
|
296
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(2);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
describe('search filtering', () => {
|
|
300
|
+
it('should filter results by pathPattern glob', async () => {
|
|
301
|
+
const mockTable = createMockTable([
|
|
302
|
+
{
|
|
303
|
+
id: 'src/utils.ts:1-10',
|
|
304
|
+
filepath: 'src/utils.ts',
|
|
305
|
+
content: 'export function utility() {}',
|
|
306
|
+
startLine: 1,
|
|
307
|
+
endLine: 10,
|
|
308
|
+
language: 'typescript',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'test/utils.test.ts:1-10',
|
|
312
|
+
filepath: 'test/utils.test.ts',
|
|
313
|
+
content: 'describe("utility", () => {})',
|
|
314
|
+
startLine: 1,
|
|
315
|
+
endLine: 10,
|
|
316
|
+
language: 'typescript',
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: 'src/index.ts:1-10',
|
|
320
|
+
filepath: 'src/index.ts',
|
|
321
|
+
content: 'import { utility } from "./utils"',
|
|
322
|
+
startLine: 1,
|
|
323
|
+
endLine: 10,
|
|
324
|
+
language: 'typescript',
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
328
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
329
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
330
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
331
|
+
await indexer.initialize();
|
|
332
|
+
// Filter to only src directory
|
|
333
|
+
const results = await indexer.search({
|
|
334
|
+
query: 'utility',
|
|
335
|
+
pathPattern: 'src/**',
|
|
336
|
+
});
|
|
337
|
+
expect(results).toHaveLength(2);
|
|
338
|
+
expect(results.map((r) => r.filepath)).toEqual(['src/utils.ts', 'src/index.ts']);
|
|
339
|
+
});
|
|
340
|
+
it('should filter results by negation pathPattern', async () => {
|
|
341
|
+
const mockTable = createMockTable([
|
|
342
|
+
{
|
|
343
|
+
id: 'src/utils.ts:1-10',
|
|
344
|
+
filepath: 'src/utils.ts',
|
|
345
|
+
content: 'export function utility() {}',
|
|
346
|
+
startLine: 1,
|
|
347
|
+
endLine: 10,
|
|
348
|
+
language: 'typescript',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'test/utils.test.ts:1-10',
|
|
352
|
+
filepath: 'test/utils.test.ts',
|
|
353
|
+
content: 'describe("utility", () => {})',
|
|
354
|
+
startLine: 1,
|
|
355
|
+
endLine: 10,
|
|
356
|
+
language: 'typescript',
|
|
357
|
+
},
|
|
358
|
+
]);
|
|
359
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
360
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
361
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
362
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
363
|
+
await indexer.initialize();
|
|
364
|
+
// Exclude test files
|
|
365
|
+
const results = await indexer.search({
|
|
366
|
+
query: 'utility',
|
|
367
|
+
pathPattern: '!test/**',
|
|
368
|
+
});
|
|
369
|
+
expect(results).toHaveLength(1);
|
|
370
|
+
expect(results[0].filepath).toBe('src/utils.ts');
|
|
371
|
+
});
|
|
372
|
+
it('should filter results by languages array', async () => {
|
|
373
|
+
const mockTable = createMockTable([
|
|
374
|
+
{
|
|
375
|
+
id: 'app.ts:1-10',
|
|
376
|
+
filepath: 'app.ts',
|
|
377
|
+
content: 'const app = express()',
|
|
378
|
+
startLine: 1,
|
|
379
|
+
endLine: 10,
|
|
380
|
+
language: 'typescript',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'app.js:1-10',
|
|
384
|
+
filepath: 'app.js',
|
|
385
|
+
content: 'const app = require("express")()',
|
|
386
|
+
startLine: 1,
|
|
387
|
+
endLine: 10,
|
|
388
|
+
language: 'javascript',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: 'styles.css:1-10',
|
|
392
|
+
filepath: 'styles.css',
|
|
393
|
+
content: '.app { display: flex; }',
|
|
394
|
+
startLine: 1,
|
|
395
|
+
endLine: 10,
|
|
396
|
+
language: 'css',
|
|
397
|
+
},
|
|
398
|
+
]);
|
|
399
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
400
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
401
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
402
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
403
|
+
await indexer.initialize();
|
|
404
|
+
// Filter to only TypeScript files
|
|
405
|
+
const results = await indexer.search({
|
|
406
|
+
query: 'app',
|
|
407
|
+
languages: ['typescript'],
|
|
408
|
+
});
|
|
409
|
+
expect(results).toHaveLength(1);
|
|
410
|
+
expect(results[0].language).toBe('typescript');
|
|
411
|
+
});
|
|
412
|
+
it('should filter by multiple languages', async () => {
|
|
413
|
+
const mockTable = createMockTable([
|
|
414
|
+
{
|
|
415
|
+
id: 'app.ts:1-10',
|
|
416
|
+
filepath: 'app.ts',
|
|
417
|
+
content: 'const app = express()',
|
|
418
|
+
startLine: 1,
|
|
419
|
+
endLine: 10,
|
|
420
|
+
language: 'typescript',
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: 'app.js:1-10',
|
|
424
|
+
filepath: 'app.js',
|
|
425
|
+
content: 'const app = require("express")()',
|
|
426
|
+
startLine: 1,
|
|
427
|
+
endLine: 10,
|
|
428
|
+
language: 'javascript',
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: 'styles.css:1-10',
|
|
432
|
+
filepath: 'styles.css',
|
|
433
|
+
content: '.app { display: flex; }',
|
|
434
|
+
startLine: 1,
|
|
435
|
+
endLine: 10,
|
|
436
|
+
language: 'css',
|
|
437
|
+
},
|
|
438
|
+
]);
|
|
439
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
440
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
441
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
442
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
443
|
+
await indexer.initialize();
|
|
444
|
+
// Filter to TypeScript and JavaScript
|
|
445
|
+
const results = await indexer.search({
|
|
446
|
+
query: 'app',
|
|
447
|
+
languages: ['typescript', 'javascript'],
|
|
448
|
+
});
|
|
449
|
+
expect(results).toHaveLength(2);
|
|
450
|
+
expect(results.map((r) => r.language).sort()).toEqual(['javascript', 'typescript']);
|
|
451
|
+
});
|
|
452
|
+
it('should combine pathPattern and languages filters', async () => {
|
|
453
|
+
const mockTable = createMockTable([
|
|
454
|
+
{
|
|
455
|
+
id: 'src/app.ts:1-10',
|
|
456
|
+
filepath: 'src/app.ts',
|
|
457
|
+
content: 'const app = express()',
|
|
458
|
+
startLine: 1,
|
|
459
|
+
endLine: 10,
|
|
460
|
+
language: 'typescript',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'src/app.js:1-10',
|
|
464
|
+
filepath: 'src/app.js',
|
|
465
|
+
content: 'const app = require("express")()',
|
|
466
|
+
startLine: 1,
|
|
467
|
+
endLine: 10,
|
|
468
|
+
language: 'javascript',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: 'test/app.test.ts:1-10',
|
|
472
|
+
filepath: 'test/app.test.ts',
|
|
473
|
+
content: 'describe("app", () => {})',
|
|
474
|
+
startLine: 1,
|
|
475
|
+
endLine: 10,
|
|
476
|
+
language: 'typescript',
|
|
477
|
+
},
|
|
478
|
+
]);
|
|
479
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
480
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
481
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
482
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
483
|
+
await indexer.initialize();
|
|
484
|
+
// Filter to src directory AND TypeScript only
|
|
485
|
+
const results = await indexer.search({
|
|
486
|
+
query: 'app',
|
|
487
|
+
pathPattern: 'src/**',
|
|
488
|
+
languages: ['typescript'],
|
|
489
|
+
});
|
|
490
|
+
expect(results).toHaveLength(1);
|
|
491
|
+
expect(results[0].filepath).toBe('src/app.ts');
|
|
492
|
+
});
|
|
493
|
+
it('should handle case-insensitive language filtering', async () => {
|
|
494
|
+
const mockTable = createMockTable([
|
|
495
|
+
{
|
|
496
|
+
id: 'app.ts:1-10',
|
|
497
|
+
filepath: 'app.ts',
|
|
498
|
+
content: 'const app = express()',
|
|
499
|
+
startLine: 1,
|
|
500
|
+
endLine: 10,
|
|
501
|
+
language: 'TypeScript',
|
|
502
|
+
},
|
|
503
|
+
]);
|
|
504
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
505
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
506
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
507
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
508
|
+
await indexer.initialize();
|
|
509
|
+
// Use lowercase in filter
|
|
510
|
+
const results = await indexer.search({
|
|
511
|
+
query: 'app',
|
|
512
|
+
languages: ['typescript'],
|
|
513
|
+
});
|
|
514
|
+
expect(results).toHaveLength(1);
|
|
515
|
+
});
|
|
516
|
+
it('should support SearchOptions object syntax', async () => {
|
|
517
|
+
const mockTable = createMockTable([
|
|
518
|
+
{
|
|
519
|
+
id: 'test.ts:1-10',
|
|
520
|
+
filepath: 'test.ts',
|
|
521
|
+
content: 'function test() {}',
|
|
522
|
+
startLine: 1,
|
|
523
|
+
endLine: 10,
|
|
524
|
+
language: 'typescript',
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
528
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
529
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
530
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
531
|
+
await indexer.initialize();
|
|
532
|
+
// Use SearchOptions object
|
|
533
|
+
const results = await indexer.search({
|
|
534
|
+
query: 'test',
|
|
535
|
+
limit: 5,
|
|
536
|
+
});
|
|
537
|
+
expect(results).toHaveLength(1);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
describe('clearIndex', () => {
|
|
541
|
+
it('should drop table when it exists', async () => {
|
|
542
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
543
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
544
|
+
await indexer.initialize();
|
|
545
|
+
await indexer.clearIndex();
|
|
546
|
+
expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
|
|
547
|
+
});
|
|
548
|
+
it('should handle non-existent table gracefully', async () => {
|
|
549
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
550
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
551
|
+
await indexer.initialize();
|
|
552
|
+
// Should not throw
|
|
553
|
+
await expect(indexer.clearIndex()).resolves.toBeUndefined();
|
|
554
|
+
expect(mockConnection.dropTable).not.toHaveBeenCalled();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
describe('searchSimilar', () => {
|
|
558
|
+
it('should find similar code with code snippet input', async () => {
|
|
559
|
+
const chunks = [
|
|
560
|
+
{
|
|
561
|
+
id: 'file1.ts:1-10',
|
|
562
|
+
filepath: 'file1.ts',
|
|
563
|
+
content: 'function authenticate(user) { return true; }',
|
|
564
|
+
startLine: 1,
|
|
565
|
+
endLine: 10,
|
|
566
|
+
language: 'typescript',
|
|
567
|
+
_distance: 0.1,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: 'file2.ts:1-10',
|
|
571
|
+
filepath: 'file2.ts',
|
|
572
|
+
content: 'function validate(input) { return false; }',
|
|
573
|
+
startLine: 1,
|
|
574
|
+
endLine: 10,
|
|
575
|
+
language: 'typescript',
|
|
576
|
+
_distance: 0.5,
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
const mockTable = createMockTable(chunks);
|
|
580
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
581
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
582
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
583
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
584
|
+
await indexer.initialize();
|
|
585
|
+
const results = await indexer.searchSimilar({
|
|
586
|
+
code: 'function login(user) { return true; }',
|
|
587
|
+
limit: 10,
|
|
588
|
+
});
|
|
589
|
+
expect(results.length).toBe(2);
|
|
590
|
+
expect(results[0].filepath).toBe('file1.ts');
|
|
591
|
+
expect(results[0]).toHaveProperty('similarity');
|
|
592
|
+
expect(mockBackend.embed).toHaveBeenCalledWith('function login(user) { return true; }');
|
|
593
|
+
});
|
|
594
|
+
it('should exclude self when excludeSelf is true', async () => {
|
|
595
|
+
const chunks = [
|
|
596
|
+
{
|
|
597
|
+
id: 'file1.ts:1-10',
|
|
598
|
+
filepath: 'file1.ts',
|
|
599
|
+
content: 'function test() {}',
|
|
600
|
+
startLine: 1,
|
|
601
|
+
endLine: 10,
|
|
602
|
+
language: 'typescript',
|
|
603
|
+
_distance: 0,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
id: 'file2.ts:1-10',
|
|
607
|
+
filepath: 'file2.ts',
|
|
608
|
+
content: 'function other() {}',
|
|
609
|
+
startLine: 1,
|
|
610
|
+
endLine: 10,
|
|
611
|
+
language: 'typescript',
|
|
612
|
+
_distance: 0.3,
|
|
613
|
+
},
|
|
614
|
+
];
|
|
615
|
+
const mockTable = createMockTable(chunks);
|
|
616
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
617
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
618
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
619
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
620
|
+
await indexer.initialize();
|
|
621
|
+
// Should exclude exact content match
|
|
622
|
+
const results = await indexer.searchSimilar({
|
|
623
|
+
code: 'function test() {}',
|
|
624
|
+
excludeSelf: true,
|
|
625
|
+
});
|
|
626
|
+
expect(results.every((r) => r.content.trim() !== 'function test() {}')).toBe(true);
|
|
627
|
+
});
|
|
628
|
+
it('should throw error when neither code nor filepath is provided', async () => {
|
|
629
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
630
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
631
|
+
await indexer.initialize();
|
|
632
|
+
await expect(indexer.searchSimilar({})).rejects.toThrow('Either code or filepath must be provided');
|
|
633
|
+
});
|
|
634
|
+
it('should respect threshold parameter', async () => {
|
|
635
|
+
const chunks = [
|
|
636
|
+
{
|
|
637
|
+
id: 'file1.ts:1-10',
|
|
638
|
+
filepath: 'file1.ts',
|
|
639
|
+
content: 'high similarity',
|
|
640
|
+
startLine: 1,
|
|
641
|
+
endLine: 10,
|
|
642
|
+
language: 'typescript',
|
|
643
|
+
_distance: 0.1,
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: 'file2.ts:1-10',
|
|
647
|
+
filepath: 'file2.ts',
|
|
648
|
+
content: 'low similarity',
|
|
649
|
+
startLine: 1,
|
|
650
|
+
endLine: 10,
|
|
651
|
+
language: 'typescript',
|
|
652
|
+
_distance: 0.9,
|
|
653
|
+
},
|
|
654
|
+
];
|
|
655
|
+
const mockTable = createMockTable(chunks);
|
|
656
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
657
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
658
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
659
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
660
|
+
await indexer.initialize();
|
|
661
|
+
// With high threshold, should filter out low similarity results
|
|
662
|
+
const results = await indexer.searchSimilar({
|
|
663
|
+
code: 'test code',
|
|
664
|
+
threshold: 0.5,
|
|
665
|
+
});
|
|
666
|
+
// Only high similarity result should pass
|
|
667
|
+
expect(results.length).toBe(1);
|
|
668
|
+
expect(results[0].filepath).toBe('file1.ts');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
describe('indexCodebase', () => {
|
|
672
|
+
beforeEach(() => {
|
|
673
|
+
// Mock glob
|
|
674
|
+
vi.doMock('glob', () => ({
|
|
675
|
+
glob: vi.fn().mockResolvedValue([]),
|
|
676
|
+
}));
|
|
677
|
+
// Mock file stats
|
|
678
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
679
|
+
vi.mocked(fsPromises.writeFile).mockResolvedValue();
|
|
680
|
+
});
|
|
681
|
+
it('should use provided patterns over config', async () => {
|
|
682
|
+
const { glob } = await import('glob');
|
|
683
|
+
vi.mocked(glob).mockResolvedValue([]);
|
|
684
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
685
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
686
|
+
await indexer.initialize();
|
|
687
|
+
await indexer.indexCodebase(['**/*.py'], ['**/venv/**']);
|
|
688
|
+
expect(glob).toHaveBeenCalledWith('**/*.py', expect.objectContaining({
|
|
689
|
+
ignore: ['**/venv/**'],
|
|
690
|
+
}));
|
|
691
|
+
});
|
|
692
|
+
it('should detect incremental vs full indexing', async () => {
|
|
693
|
+
const { glob } = await import('glob');
|
|
694
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
695
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
|
|
696
|
+
if (path.includes('index-metadata')) {
|
|
697
|
+
return JSON.stringify({
|
|
698
|
+
lastUpdated: '2024-01-01',
|
|
699
|
+
fileCount: 1,
|
|
700
|
+
chunkCount: 1,
|
|
701
|
+
embeddingBackend: 'mock',
|
|
702
|
+
embeddingDimensions: 1536,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return 'const x = 1;';
|
|
706
|
+
});
|
|
707
|
+
// Has existing index
|
|
708
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
709
|
+
const mockTable = createMockTable([]);
|
|
710
|
+
const mockMetadataTable = createMockTable([]);
|
|
711
|
+
mockMetadataTable.query = vi.fn().mockReturnValue({
|
|
712
|
+
toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: Date.now() }]),
|
|
713
|
+
});
|
|
714
|
+
mockConnection.openTable.mockImplementation(async (name) => {
|
|
715
|
+
if (name === 'file_metadata')
|
|
716
|
+
return mockMetadataTable;
|
|
717
|
+
return mockTable;
|
|
718
|
+
});
|
|
719
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
720
|
+
await indexer.initialize();
|
|
721
|
+
const result = await indexer.indexCodebase();
|
|
722
|
+
expect(result.incremental).toBe(true);
|
|
723
|
+
});
|
|
724
|
+
it('should handle force reindex flag', async () => {
|
|
725
|
+
const { glob } = await import('glob');
|
|
726
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
727
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
728
|
+
// Has existing index
|
|
729
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
730
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
731
|
+
await indexer.initialize();
|
|
732
|
+
const result = await indexer.indexCodebase(undefined, undefined, true);
|
|
733
|
+
expect(result.incremental).toBe(false);
|
|
734
|
+
expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
|
|
735
|
+
});
|
|
736
|
+
it('should report progress via callback', async () => {
|
|
737
|
+
const { glob } = await import('glob');
|
|
738
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
739
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
740
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
741
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
742
|
+
await indexer.initialize();
|
|
743
|
+
const progressUpdates = [];
|
|
744
|
+
await indexer.indexCodebase(undefined, undefined, false, (progress) => {
|
|
745
|
+
progressUpdates.push(progress);
|
|
746
|
+
});
|
|
747
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
748
|
+
expect(progressUpdates.some((p) => p.phase === 'scanning')).toBe(true);
|
|
749
|
+
expect(progressUpdates.some((p) => p.phase === 'chunking')).toBe(true);
|
|
750
|
+
expect(progressUpdates.some((p) => p.phase === 'embedding')).toBe(true);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
describe('hybrid scoring', () => {
|
|
754
|
+
it('should apply 70/30 semantic/keyword split', async () => {
|
|
755
|
+
// This tests the calculateKeywordScore indirectly through search
|
|
756
|
+
const mockTable = createMockTable([
|
|
757
|
+
{
|
|
758
|
+
id: '1',
|
|
759
|
+
filepath: 'auth.ts',
|
|
760
|
+
content: 'function authenticate() {}',
|
|
761
|
+
startLine: 1,
|
|
762
|
+
endLine: 1,
|
|
763
|
+
language: 'typescript',
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
id: '2',
|
|
767
|
+
filepath: 'other.ts',
|
|
768
|
+
content: 'function other() {}',
|
|
769
|
+
startLine: 1,
|
|
770
|
+
endLine: 1,
|
|
771
|
+
language: 'typescript',
|
|
772
|
+
},
|
|
773
|
+
]);
|
|
774
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
775
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
776
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
777
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
778
|
+
await indexer.initialize();
|
|
779
|
+
const results = await indexer.search('authenticate auth');
|
|
780
|
+
// auth.ts should rank higher due to keyword match in both content and filepath
|
|
781
|
+
expect(results[0].filepath).toBe('auth.ts');
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
describe('corruption detection', () => {
|
|
785
|
+
it('should detect missing metadata as corruption', async () => {
|
|
786
|
+
const mockTable = createMockTable([
|
|
787
|
+
{
|
|
788
|
+
id: '1',
|
|
789
|
+
filepath: 'test.ts',
|
|
790
|
+
content: 'code',
|
|
791
|
+
startLine: 1,
|
|
792
|
+
endLine: 10,
|
|
793
|
+
language: 'typescript',
|
|
794
|
+
},
|
|
795
|
+
]);
|
|
796
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
797
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
798
|
+
// Metadata file doesn't exist
|
|
799
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
800
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
801
|
+
await indexer.initialize();
|
|
802
|
+
const status = await indexer.getStatus();
|
|
803
|
+
expect(status.corrupted).toBe(true);
|
|
804
|
+
expect(status.corruptionReason).toContain('Missing index metadata');
|
|
805
|
+
});
|
|
806
|
+
it('should detect chunk count mismatch as corruption', async () => {
|
|
807
|
+
const mockTable = createMockTable([
|
|
808
|
+
{
|
|
809
|
+
id: '1',
|
|
810
|
+
filepath: 'test.ts',
|
|
811
|
+
content: 'code',
|
|
812
|
+
startLine: 1,
|
|
813
|
+
endLine: 10,
|
|
814
|
+
language: 'typescript',
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
818
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
819
|
+
// Metadata says 5 chunks but table only has 1
|
|
820
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
821
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
822
|
+
fileCount: 1,
|
|
823
|
+
chunkCount: 5,
|
|
824
|
+
embeddingBackend: 'mock',
|
|
825
|
+
embeddingDimensions: 1536,
|
|
826
|
+
checksum: 'abc123',
|
|
827
|
+
}));
|
|
828
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
829
|
+
await indexer.initialize();
|
|
830
|
+
const status = await indexer.getStatus();
|
|
831
|
+
expect(status.corrupted).toBe(true);
|
|
832
|
+
expect(status.corruptionReason).toContain('Chunk count mismatch');
|
|
833
|
+
});
|
|
834
|
+
it('should report healthy index when metadata matches', async () => {
|
|
835
|
+
const mockTable = createMockTable([
|
|
836
|
+
{
|
|
837
|
+
id: '1',
|
|
838
|
+
filepath: 'test.ts',
|
|
839
|
+
content: 'code',
|
|
840
|
+
startLine: 1,
|
|
841
|
+
endLine: 10,
|
|
842
|
+
language: 'typescript',
|
|
843
|
+
},
|
|
844
|
+
]);
|
|
845
|
+
const mockMetadataTable = createMockTable([{ filepath: 'test.ts', mtime: Date.now() }]);
|
|
846
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
847
|
+
mockConnection.openTable.mockImplementation(async (name) => {
|
|
848
|
+
if (name === 'file_metadata')
|
|
849
|
+
return mockMetadataTable;
|
|
850
|
+
return mockTable;
|
|
851
|
+
});
|
|
852
|
+
// Metadata matches actual state
|
|
853
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
854
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
855
|
+
fileCount: 1,
|
|
856
|
+
chunkCount: 1,
|
|
857
|
+
embeddingBackend: 'mock',
|
|
858
|
+
embeddingDimensions: 1536,
|
|
859
|
+
checksum: '4694592dbdda93c1',
|
|
860
|
+
}));
|
|
861
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
862
|
+
await indexer.initialize();
|
|
863
|
+
const status = await indexer.getStatus();
|
|
864
|
+
expect(status.corrupted).toBe(false);
|
|
865
|
+
expect(status.corruptionReason).toBeUndefined();
|
|
866
|
+
});
|
|
867
|
+
it('should report no corruption when index is empty', async () => {
|
|
868
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
869
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
870
|
+
await indexer.initialize();
|
|
871
|
+
const status = await indexer.getStatus();
|
|
872
|
+
expect(status.indexed).toBe(false);
|
|
873
|
+
// Empty index has no corruption field (undefined, not false)
|
|
874
|
+
expect(status.corrupted).toBeUndefined();
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
//# sourceMappingURL=indexer.test.js.map
|