glancey 2.0.0
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/LICENSE +21 -0
- package/README.md +435 -0
- 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__/dashboard/token-tracking.test.d.ts +2 -0
- package/dist/__tests__/dashboard/token-tracking.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/token-tracking.test.js +315 -0
- package/dist/__tests__/dashboard/token-tracking.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 +157 -0
- package/dist/__tests__/embeddings/factory.test.js.map +1 -0
- package/dist/__tests__/embeddings/gemini.test.d.ts +2 -0
- package/dist/__tests__/embeddings/gemini.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/gemini.test.js +178 -0
- package/dist/__tests__/embeddings/gemini.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 +273 -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 +334 -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 +49 -0
- package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/fetch.mock.js +102 -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 +279 -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 +1305 -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__/tools/clustering-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/clustering-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/clustering-handlers.test.js +286 -0
- package/dist/__tests__/tools/clustering-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/commit-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/commit-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/commit-handlers.test.js +153 -0
- package/dist/__tests__/tools/commit-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/index-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/index-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/index-handlers.test.js +184 -0
- package/dist/__tests__/tools/index-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/instructions-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/instructions-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/instructions-handlers.test.js +53 -0
- package/dist/__tests__/tools/instructions-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/memory-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/memory-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/memory-handlers.test.js +175 -0
- package/dist/__tests__/tools/memory-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/search-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/search-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/search-handlers.test.js +229 -0
- package/dist/__tests__/tools/search-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/symbol-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/symbol-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/symbol-handlers.test.js +348 -0
- package/dist/__tests__/tools/symbol-handlers.test.js.map +1 -0
- package/dist/__tests__/tools/worktree-handlers.test.d.ts +2 -0
- package/dist/__tests__/tools/worktree-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/worktree-handlers.test.js +297 -0
- package/dist/__tests__/tools/worktree-handlers.test.js.map +1 -0
- package/dist/__tests__/utils/cache.test.d.ts +2 -0
- package/dist/__tests__/utils/cache.test.d.ts.map +1 -0
- package/dist/__tests__/utils/cache.test.js +147 -0
- package/dist/__tests__/utils/cache.test.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/logger.test.d.ts +2 -0
- package/dist/__tests__/utils/logger.test.d.ts.map +1 -0
- package/dist/__tests__/utils/logger.test.js +131 -0
- package/dist/__tests__/utils/logger.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 +191 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +788 -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 +64 -0
- package/dist/dashboard/events.d.ts.map +1 -0
- package/dist/dashboard/events.js +211 -0
- package/dist/dashboard/events.js.map +1 -0
- package/dist/dashboard/index.d.ts +69 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +93 -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 +505 -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 +172 -0
- package/dist/dashboard/state.d.ts.map +1 -0
- package/dist/dashboard/state.js +362 -0
- package/dist/dashboard/state.js.map +1 -0
- package/dist/dashboard/token-tracking.d.ts +143 -0
- package/dist/dashboard/token-tracking.d.ts.map +1 -0
- package/dist/dashboard/token-tracking.js +363 -0
- package/dist/dashboard/token-tracking.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 +2710 -0
- package/dist/dashboard/ui.js.map +1 -0
- package/dist/embeddings/gemini.d.ts +28 -0
- package/dist/embeddings/gemini.d.ts.map +1 -0
- package/dist/embeddings/gemini.js +165 -0
- package/dist/embeddings/gemini.js.map +1 -0
- package/dist/embeddings/index.d.ts +34 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +120 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/ollama.d.ts +22 -0
- package/dist/embeddings/ollama.d.ts.map +1 -0
- package/dist/embeddings/ollama.js +179 -0
- package/dist/embeddings/ollama.js.map +1 -0
- 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 +20 -0
- package/dist/embeddings/retry.d.ts.map +1 -0
- package/dist/embeddings/retry.js +227 -0
- package/dist/embeddings/retry.js.map +1 -0
- package/dist/embeddings/types.d.ts +103 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +23 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2028 -0
- package/dist/index.js.map +1 -0
- 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 +29 -0
- package/dist/search/ast-chunker.d.ts.map +1 -0
- package/dist/search/ast-chunker.js +236 -0
- package/dist/search/ast-chunker.js.map +1 -0
- package/dist/search/chunk-utils.d.ts +24 -0
- package/dist/search/chunk-utils.d.ts.map +1 -0
- package/dist/search/chunk-utils.js +41 -0
- package/dist/search/chunk-utils.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 +377 -0
- package/dist/search/indexer.d.ts.map +1 -0
- package/dist/search/indexer.js +1557 -0
- package/dist/search/indexer.js.map +1 -0
- package/dist/search/tree-sitter-chunker.d.ts +64 -0
- package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
- package/dist/search/tree-sitter-chunker.js +412 -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/tools/clustering-handlers.d.ts +70 -0
- package/dist/tools/clustering-handlers.d.ts.map +1 -0
- package/dist/tools/clustering-handlers.js +150 -0
- package/dist/tools/clustering-handlers.js.map +1 -0
- package/dist/tools/commit-handlers.d.ts +77 -0
- package/dist/tools/commit-handlers.d.ts.map +1 -0
- package/dist/tools/commit-handlers.js +251 -0
- package/dist/tools/commit-handlers.js.map +1 -0
- package/dist/tools/index-handlers.d.ts +31 -0
- package/dist/tools/index-handlers.d.ts.map +1 -0
- package/dist/tools/index-handlers.js +53 -0
- package/dist/tools/index-handlers.js.map +1 -0
- package/dist/tools/instructions-handlers.d.ts +25 -0
- package/dist/tools/instructions-handlers.d.ts.map +1 -0
- package/dist/tools/instructions-handlers.js +36 -0
- package/dist/tools/instructions-handlers.js.map +1 -0
- package/dist/tools/memory-handlers.d.ts +95 -0
- package/dist/tools/memory-handlers.d.ts.map +1 -0
- package/dist/tools/memory-handlers.js +126 -0
- package/dist/tools/memory-handlers.js.map +1 -0
- package/dist/tools/search-handlers.d.ts +51 -0
- package/dist/tools/search-handlers.d.ts.map +1 -0
- package/dist/tools/search-handlers.js +128 -0
- package/dist/tools/search-handlers.js.map +1 -0
- package/dist/tools/symbol-handlers.d.ts +171 -0
- package/dist/tools/symbol-handlers.d.ts.map +1 -0
- package/dist/tools/symbol-handlers.js +456 -0
- package/dist/tools/symbol-handlers.js.map +1 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +17 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/worktree-handlers.d.ts +96 -0
- package/dist/tools/worktree-handlers.d.ts.map +1 -0
- package/dist/tools/worktree-handlers.js +186 -0
- package/dist/tools/worktree-handlers.js.map +1 -0
- package/dist/utils/cache.d.ts +77 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +134 -0
- package/dist/utils/cache.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/logger.d.ts +37 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +114 -0
- package/dist/utils/logger.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 +87 -0
- package/scripts/postinstall.js +48 -0
|
@@ -0,0 +1,1305 @@
|
|
|
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
|
+
mkdir: vi.fn(),
|
|
15
|
+
unlink: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
// Mock config
|
|
18
|
+
vi.mock('../../config.js', async (_importOriginal) => {
|
|
19
|
+
return {
|
|
20
|
+
loadConfig: vi
|
|
21
|
+
.fn()
|
|
22
|
+
.mockResolvedValue({ patterns: ['**/*.ts'], excludePatterns: ['**/node_modules/**'] }),
|
|
23
|
+
getDefaultPatterns: vi.fn().mockReturnValue(['**/*.ts', '**/*.js']),
|
|
24
|
+
getDefaultExcludePatterns: vi.fn().mockReturnValue(['**/node_modules/**']),
|
|
25
|
+
getChunkingConfig: vi.fn().mockReturnValue({ maxLines: 100, overlap: 20 }),
|
|
26
|
+
getSearchConfig: vi.fn().mockReturnValue({ semanticWeight: 0.7, keywordWeight: 0.3 }),
|
|
27
|
+
getIndexingConfig: vi.fn().mockReturnValue({ batchSize: 32, batchDelayMs: 0 }),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
describe('CodeIndexer', () => {
|
|
31
|
+
let mockBackend;
|
|
32
|
+
let mockConnection;
|
|
33
|
+
let lancedb;
|
|
34
|
+
let fsPromises;
|
|
35
|
+
let configModule;
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
mockBackend = createMockEmbeddingBackend();
|
|
38
|
+
mockConnection = createMockConnection();
|
|
39
|
+
lancedb = await import('@lancedb/lancedb');
|
|
40
|
+
fsPromises = await import('fs/promises');
|
|
41
|
+
configModule = await import('../../config.js');
|
|
42
|
+
vi.mocked(lancedb.connect).mockResolvedValue(mockConnection);
|
|
43
|
+
// Re-establish the config mock after resetAllMocks
|
|
44
|
+
vi.mocked(configModule.loadConfig).mockResolvedValue({
|
|
45
|
+
patterns: ['**/*.ts'],
|
|
46
|
+
excludePatterns: ['**/node_modules/**'],
|
|
47
|
+
chunking: { maxLines: 100, overlap: 20 },
|
|
48
|
+
search: { semanticWeight: 0.7, keywordWeight: 0.3 },
|
|
49
|
+
});
|
|
50
|
+
vi.mocked(configModule.getDefaultPatterns).mockReturnValue(['**/*.ts', '**/*.js']);
|
|
51
|
+
vi.mocked(configModule.getDefaultExcludePatterns).mockReturnValue(['**/node_modules/**']);
|
|
52
|
+
vi.mocked(configModule.getChunkingConfig).mockReturnValue({ maxLines: 100, overlap: 20 });
|
|
53
|
+
vi.mocked(configModule.getSearchConfig).mockReturnValue({
|
|
54
|
+
semanticWeight: 0.7,
|
|
55
|
+
keywordWeight: 0.3,
|
|
56
|
+
autoReindex: false, // Disable for tests to avoid reindexing during search
|
|
57
|
+
});
|
|
58
|
+
vi.mocked(configModule.getIndexingConfig).mockReturnValue({ batchSize: 32, batchDelayMs: 0 });
|
|
59
|
+
});
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.resetAllMocks();
|
|
62
|
+
});
|
|
63
|
+
describe('constructor', () => {
|
|
64
|
+
it('should set correct index path', () => {
|
|
65
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
66
|
+
// Access private property for testing
|
|
67
|
+
expect(indexer.projectPath).toBe('/project');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('initialize', () => {
|
|
71
|
+
it('should connect to LanceDB', async () => {
|
|
72
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
73
|
+
await indexer.initialize();
|
|
74
|
+
expect(lancedb.connect).toHaveBeenCalledWith('/project/.glancey');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('getStatus', () => {
|
|
78
|
+
it('should return indexed:false when no table exists', async () => {
|
|
79
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
80
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
81
|
+
await indexer.initialize();
|
|
82
|
+
const status = await indexer.getStatus();
|
|
83
|
+
expect(status.indexed).toBe(false);
|
|
84
|
+
expect(status.fileCount).toBe(0);
|
|
85
|
+
expect(status.chunkCount).toBe(0);
|
|
86
|
+
expect(status.lastUpdated).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
it('should return correct counts when indexed', async () => {
|
|
89
|
+
const mockTable = createMockTable([
|
|
90
|
+
{
|
|
91
|
+
id: '1',
|
|
92
|
+
filepath: 'test.ts',
|
|
93
|
+
content: 'code',
|
|
94
|
+
startLine: 1,
|
|
95
|
+
endLine: 10,
|
|
96
|
+
language: 'typescript',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: '2',
|
|
100
|
+
filepath: 'test.ts',
|
|
101
|
+
content: 'more',
|
|
102
|
+
startLine: 11,
|
|
103
|
+
endLine: 20,
|
|
104
|
+
language: 'typescript',
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
108
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
109
|
+
// Mock metadata file
|
|
110
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
111
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
112
|
+
fileCount: 5,
|
|
113
|
+
chunkCount: 10,
|
|
114
|
+
embeddingBackend: 'mock',
|
|
115
|
+
embeddingDimensions: 1536,
|
|
116
|
+
}));
|
|
117
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
118
|
+
await indexer.initialize();
|
|
119
|
+
const status = await indexer.getStatus();
|
|
120
|
+
expect(status.indexed).toBe(true);
|
|
121
|
+
expect(status.fileCount).toBe(5);
|
|
122
|
+
expect(status.chunkCount).toBe(2); // From table.countRows
|
|
123
|
+
expect(status.lastUpdated).toBe('2024-01-01T00:00:00Z');
|
|
124
|
+
expect(status.embeddingBackend).toBe('mock');
|
|
125
|
+
});
|
|
126
|
+
it('should handle missing metadata gracefully', async () => {
|
|
127
|
+
const mockTable = createMockTable([
|
|
128
|
+
{
|
|
129
|
+
id: '1',
|
|
130
|
+
filepath: 'test.ts',
|
|
131
|
+
content: 'code',
|
|
132
|
+
startLine: 1,
|
|
133
|
+
endLine: 10,
|
|
134
|
+
language: 'typescript',
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
138
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
139
|
+
// Metadata file doesn't exist
|
|
140
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
141
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
142
|
+
await indexer.initialize();
|
|
143
|
+
const status = await indexer.getStatus();
|
|
144
|
+
expect(status.indexed).toBe(true);
|
|
145
|
+
expect(status.fileCount).toBe(0);
|
|
146
|
+
expect(status.lastUpdated).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('search', () => {
|
|
150
|
+
it('should throw when not indexed', async () => {
|
|
151
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
152
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
153
|
+
await indexer.initialize();
|
|
154
|
+
await expect(indexer.search('query')).rejects.toThrow('Codebase not indexed');
|
|
155
|
+
});
|
|
156
|
+
it('should embed query via backend', async () => {
|
|
157
|
+
const mockTable = createMockTable([
|
|
158
|
+
{
|
|
159
|
+
id: '1',
|
|
160
|
+
filepath: 'test.ts',
|
|
161
|
+
content: 'function test',
|
|
162
|
+
startLine: 1,
|
|
163
|
+
endLine: 10,
|
|
164
|
+
language: 'typescript',
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
168
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
169
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
170
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
171
|
+
await indexer.initialize();
|
|
172
|
+
await indexer.search('find function');
|
|
173
|
+
expect(mockBackend.embed).toHaveBeenCalledWith('find function');
|
|
174
|
+
});
|
|
175
|
+
it('should respect limit parameter', async () => {
|
|
176
|
+
const chunks = Array(10)
|
|
177
|
+
.fill(null)
|
|
178
|
+
.map((_, i) => ({
|
|
179
|
+
id: `${i}`,
|
|
180
|
+
filepath: `test${i}.ts`,
|
|
181
|
+
content: `content ${i}`,
|
|
182
|
+
startLine: 1,
|
|
183
|
+
endLine: 10,
|
|
184
|
+
language: 'typescript',
|
|
185
|
+
}));
|
|
186
|
+
const mockTable = createMockTable(chunks);
|
|
187
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
188
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
189
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
190
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
191
|
+
await indexer.initialize();
|
|
192
|
+
const results = await indexer.search('query', 3);
|
|
193
|
+
expect(results.length).toBe(3);
|
|
194
|
+
});
|
|
195
|
+
it('should return results with correct structure', async () => {
|
|
196
|
+
const mockTable = createMockTable([
|
|
197
|
+
{
|
|
198
|
+
id: 'test.ts:1-10',
|
|
199
|
+
filepath: 'test.ts',
|
|
200
|
+
content: 'function hello() {}',
|
|
201
|
+
startLine: 1,
|
|
202
|
+
endLine: 10,
|
|
203
|
+
language: 'typescript',
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
207
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
208
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
209
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
210
|
+
await indexer.initialize();
|
|
211
|
+
const results = await indexer.search('hello');
|
|
212
|
+
expect(results[0]).toMatchObject({
|
|
213
|
+
id: 'test.ts:1-10',
|
|
214
|
+
filepath: 'test.ts',
|
|
215
|
+
content: 'function hello() {}',
|
|
216
|
+
startLine: 1,
|
|
217
|
+
endLine: 10,
|
|
218
|
+
language: 'typescript',
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
it('should include symbol context when available', async () => {
|
|
222
|
+
const mockTable = createMockTable([
|
|
223
|
+
{
|
|
224
|
+
id: 'test.ts:1-10:hello',
|
|
225
|
+
filepath: 'test.ts',
|
|
226
|
+
content: 'function hello() {}',
|
|
227
|
+
startLine: 1,
|
|
228
|
+
endLine: 10,
|
|
229
|
+
language: 'typescript',
|
|
230
|
+
symbolType: 'function',
|
|
231
|
+
symbolName: 'hello',
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
235
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
236
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
237
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
238
|
+
await indexer.initialize();
|
|
239
|
+
const results = await indexer.search('hello');
|
|
240
|
+
expect(results[0]).toMatchObject({
|
|
241
|
+
id: 'test.ts:1-10:hello',
|
|
242
|
+
filepath: 'test.ts',
|
|
243
|
+
symbolType: 'function',
|
|
244
|
+
symbolName: 'hello',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('query embedding cache', () => {
|
|
249
|
+
it('should cache query embeddings and not recompute for identical queries', async () => {
|
|
250
|
+
const mockTable = createMockTable([
|
|
251
|
+
{
|
|
252
|
+
id: 'test.ts:1-10',
|
|
253
|
+
filepath: 'test.ts',
|
|
254
|
+
content: 'function test() {}',
|
|
255
|
+
startLine: 1,
|
|
256
|
+
endLine: 10,
|
|
257
|
+
language: 'typescript',
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
261
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
262
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
263
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
264
|
+
await indexer.initialize();
|
|
265
|
+
// First search - should call embed
|
|
266
|
+
await indexer.search('test query');
|
|
267
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1);
|
|
268
|
+
// Second search with same query - should use cache
|
|
269
|
+
await indexer.search('test query');
|
|
270
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1); // Still 1, not 2
|
|
271
|
+
// Different query - should call embed again
|
|
272
|
+
await indexer.search('different query');
|
|
273
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(2);
|
|
274
|
+
});
|
|
275
|
+
it('should clear cache when index is cleared', async () => {
|
|
276
|
+
const mockTable = createMockTable([
|
|
277
|
+
{
|
|
278
|
+
id: 'test.ts:1-10',
|
|
279
|
+
filepath: 'test.ts',
|
|
280
|
+
content: 'function test() {}',
|
|
281
|
+
startLine: 1,
|
|
282
|
+
endLine: 10,
|
|
283
|
+
language: 'typescript',
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
287
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
288
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
289
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
290
|
+
await indexer.initialize();
|
|
291
|
+
// First search - populates cache
|
|
292
|
+
await indexer.search('cached query');
|
|
293
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(1);
|
|
294
|
+
// Clear index - should clear cache
|
|
295
|
+
await indexer.clearIndex();
|
|
296
|
+
// Re-setup the mock table for next search
|
|
297
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
298
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
299
|
+
// Same query after clear - should recompute
|
|
300
|
+
await indexer.search('cached query');
|
|
301
|
+
expect(mockBackend.embed).toHaveBeenCalledTimes(2);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('search filtering', () => {
|
|
305
|
+
it('should filter results by pathPattern glob', async () => {
|
|
306
|
+
const mockTable = createMockTable([
|
|
307
|
+
{
|
|
308
|
+
id: 'src/utils.ts:1-10',
|
|
309
|
+
filepath: 'src/utils.ts',
|
|
310
|
+
content: 'export function utility() {}',
|
|
311
|
+
startLine: 1,
|
|
312
|
+
endLine: 10,
|
|
313
|
+
language: 'typescript',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'test/utils.test.ts:1-10',
|
|
317
|
+
filepath: 'test/utils.test.ts',
|
|
318
|
+
content: 'describe("utility", () => {})',
|
|
319
|
+
startLine: 1,
|
|
320
|
+
endLine: 10,
|
|
321
|
+
language: 'typescript',
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'src/index.ts:1-10',
|
|
325
|
+
filepath: 'src/index.ts',
|
|
326
|
+
content: 'import { utility } from "./utils"',
|
|
327
|
+
startLine: 1,
|
|
328
|
+
endLine: 10,
|
|
329
|
+
language: 'typescript',
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
333
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
334
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
335
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
336
|
+
await indexer.initialize();
|
|
337
|
+
// Filter to only src directory
|
|
338
|
+
const results = await indexer.search({
|
|
339
|
+
query: 'utility',
|
|
340
|
+
pathPattern: 'src/**',
|
|
341
|
+
});
|
|
342
|
+
expect(results).toHaveLength(2);
|
|
343
|
+
expect(results.map((r) => r.filepath)).toEqual(['src/utils.ts', 'src/index.ts']);
|
|
344
|
+
});
|
|
345
|
+
it('should filter results by negation pathPattern', async () => {
|
|
346
|
+
const mockTable = createMockTable([
|
|
347
|
+
{
|
|
348
|
+
id: 'src/utils.ts:1-10',
|
|
349
|
+
filepath: 'src/utils.ts',
|
|
350
|
+
content: 'export function utility() {}',
|
|
351
|
+
startLine: 1,
|
|
352
|
+
endLine: 10,
|
|
353
|
+
language: 'typescript',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: 'test/utils.test.ts:1-10',
|
|
357
|
+
filepath: 'test/utils.test.ts',
|
|
358
|
+
content: 'describe("utility", () => {})',
|
|
359
|
+
startLine: 1,
|
|
360
|
+
endLine: 10,
|
|
361
|
+
language: 'typescript',
|
|
362
|
+
},
|
|
363
|
+
]);
|
|
364
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
365
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
366
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
367
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
368
|
+
await indexer.initialize();
|
|
369
|
+
// Exclude test files
|
|
370
|
+
const results = await indexer.search({
|
|
371
|
+
query: 'utility',
|
|
372
|
+
pathPattern: '!test/**',
|
|
373
|
+
});
|
|
374
|
+
expect(results).toHaveLength(1);
|
|
375
|
+
expect(results[0].filepath).toBe('src/utils.ts');
|
|
376
|
+
});
|
|
377
|
+
it('should filter results by languages array', async () => {
|
|
378
|
+
const mockTable = createMockTable([
|
|
379
|
+
{
|
|
380
|
+
id: 'app.ts:1-10',
|
|
381
|
+
filepath: 'app.ts',
|
|
382
|
+
content: 'const app = express()',
|
|
383
|
+
startLine: 1,
|
|
384
|
+
endLine: 10,
|
|
385
|
+
language: 'typescript',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: 'app.js:1-10',
|
|
389
|
+
filepath: 'app.js',
|
|
390
|
+
content: 'const app = require("express")()',
|
|
391
|
+
startLine: 1,
|
|
392
|
+
endLine: 10,
|
|
393
|
+
language: 'javascript',
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: 'styles.css:1-10',
|
|
397
|
+
filepath: 'styles.css',
|
|
398
|
+
content: '.app { display: flex; }',
|
|
399
|
+
startLine: 1,
|
|
400
|
+
endLine: 10,
|
|
401
|
+
language: 'css',
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
404
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
405
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
406
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
407
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
408
|
+
await indexer.initialize();
|
|
409
|
+
// Filter to only TypeScript files
|
|
410
|
+
const results = await indexer.search({
|
|
411
|
+
query: 'app',
|
|
412
|
+
languages: ['typescript'],
|
|
413
|
+
});
|
|
414
|
+
expect(results).toHaveLength(1);
|
|
415
|
+
expect(results[0].language).toBe('typescript');
|
|
416
|
+
});
|
|
417
|
+
it('should filter by multiple languages', async () => {
|
|
418
|
+
const mockTable = createMockTable([
|
|
419
|
+
{
|
|
420
|
+
id: 'app.ts:1-10',
|
|
421
|
+
filepath: 'app.ts',
|
|
422
|
+
content: 'const app = express()',
|
|
423
|
+
startLine: 1,
|
|
424
|
+
endLine: 10,
|
|
425
|
+
language: 'typescript',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: 'app.js:1-10',
|
|
429
|
+
filepath: 'app.js',
|
|
430
|
+
content: 'const app = require("express")()',
|
|
431
|
+
startLine: 1,
|
|
432
|
+
endLine: 10,
|
|
433
|
+
language: 'javascript',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: 'styles.css:1-10',
|
|
437
|
+
filepath: 'styles.css',
|
|
438
|
+
content: '.app { display: flex; }',
|
|
439
|
+
startLine: 1,
|
|
440
|
+
endLine: 10,
|
|
441
|
+
language: 'css',
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
445
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
446
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
447
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
448
|
+
await indexer.initialize();
|
|
449
|
+
// Filter to TypeScript and JavaScript
|
|
450
|
+
const results = await indexer.search({
|
|
451
|
+
query: 'app',
|
|
452
|
+
languages: ['typescript', 'javascript'],
|
|
453
|
+
});
|
|
454
|
+
expect(results).toHaveLength(2);
|
|
455
|
+
expect(results.map((r) => r.language).sort()).toEqual(['javascript', 'typescript']);
|
|
456
|
+
});
|
|
457
|
+
it('should combine pathPattern and languages filters', async () => {
|
|
458
|
+
const mockTable = createMockTable([
|
|
459
|
+
{
|
|
460
|
+
id: 'src/app.ts:1-10',
|
|
461
|
+
filepath: 'src/app.ts',
|
|
462
|
+
content: 'const app = express()',
|
|
463
|
+
startLine: 1,
|
|
464
|
+
endLine: 10,
|
|
465
|
+
language: 'typescript',
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'src/app.js:1-10',
|
|
469
|
+
filepath: 'src/app.js',
|
|
470
|
+
content: 'const app = require("express")()',
|
|
471
|
+
startLine: 1,
|
|
472
|
+
endLine: 10,
|
|
473
|
+
language: 'javascript',
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: 'test/app.test.ts:1-10',
|
|
477
|
+
filepath: 'test/app.test.ts',
|
|
478
|
+
content: 'describe("app", () => {})',
|
|
479
|
+
startLine: 1,
|
|
480
|
+
endLine: 10,
|
|
481
|
+
language: 'typescript',
|
|
482
|
+
},
|
|
483
|
+
]);
|
|
484
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
485
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
486
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
487
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
488
|
+
await indexer.initialize();
|
|
489
|
+
// Filter to src directory AND TypeScript only
|
|
490
|
+
const results = await indexer.search({
|
|
491
|
+
query: 'app',
|
|
492
|
+
pathPattern: 'src/**',
|
|
493
|
+
languages: ['typescript'],
|
|
494
|
+
});
|
|
495
|
+
expect(results).toHaveLength(1);
|
|
496
|
+
expect(results[0].filepath).toBe('src/app.ts');
|
|
497
|
+
});
|
|
498
|
+
it('should handle case-insensitive language filtering', async () => {
|
|
499
|
+
const mockTable = createMockTable([
|
|
500
|
+
{
|
|
501
|
+
id: 'app.ts:1-10',
|
|
502
|
+
filepath: 'app.ts',
|
|
503
|
+
content: 'const app = express()',
|
|
504
|
+
startLine: 1,
|
|
505
|
+
endLine: 10,
|
|
506
|
+
language: 'TypeScript',
|
|
507
|
+
},
|
|
508
|
+
]);
|
|
509
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
510
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
511
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
512
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
513
|
+
await indexer.initialize();
|
|
514
|
+
// Use lowercase in filter
|
|
515
|
+
const results = await indexer.search({
|
|
516
|
+
query: 'app',
|
|
517
|
+
languages: ['typescript'],
|
|
518
|
+
});
|
|
519
|
+
expect(results).toHaveLength(1);
|
|
520
|
+
});
|
|
521
|
+
it('should support SearchOptions object syntax', async () => {
|
|
522
|
+
const mockTable = createMockTable([
|
|
523
|
+
{
|
|
524
|
+
id: 'test.ts:1-10',
|
|
525
|
+
filepath: 'test.ts',
|
|
526
|
+
content: 'function test() {}',
|
|
527
|
+
startLine: 1,
|
|
528
|
+
endLine: 10,
|
|
529
|
+
language: 'typescript',
|
|
530
|
+
},
|
|
531
|
+
]);
|
|
532
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
533
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
534
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
535
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
536
|
+
await indexer.initialize();
|
|
537
|
+
// Use SearchOptions object
|
|
538
|
+
const results = await indexer.search({
|
|
539
|
+
query: 'test',
|
|
540
|
+
limit: 5,
|
|
541
|
+
});
|
|
542
|
+
expect(results).toHaveLength(1);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
describe('clearIndex', () => {
|
|
546
|
+
it('should drop table when it exists', async () => {
|
|
547
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
548
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
549
|
+
await indexer.initialize();
|
|
550
|
+
await indexer.clearIndex();
|
|
551
|
+
expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
|
|
552
|
+
});
|
|
553
|
+
it('should handle non-existent table gracefully', async () => {
|
|
554
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
555
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
556
|
+
await indexer.initialize();
|
|
557
|
+
// Should not throw
|
|
558
|
+
await expect(indexer.clearIndex()).resolves.toBeUndefined();
|
|
559
|
+
expect(mockConnection.dropTable).not.toHaveBeenCalled();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
describe('searchSimilar', () => {
|
|
563
|
+
it('should find similar code with code snippet input', async () => {
|
|
564
|
+
const chunks = [
|
|
565
|
+
{
|
|
566
|
+
id: 'file1.ts:1-10',
|
|
567
|
+
filepath: 'file1.ts',
|
|
568
|
+
content: 'function authenticate(user) { return true; }',
|
|
569
|
+
startLine: 1,
|
|
570
|
+
endLine: 10,
|
|
571
|
+
language: 'typescript',
|
|
572
|
+
_distance: 0.1,
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: 'file2.ts:1-10',
|
|
576
|
+
filepath: 'file2.ts',
|
|
577
|
+
content: 'function validate(input) { return false; }',
|
|
578
|
+
startLine: 1,
|
|
579
|
+
endLine: 10,
|
|
580
|
+
language: 'typescript',
|
|
581
|
+
_distance: 0.5,
|
|
582
|
+
},
|
|
583
|
+
];
|
|
584
|
+
const mockTable = createMockTable(chunks);
|
|
585
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
586
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
587
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
588
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
589
|
+
await indexer.initialize();
|
|
590
|
+
const results = await indexer.searchSimilar({
|
|
591
|
+
code: 'function login(user) { return true; }',
|
|
592
|
+
limit: 10,
|
|
593
|
+
});
|
|
594
|
+
expect(results.length).toBe(2);
|
|
595
|
+
expect(results[0].filepath).toBe('file1.ts');
|
|
596
|
+
expect(results[0]).toHaveProperty('similarity');
|
|
597
|
+
expect(mockBackend.embed).toHaveBeenCalledWith('function login(user) { return true; }');
|
|
598
|
+
});
|
|
599
|
+
it('should exclude self when excludeSelf is true', async () => {
|
|
600
|
+
const chunks = [
|
|
601
|
+
{
|
|
602
|
+
id: 'file1.ts:1-10',
|
|
603
|
+
filepath: 'file1.ts',
|
|
604
|
+
content: 'function test() {}',
|
|
605
|
+
startLine: 1,
|
|
606
|
+
endLine: 10,
|
|
607
|
+
language: 'typescript',
|
|
608
|
+
_distance: 0,
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: 'file2.ts:1-10',
|
|
612
|
+
filepath: 'file2.ts',
|
|
613
|
+
content: 'function other() {}',
|
|
614
|
+
startLine: 1,
|
|
615
|
+
endLine: 10,
|
|
616
|
+
language: 'typescript',
|
|
617
|
+
_distance: 0.3,
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
const mockTable = createMockTable(chunks);
|
|
621
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
622
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
623
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
624
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
625
|
+
await indexer.initialize();
|
|
626
|
+
// Should exclude exact content match
|
|
627
|
+
const results = await indexer.searchSimilar({
|
|
628
|
+
code: 'function test() {}',
|
|
629
|
+
excludeSelf: true,
|
|
630
|
+
});
|
|
631
|
+
expect(results.every((r) => r.content.trim() !== 'function test() {}')).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
it('should throw error when neither code nor filepath is provided', async () => {
|
|
634
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
635
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
636
|
+
await indexer.initialize();
|
|
637
|
+
await expect(indexer.searchSimilar({})).rejects.toThrow('Either code or filepath must be provided');
|
|
638
|
+
});
|
|
639
|
+
it('should respect threshold parameter', async () => {
|
|
640
|
+
const chunks = [
|
|
641
|
+
{
|
|
642
|
+
id: 'file1.ts:1-10',
|
|
643
|
+
filepath: 'file1.ts',
|
|
644
|
+
content: 'high similarity',
|
|
645
|
+
startLine: 1,
|
|
646
|
+
endLine: 10,
|
|
647
|
+
language: 'typescript',
|
|
648
|
+
_distance: 0.1,
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: 'file2.ts:1-10',
|
|
652
|
+
filepath: 'file2.ts',
|
|
653
|
+
content: 'low similarity',
|
|
654
|
+
startLine: 1,
|
|
655
|
+
endLine: 10,
|
|
656
|
+
language: 'typescript',
|
|
657
|
+
_distance: 0.9,
|
|
658
|
+
},
|
|
659
|
+
];
|
|
660
|
+
const mockTable = createMockTable(chunks);
|
|
661
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
662
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
663
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
664
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
665
|
+
await indexer.initialize();
|
|
666
|
+
// With high threshold, should filter out low similarity results
|
|
667
|
+
const results = await indexer.searchSimilar({
|
|
668
|
+
code: 'test code',
|
|
669
|
+
threshold: 0.5,
|
|
670
|
+
});
|
|
671
|
+
// Only high similarity result should pass
|
|
672
|
+
expect(results.length).toBe(1);
|
|
673
|
+
expect(results[0].filepath).toBe('file1.ts');
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
describe('indexCodebase', () => {
|
|
677
|
+
beforeEach(() => {
|
|
678
|
+
// Mock glob
|
|
679
|
+
vi.doMock('glob', () => ({
|
|
680
|
+
glob: vi.fn().mockResolvedValue([]),
|
|
681
|
+
}));
|
|
682
|
+
// Mock file stats
|
|
683
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
684
|
+
vi.mocked(fsPromises.writeFile).mockResolvedValue();
|
|
685
|
+
});
|
|
686
|
+
it('should use provided patterns over config', async () => {
|
|
687
|
+
const { glob } = await import('glob');
|
|
688
|
+
vi.mocked(glob).mockResolvedValue([]);
|
|
689
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
690
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
691
|
+
await indexer.initialize();
|
|
692
|
+
await indexer.indexCodebase(['**/*.py'], ['**/venv/**']);
|
|
693
|
+
expect(glob).toHaveBeenCalledWith('**/*.py', expect.objectContaining({
|
|
694
|
+
ignore: ['**/venv/**'],
|
|
695
|
+
}));
|
|
696
|
+
});
|
|
697
|
+
it('should detect incremental vs full indexing', async () => {
|
|
698
|
+
const { glob } = await import('glob');
|
|
699
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
700
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
|
|
701
|
+
if (path.includes('index-metadata')) {
|
|
702
|
+
return JSON.stringify({
|
|
703
|
+
lastUpdated: '2024-01-01',
|
|
704
|
+
fileCount: 1,
|
|
705
|
+
chunkCount: 1,
|
|
706
|
+
embeddingBackend: 'mock',
|
|
707
|
+
embeddingDimensions: 1536,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return 'const x = 1;';
|
|
711
|
+
});
|
|
712
|
+
// Has existing index
|
|
713
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
714
|
+
const mockTable = createMockTable([]);
|
|
715
|
+
const mockMetadataTable = createMockTable([]);
|
|
716
|
+
mockMetadataTable.query = vi.fn().mockReturnValue({
|
|
717
|
+
toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: Date.now() }]),
|
|
718
|
+
});
|
|
719
|
+
mockConnection.openTable.mockImplementation(async (name) => {
|
|
720
|
+
if (name === 'file_metadata')
|
|
721
|
+
return mockMetadataTable;
|
|
722
|
+
return mockTable;
|
|
723
|
+
});
|
|
724
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
725
|
+
await indexer.initialize();
|
|
726
|
+
const result = await indexer.indexCodebase();
|
|
727
|
+
expect(result.incremental).toBe(true);
|
|
728
|
+
});
|
|
729
|
+
it('should handle force reindex flag', async () => {
|
|
730
|
+
const { glob } = await import('glob');
|
|
731
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
732
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
733
|
+
// Has existing index
|
|
734
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
735
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
736
|
+
await indexer.initialize();
|
|
737
|
+
const result = await indexer.indexCodebase(undefined, undefined, true);
|
|
738
|
+
expect(result.incremental).toBe(false);
|
|
739
|
+
expect(mockConnection.dropTable).toHaveBeenCalledWith('code_chunks');
|
|
740
|
+
});
|
|
741
|
+
it('should report progress via callback', async () => {
|
|
742
|
+
const { glob } = await import('glob');
|
|
743
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
744
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
745
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
746
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
747
|
+
await indexer.initialize();
|
|
748
|
+
const progressUpdates = [];
|
|
749
|
+
await indexer.indexCodebase(undefined, undefined, false, (progress) => {
|
|
750
|
+
progressUpdates.push(progress);
|
|
751
|
+
});
|
|
752
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
753
|
+
expect(progressUpdates.some((p) => p.phase === 'scanning')).toBe(true);
|
|
754
|
+
expect(progressUpdates.some((p) => p.phase === 'chunking')).toBe(true);
|
|
755
|
+
expect(progressUpdates.some((p) => p.phase === 'embedding')).toBe(true);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
describe('hybrid scoring', () => {
|
|
759
|
+
it('should apply 70/30 semantic/keyword split', async () => {
|
|
760
|
+
// This tests the calculateKeywordScore indirectly through search
|
|
761
|
+
const mockTable = createMockTable([
|
|
762
|
+
{
|
|
763
|
+
id: '1',
|
|
764
|
+
filepath: 'auth.ts',
|
|
765
|
+
content: 'function authenticate() {}',
|
|
766
|
+
startLine: 1,
|
|
767
|
+
endLine: 1,
|
|
768
|
+
language: 'typescript',
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
id: '2',
|
|
772
|
+
filepath: 'other.ts',
|
|
773
|
+
content: 'function other() {}',
|
|
774
|
+
startLine: 1,
|
|
775
|
+
endLine: 1,
|
|
776
|
+
language: 'typescript',
|
|
777
|
+
},
|
|
778
|
+
]);
|
|
779
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
780
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
781
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
782
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
783
|
+
await indexer.initialize();
|
|
784
|
+
const results = await indexer.search('authenticate auth');
|
|
785
|
+
// auth.ts should rank higher due to keyword match in both content and filepath
|
|
786
|
+
expect(results[0].filepath).toBe('auth.ts');
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
describe('corruption detection', () => {
|
|
790
|
+
it('should detect missing metadata as corruption', async () => {
|
|
791
|
+
const mockTable = createMockTable([
|
|
792
|
+
{
|
|
793
|
+
id: '1',
|
|
794
|
+
filepath: 'test.ts',
|
|
795
|
+
content: 'code',
|
|
796
|
+
startLine: 1,
|
|
797
|
+
endLine: 10,
|
|
798
|
+
language: 'typescript',
|
|
799
|
+
},
|
|
800
|
+
]);
|
|
801
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
802
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
803
|
+
// Metadata file doesn't exist
|
|
804
|
+
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
805
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
806
|
+
await indexer.initialize();
|
|
807
|
+
const status = await indexer.getStatus();
|
|
808
|
+
expect(status.corrupted).toBe(true);
|
|
809
|
+
expect(status.corruptionReason).toContain('Missing index metadata');
|
|
810
|
+
});
|
|
811
|
+
it('should detect chunk count mismatch as corruption', async () => {
|
|
812
|
+
const mockTable = createMockTable([
|
|
813
|
+
{
|
|
814
|
+
id: '1',
|
|
815
|
+
filepath: 'test.ts',
|
|
816
|
+
content: 'code',
|
|
817
|
+
startLine: 1,
|
|
818
|
+
endLine: 10,
|
|
819
|
+
language: 'typescript',
|
|
820
|
+
},
|
|
821
|
+
]);
|
|
822
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
823
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
824
|
+
// Metadata says 5 chunks but table only has 1
|
|
825
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
826
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
827
|
+
fileCount: 1,
|
|
828
|
+
chunkCount: 5,
|
|
829
|
+
embeddingBackend: 'mock',
|
|
830
|
+
embeddingDimensions: 1536,
|
|
831
|
+
checksum: 'abc123',
|
|
832
|
+
}));
|
|
833
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
834
|
+
await indexer.initialize();
|
|
835
|
+
const status = await indexer.getStatus();
|
|
836
|
+
expect(status.corrupted).toBe(true);
|
|
837
|
+
expect(status.corruptionReason).toContain('Chunk count mismatch');
|
|
838
|
+
});
|
|
839
|
+
it('should report healthy index when metadata matches', async () => {
|
|
840
|
+
const mockTable = createMockTable([
|
|
841
|
+
{
|
|
842
|
+
id: '1',
|
|
843
|
+
filepath: 'test.ts',
|
|
844
|
+
content: 'code',
|
|
845
|
+
startLine: 1,
|
|
846
|
+
endLine: 10,
|
|
847
|
+
language: 'typescript',
|
|
848
|
+
},
|
|
849
|
+
]);
|
|
850
|
+
const mockMetadataTable = createMockTable([{ filepath: 'test.ts', mtime: Date.now() }]);
|
|
851
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
852
|
+
mockConnection.openTable.mockImplementation(async (name) => {
|
|
853
|
+
if (name === 'file_metadata')
|
|
854
|
+
return mockMetadataTable;
|
|
855
|
+
return mockTable;
|
|
856
|
+
});
|
|
857
|
+
// Metadata matches actual state
|
|
858
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify({
|
|
859
|
+
lastUpdated: '2024-01-01T00:00:00Z',
|
|
860
|
+
fileCount: 1,
|
|
861
|
+
chunkCount: 1,
|
|
862
|
+
embeddingBackend: 'mock',
|
|
863
|
+
embeddingDimensions: 1536,
|
|
864
|
+
checksum: '4694592dbdda93c1',
|
|
865
|
+
}));
|
|
866
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
867
|
+
await indexer.initialize();
|
|
868
|
+
const status = await indexer.getStatus();
|
|
869
|
+
expect(status.corrupted).toBe(false);
|
|
870
|
+
expect(status.corruptionReason).toBeUndefined();
|
|
871
|
+
});
|
|
872
|
+
it('should report no corruption when index is empty', async () => {
|
|
873
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
874
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
875
|
+
await indexer.initialize();
|
|
876
|
+
const status = await indexer.getStatus();
|
|
877
|
+
expect(status.indexed).toBe(false);
|
|
878
|
+
// Empty index has no corruption field (undefined, not false)
|
|
879
|
+
expect(status.corrupted).toBeUndefined();
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
describe('checkpoint-based indexing', () => {
|
|
883
|
+
it('should save checkpoint after chunking phase', async () => {
|
|
884
|
+
const { glob } = await import('glob');
|
|
885
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
886
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
887
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
888
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
889
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
890
|
+
await indexer.initialize();
|
|
891
|
+
await indexer.indexCodebase();
|
|
892
|
+
// Should have written checkpoint at least once
|
|
893
|
+
const writeFileCalls = vi.mocked(fsPromises.writeFile).mock.calls;
|
|
894
|
+
const checkpointWrites = writeFileCalls.filter((call) => call[0] === '/project/.glancey/checkpoint.json');
|
|
895
|
+
expect(checkpointWrites.length).toBeGreaterThan(0);
|
|
896
|
+
// First checkpoint should be after chunking phase
|
|
897
|
+
const firstCheckpoint = JSON.parse(checkpointWrites[0][1]);
|
|
898
|
+
expect(firstCheckpoint.phase).toBe('chunking');
|
|
899
|
+
expect(firstCheckpoint.embeddingBackend).toBe('mock');
|
|
900
|
+
});
|
|
901
|
+
it('should clear checkpoint after successful indexing', async () => {
|
|
902
|
+
const { glob } = await import('glob');
|
|
903
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
904
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('const x = 1;');
|
|
905
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
906
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
907
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
908
|
+
await indexer.initialize();
|
|
909
|
+
await indexer.indexCodebase();
|
|
910
|
+
// Should have tried to unlink the checkpoint file
|
|
911
|
+
expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
|
|
912
|
+
});
|
|
913
|
+
it('should resume from checkpoint with embedded chunks', async () => {
|
|
914
|
+
// Use a fixed mtime for checkpoint freshness validation
|
|
915
|
+
const checkpointMtime = Date.now() - 1000; // 1 second ago
|
|
916
|
+
// Create a checkpoint file with embedded chunks
|
|
917
|
+
const checkpoint = {
|
|
918
|
+
phase: 'embedding',
|
|
919
|
+
startedAt: '2024-01-01T00:00:00Z',
|
|
920
|
+
files: ['/project/test.ts'],
|
|
921
|
+
processedFiles: [],
|
|
922
|
+
embeddedChunks: [
|
|
923
|
+
{
|
|
924
|
+
id: 'test.ts:1-10',
|
|
925
|
+
filepath: 'test.ts',
|
|
926
|
+
content: 'const x = 1;',
|
|
927
|
+
startLine: 1,
|
|
928
|
+
endLine: 10,
|
|
929
|
+
language: 'typescript',
|
|
930
|
+
embedding: [0.1, 0.2, 0.3],
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
embeddingBackend: 'mock',
|
|
934
|
+
embeddingModel: 'mock-model',
|
|
935
|
+
fileMtimes: { 'test.ts': checkpointMtime },
|
|
936
|
+
};
|
|
937
|
+
let checkpointCleared = false;
|
|
938
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
|
|
939
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
940
|
+
if (checkpointCleared) {
|
|
941
|
+
throw new Error('ENOENT');
|
|
942
|
+
}
|
|
943
|
+
return JSON.stringify(checkpoint);
|
|
944
|
+
}
|
|
945
|
+
return 'const x = 1;';
|
|
946
|
+
});
|
|
947
|
+
// Return same mtime as checkpoint to indicate file hasn't changed
|
|
948
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: checkpointMtime });
|
|
949
|
+
vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
|
|
950
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
951
|
+
checkpointCleared = true;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
955
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
956
|
+
await indexer.initialize();
|
|
957
|
+
const result = await indexer.indexCodebase();
|
|
958
|
+
// Should have resumed and completed without re-embedding
|
|
959
|
+
expect(result.filesIndexed).toBe(1);
|
|
960
|
+
expect(result.chunksCreated).toBe(1);
|
|
961
|
+
// Should not have called embedBatch since chunks were already embedded
|
|
962
|
+
expect(mockBackend.embedBatch).not.toHaveBeenCalled();
|
|
963
|
+
});
|
|
964
|
+
it('should discard checkpoint when embedding backend changes', async () => {
|
|
965
|
+
const { glob } = await import('glob');
|
|
966
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
967
|
+
// Checkpoint was created with different backend
|
|
968
|
+
const checkpoint = {
|
|
969
|
+
phase: 'chunking',
|
|
970
|
+
startedAt: '2024-01-01T00:00:00Z',
|
|
971
|
+
files: ['/project/test.ts'],
|
|
972
|
+
processedFiles: [],
|
|
973
|
+
pendingChunks: [
|
|
974
|
+
{
|
|
975
|
+
id: 'test.ts:1-10',
|
|
976
|
+
filepath: 'test.ts',
|
|
977
|
+
content: 'const x = 1;',
|
|
978
|
+
startLine: 1,
|
|
979
|
+
endLine: 10,
|
|
980
|
+
language: 'typescript',
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
embeddingBackend: 'different-backend',
|
|
984
|
+
embeddingModel: 'different-model',
|
|
985
|
+
};
|
|
986
|
+
let checkpointCleared = false;
|
|
987
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
|
|
988
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
989
|
+
if (checkpointCleared) {
|
|
990
|
+
throw new Error('ENOENT');
|
|
991
|
+
}
|
|
992
|
+
return JSON.stringify(checkpoint);
|
|
993
|
+
}
|
|
994
|
+
return 'const x = 1;';
|
|
995
|
+
});
|
|
996
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
997
|
+
vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
|
|
998
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
999
|
+
checkpointCleared = true;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
1003
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1004
|
+
await indexer.initialize();
|
|
1005
|
+
await indexer.indexCodebase();
|
|
1006
|
+
// Should have cleared the incompatible checkpoint
|
|
1007
|
+
expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
|
|
1008
|
+
// Should have re-embedded since backend changed
|
|
1009
|
+
expect(mockBackend.embedBatch).toHaveBeenCalled();
|
|
1010
|
+
});
|
|
1011
|
+
it('should clear checkpoint when clearIndex is called', async () => {
|
|
1012
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
1013
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1014
|
+
await indexer.initialize();
|
|
1015
|
+
await indexer.clearIndex();
|
|
1016
|
+
// Should have tried to unlink the checkpoint file
|
|
1017
|
+
expect(fsPromises.unlink).toHaveBeenCalledWith('/project/.glancey/checkpoint.json');
|
|
1018
|
+
});
|
|
1019
|
+
it('should handle invalid checkpoint gracefully', async () => {
|
|
1020
|
+
const { glob } = await import('glob');
|
|
1021
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
1022
|
+
// Invalid checkpoint (missing required fields)
|
|
1023
|
+
let checkpointCleared = false;
|
|
1024
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (path) => {
|
|
1025
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
1026
|
+
if (checkpointCleared) {
|
|
1027
|
+
throw new Error('ENOENT');
|
|
1028
|
+
}
|
|
1029
|
+
return JSON.stringify({ phase: 'chunking' }); // Missing files and processedFiles
|
|
1030
|
+
}
|
|
1031
|
+
return 'const x = 1;';
|
|
1032
|
+
});
|
|
1033
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
1034
|
+
vi.mocked(fsPromises.unlink).mockImplementation(async (path) => {
|
|
1035
|
+
if (path === '/project/.glancey/checkpoint.json') {
|
|
1036
|
+
checkpointCleared = true;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
1040
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1041
|
+
await indexer.initialize();
|
|
1042
|
+
// Should not throw and should do full indexing
|
|
1043
|
+
const result = await indexer.indexCodebase();
|
|
1044
|
+
expect(result.filesIndexed).toBe(1);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
describe('incremental indexing race conditions', () => {
|
|
1048
|
+
it('should handle file deletion detected via metadata', async () => {
|
|
1049
|
+
const { glob } = await import('glob');
|
|
1050
|
+
// Only return test.ts, simulating deleted.ts was already removed before glob
|
|
1051
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
1052
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
|
|
1053
|
+
const pathStr = String(filePath);
|
|
1054
|
+
if (pathStr.includes('checkpoint.json')) {
|
|
1055
|
+
throw new Error('ENOENT');
|
|
1056
|
+
}
|
|
1057
|
+
if (pathStr.includes('metadata.json')) {
|
|
1058
|
+
return JSON.stringify({
|
|
1059
|
+
lastUpdated: new Date().toISOString(),
|
|
1060
|
+
fileCount: 2,
|
|
1061
|
+
chunkCount: 2,
|
|
1062
|
+
embeddingBackend: 'mock',
|
|
1063
|
+
embeddingModel: 'mock-model',
|
|
1064
|
+
embeddingDimensions: 1536,
|
|
1065
|
+
version: '1.0.0',
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
return 'const x = 1;';
|
|
1069
|
+
});
|
|
1070
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
1071
|
+
// Include file_metadata table to enable incremental detection
|
|
1072
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
1073
|
+
const mockTable = createMockTable();
|
|
1074
|
+
// Mock metadata table with two files - one was deleted
|
|
1075
|
+
const mockMetadataTable = createMockTable();
|
|
1076
|
+
mockMetadataTable.query.mockReturnValue({
|
|
1077
|
+
toArray: vi.fn().mockResolvedValue([
|
|
1078
|
+
{ filepath: 'test.ts', mtime: Date.now() - 1000 },
|
|
1079
|
+
{ filepath: 'deleted.ts', mtime: Date.now() - 1000 },
|
|
1080
|
+
]),
|
|
1081
|
+
});
|
|
1082
|
+
mockConnection.openTable.mockImplementation(async (name) => {
|
|
1083
|
+
if (name === 'file_metadata') {
|
|
1084
|
+
return mockMetadataTable;
|
|
1085
|
+
}
|
|
1086
|
+
return mockTable;
|
|
1087
|
+
});
|
|
1088
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1089
|
+
await indexer.initialize();
|
|
1090
|
+
// Should handle gracefully - detect deleted.ts as deleted since it's in metadata but not in glob
|
|
1091
|
+
const result = await indexer.indexCodebase();
|
|
1092
|
+
expect(result.incremental).toBe(true);
|
|
1093
|
+
// The deleted file should be removed from the index
|
|
1094
|
+
expect(mockTable.delete).toHaveBeenCalled();
|
|
1095
|
+
});
|
|
1096
|
+
it('should handle file modified during change detection', async () => {
|
|
1097
|
+
const { glob } = await import('glob');
|
|
1098
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
1099
|
+
// File mtime changes between calls to stat
|
|
1100
|
+
let statCallCount = 0;
|
|
1101
|
+
const initialMtime = Date.now() - 5000;
|
|
1102
|
+
const modifiedMtime = Date.now();
|
|
1103
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
|
|
1104
|
+
const pathStr = String(filePath);
|
|
1105
|
+
if (pathStr.includes('checkpoint.json')) {
|
|
1106
|
+
throw new Error('ENOENT');
|
|
1107
|
+
}
|
|
1108
|
+
if (pathStr.includes('metadata.json')) {
|
|
1109
|
+
return JSON.stringify({
|
|
1110
|
+
lastUpdated: new Date().toISOString(),
|
|
1111
|
+
fileCount: 1,
|
|
1112
|
+
chunkCount: 1,
|
|
1113
|
+
embeddingBackend: 'mock',
|
|
1114
|
+
embeddingModel: 'mock-model',
|
|
1115
|
+
embeddingDimensions: 1536, // Match mock backend dimensions
|
|
1116
|
+
version: '1.0.0',
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
if (pathStr.includes('files.json')) {
|
|
1120
|
+
return JSON.stringify({
|
|
1121
|
+
'test.ts': initialMtime,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return 'const x = 1;';
|
|
1125
|
+
});
|
|
1126
|
+
vi.mocked(fsPromises.stat).mockImplementation(async () => {
|
|
1127
|
+
statCallCount++;
|
|
1128
|
+
// First call during change detection returns old mtime
|
|
1129
|
+
// Second call (if any) returns new mtime simulating modification
|
|
1130
|
+
return { mtimeMs: statCallCount === 1 ? initialMtime : modifiedMtime };
|
|
1131
|
+
});
|
|
1132
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
1133
|
+
const mockTable = createMockTable();
|
|
1134
|
+
mockConnection.openTable.mockResolvedValue(mockTable);
|
|
1135
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1136
|
+
await indexer.initialize();
|
|
1137
|
+
// Should complete without error - the file appears unchanged based on first stat
|
|
1138
|
+
const result = await indexer.indexCodebase();
|
|
1139
|
+
expect(result.incremental).toBe(true);
|
|
1140
|
+
});
|
|
1141
|
+
it('should handle concurrent indexCodebase calls gracefully', async () => {
|
|
1142
|
+
const { glob } = await import('glob');
|
|
1143
|
+
vi.mocked(glob).mockResolvedValue(['/project/test.ts']);
|
|
1144
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
|
|
1145
|
+
const pathStr = String(filePath);
|
|
1146
|
+
if (pathStr.includes('checkpoint.json')) {
|
|
1147
|
+
throw new Error('ENOENT');
|
|
1148
|
+
}
|
|
1149
|
+
return 'const x = 1;';
|
|
1150
|
+
});
|
|
1151
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
1152
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
1153
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1154
|
+
await indexer.initialize();
|
|
1155
|
+
// Start two concurrent indexing operations
|
|
1156
|
+
const promise1 = indexer.indexCodebase();
|
|
1157
|
+
const promise2 = indexer.indexCodebase();
|
|
1158
|
+
// Both should complete without throwing
|
|
1159
|
+
const [result1, result2] = await Promise.all([promise1, promise2]);
|
|
1160
|
+
// Both should report success (though the actual behavior depends on implementation)
|
|
1161
|
+
expect(result1.filesIndexed).toBeGreaterThanOrEqual(0);
|
|
1162
|
+
expect(result2.filesIndexed).toBeGreaterThanOrEqual(0);
|
|
1163
|
+
});
|
|
1164
|
+
it('should handle empty file list during indexing', async () => {
|
|
1165
|
+
const { glob } = await import('glob');
|
|
1166
|
+
// All files were deleted or none match
|
|
1167
|
+
vi.mocked(glob).mockResolvedValue([]);
|
|
1168
|
+
vi.mocked(fsPromises.readFile).mockImplementation(async (filePath) => {
|
|
1169
|
+
const pathStr = String(filePath);
|
|
1170
|
+
if (pathStr.includes('checkpoint.json')) {
|
|
1171
|
+
throw new Error('ENOENT');
|
|
1172
|
+
}
|
|
1173
|
+
return '';
|
|
1174
|
+
});
|
|
1175
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: Date.now() });
|
|
1176
|
+
// No existing index
|
|
1177
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
1178
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1179
|
+
await indexer.initialize();
|
|
1180
|
+
// Should handle gracefully - create empty index
|
|
1181
|
+
const result = await indexer.indexCodebase();
|
|
1182
|
+
expect(result.filesIndexed).toBe(0);
|
|
1183
|
+
expect(result.chunksCreated).toBe(0);
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
describe('checkIfStale', () => {
|
|
1187
|
+
it('should return stale=true when index does not exist', async () => {
|
|
1188
|
+
mockConnection.tableNames.mockResolvedValue([]);
|
|
1189
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1190
|
+
await indexer.initialize();
|
|
1191
|
+
const result = await indexer.checkIfStale();
|
|
1192
|
+
expect(result.stale).toBe(true);
|
|
1193
|
+
expect(result.reason).toBe('Index does not exist');
|
|
1194
|
+
});
|
|
1195
|
+
it('should return stale=true when file_metadata table is missing', async () => {
|
|
1196
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks']);
|
|
1197
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1198
|
+
await indexer.initialize();
|
|
1199
|
+
const result = await indexer.checkIfStale();
|
|
1200
|
+
expect(result.stale).toBe(true);
|
|
1201
|
+
expect(result.reason).toBe('No file metadata stored');
|
|
1202
|
+
});
|
|
1203
|
+
it('should return stale=false when no files have changed', async () => {
|
|
1204
|
+
const { glob } = await import('glob');
|
|
1205
|
+
const testFilePath = '/project/test.ts';
|
|
1206
|
+
const storedMtime = 1000;
|
|
1207
|
+
vi.mocked(glob).mockResolvedValue([testFilePath]);
|
|
1208
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: storedMtime });
|
|
1209
|
+
// Mock metadata table with matching mtime - needs query().toArray() chain
|
|
1210
|
+
const mockMetadataTable = {
|
|
1211
|
+
query: vi.fn().mockReturnValue({
|
|
1212
|
+
toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: storedMtime }]),
|
|
1213
|
+
}),
|
|
1214
|
+
};
|
|
1215
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
1216
|
+
mockConnection.openTable.mockImplementation(async (tableName) => {
|
|
1217
|
+
if (tableName === 'file_metadata') {
|
|
1218
|
+
return mockMetadataTable;
|
|
1219
|
+
}
|
|
1220
|
+
return createMockTable([]);
|
|
1221
|
+
});
|
|
1222
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1223
|
+
await indexer.initialize();
|
|
1224
|
+
const result = await indexer.checkIfStale();
|
|
1225
|
+
expect(result.stale).toBe(false);
|
|
1226
|
+
expect(result.reason).toBeUndefined();
|
|
1227
|
+
});
|
|
1228
|
+
it('should return stale=true with reason when files are modified', async () => {
|
|
1229
|
+
const { glob } = await import('glob');
|
|
1230
|
+
const testFilePath = '/project/test.ts';
|
|
1231
|
+
const storedMtime = 1000;
|
|
1232
|
+
const newMtime = 2000; // Newer than stored
|
|
1233
|
+
vi.mocked(glob).mockResolvedValue([testFilePath]);
|
|
1234
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: newMtime });
|
|
1235
|
+
// Mock metadata table with older mtime - needs query().toArray() chain
|
|
1236
|
+
const mockMetadataTable = {
|
|
1237
|
+
query: vi.fn().mockReturnValue({
|
|
1238
|
+
toArray: vi.fn().mockResolvedValue([{ filepath: 'test.ts', mtime: storedMtime }]),
|
|
1239
|
+
}),
|
|
1240
|
+
};
|
|
1241
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
1242
|
+
mockConnection.openTable.mockImplementation(async (tableName) => {
|
|
1243
|
+
if (tableName === 'file_metadata') {
|
|
1244
|
+
return mockMetadataTable;
|
|
1245
|
+
}
|
|
1246
|
+
return createMockTable([]);
|
|
1247
|
+
});
|
|
1248
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1249
|
+
await indexer.initialize();
|
|
1250
|
+
const result = await indexer.checkIfStale();
|
|
1251
|
+
expect(result.stale).toBe(true);
|
|
1252
|
+
expect(result.reason).toContain('modified');
|
|
1253
|
+
});
|
|
1254
|
+
it('should return stale=true with reason when new files are added', async () => {
|
|
1255
|
+
const { glob } = await import('glob');
|
|
1256
|
+
vi.mocked(glob).mockResolvedValue(['/project/existing.ts', '/project/new.ts']);
|
|
1257
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: 1000 });
|
|
1258
|
+
// Mock metadata table with only one file - needs query().toArray() chain
|
|
1259
|
+
const mockMetadataTable = {
|
|
1260
|
+
query: vi.fn().mockReturnValue({
|
|
1261
|
+
toArray: vi.fn().mockResolvedValue([{ filepath: 'existing.ts', mtime: 1000 }]),
|
|
1262
|
+
}),
|
|
1263
|
+
};
|
|
1264
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
1265
|
+
mockConnection.openTable.mockImplementation(async (tableName) => {
|
|
1266
|
+
if (tableName === 'file_metadata') {
|
|
1267
|
+
return mockMetadataTable;
|
|
1268
|
+
}
|
|
1269
|
+
return createMockTable([]);
|
|
1270
|
+
});
|
|
1271
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1272
|
+
await indexer.initialize();
|
|
1273
|
+
const result = await indexer.checkIfStale();
|
|
1274
|
+
expect(result.stale).toBe(true);
|
|
1275
|
+
expect(result.reason).toContain('new file');
|
|
1276
|
+
});
|
|
1277
|
+
it('should return stale=true with reason when files are deleted', async () => {
|
|
1278
|
+
const { glob } = await import('glob');
|
|
1279
|
+
vi.mocked(glob).mockResolvedValue(['/project/remaining.ts']);
|
|
1280
|
+
vi.mocked(fsPromises.stat).mockResolvedValue({ mtimeMs: 1000 });
|
|
1281
|
+
// Mock metadata table with two files (one deleted) - needs query().toArray() chain
|
|
1282
|
+
const mockMetadataTable = {
|
|
1283
|
+
query: vi.fn().mockReturnValue({
|
|
1284
|
+
toArray: vi.fn().mockResolvedValue([
|
|
1285
|
+
{ filepath: 'remaining.ts', mtime: 1000 },
|
|
1286
|
+
{ filepath: 'deleted.ts', mtime: 1000 },
|
|
1287
|
+
]),
|
|
1288
|
+
}),
|
|
1289
|
+
};
|
|
1290
|
+
mockConnection.tableNames.mockResolvedValue(['code_chunks', 'file_metadata']);
|
|
1291
|
+
mockConnection.openTable.mockImplementation(async (tableName) => {
|
|
1292
|
+
if (tableName === 'file_metadata') {
|
|
1293
|
+
return mockMetadataTable;
|
|
1294
|
+
}
|
|
1295
|
+
return createMockTable([]);
|
|
1296
|
+
});
|
|
1297
|
+
const indexer = new CodeIndexer('/project', mockBackend);
|
|
1298
|
+
await indexer.initialize();
|
|
1299
|
+
const result = await indexer.checkIfStale();
|
|
1300
|
+
expect(result.stale).toBe(true);
|
|
1301
|
+
expect(result.reason).toContain('deleted');
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
//# sourceMappingURL=indexer.test.js.map
|