doc-freshness-checker 1.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 +305 -0
- package/dist/cache/cacheManager.d.ts +42 -0
- package/dist/cache/cacheManager.js +138 -0
- package/dist/cache/cacheManager.js.map +1 -0
- package/dist/cache/cacheManager.test.d.ts +1 -0
- package/dist/cache/cacheManager.test.js +142 -0
- package/dist/cache/cacheManager.test.js.map +1 -0
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +137 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +184 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.js +135 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/defineConfig.d.ts +28 -0
- package/dist/config/defineConfig.js +30 -0
- package/dist/config/defineConfig.js.map +1 -0
- package/dist/config/defineConfig.test.d.ts +1 -0
- package/dist/config/defineConfig.test.js +10 -0
- package/dist/config/defineConfig.test.js.map +1 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +250 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/loader.test.d.ts +1 -0
- package/dist/config/loader.test.js +276 -0
- package/dist/config/loader.test.js.map +1 -0
- package/dist/git/changeTracker.d.ts +44 -0
- package/dist/git/changeTracker.js +149 -0
- package/dist/git/changeTracker.js.map +1 -0
- package/dist/git/changeTracker.test.d.ts +1 -0
- package/dist/git/changeTracker.test.js +184 -0
- package/dist/git/changeTracker.test.js.map +1 -0
- package/dist/graph/codeDocGraph.d.ts +43 -0
- package/dist/graph/codeDocGraph.js +103 -0
- package/dist/graph/codeDocGraph.js.map +1 -0
- package/dist/graph/codeDocGraph.test.d.ts +1 -0
- package/dist/graph/codeDocGraph.test.js +78 -0
- package/dist/graph/codeDocGraph.test.js.map +1 -0
- package/dist/graph/graphBuilder.d.ts +17 -0
- package/dist/graph/graphBuilder.js +76 -0
- package/dist/graph/graphBuilder.js.map +1 -0
- package/dist/graph/graphBuilder.test.d.ts +1 -0
- package/dist/graph/graphBuilder.test.js +87 -0
- package/dist/graph/graphBuilder.test.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/documentParser.d.ts +22 -0
- package/dist/parsers/documentParser.js +76 -0
- package/dist/parsers/documentParser.js.map +1 -0
- package/dist/parsers/documentParser.test.d.ts +1 -0
- package/dist/parsers/documentParser.test.js +116 -0
- package/dist/parsers/documentParser.test.js.map +1 -0
- package/dist/parsers/extractors/baseExtractor.d.ts +19 -0
- package/dist/parsers/extractors/baseExtractor.js +33 -0
- package/dist/parsers/extractors/baseExtractor.js.map +1 -0
- package/dist/parsers/extractors/baseExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/baseExtractor.test.js +43 -0
- package/dist/parsers/extractors/baseExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/codePatternExtractor.d.ts +13 -0
- package/dist/parsers/extractors/codePatternExtractor.js +108 -0
- package/dist/parsers/extractors/codePatternExtractor.js.map +1 -0
- package/dist/parsers/extractors/codePatternExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/codePatternExtractor.test.js +49 -0
- package/dist/parsers/extractors/codePatternExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/dependencyExtractor.d.ts +12 -0
- package/dist/parsers/extractors/dependencyExtractor.js +92 -0
- package/dist/parsers/extractors/dependencyExtractor.js.map +1 -0
- package/dist/parsers/extractors/dependencyExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/dependencyExtractor.test.js +48 -0
- package/dist/parsers/extractors/dependencyExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/directoryStructureExtractor.d.ts +34 -0
- package/dist/parsers/extractors/directoryStructureExtractor.js +168 -0
- package/dist/parsers/extractors/directoryStructureExtractor.js.map +1 -0
- package/dist/parsers/extractors/directoryStructureExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/directoryStructureExtractor.test.js +121 -0
- package/dist/parsers/extractors/directoryStructureExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/externalUrlExtractor.d.ts +14 -0
- package/dist/parsers/extractors/externalUrlExtractor.js +53 -0
- package/dist/parsers/extractors/externalUrlExtractor.js.map +1 -0
- package/dist/parsers/extractors/externalUrlExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/externalUrlExtractor.test.js +85 -0
- package/dist/parsers/extractors/externalUrlExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/filePathExtractor.d.ts +18 -0
- package/dist/parsers/extractors/filePathExtractor.js +72 -0
- package/dist/parsers/extractors/filePathExtractor.js.map +1 -0
- package/dist/parsers/extractors/filePathExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/filePathExtractor.test.js +73 -0
- package/dist/parsers/extractors/filePathExtractor.test.js.map +1 -0
- package/dist/parsers/extractors/versionExtractor.d.ts +11 -0
- package/dist/parsers/extractors/versionExtractor.js +74 -0
- package/dist/parsers/extractors/versionExtractor.js.map +1 -0
- package/dist/parsers/extractors/versionExtractor.test.d.ts +1 -0
- package/dist/parsers/extractors/versionExtractor.test.js +55 -0
- package/dist/parsers/extractors/versionExtractor.test.js.map +1 -0
- package/dist/plugins/plugin.d.ts +32 -0
- package/dist/plugins/plugin.js +40 -0
- package/dist/plugins/plugin.js.map +1 -0
- package/dist/plugins/plugin.test.d.ts +1 -0
- package/dist/plugins/plugin.test.js +23 -0
- package/dist/plugins/plugin.test.js.map +1 -0
- package/dist/reporters/consoleReporter.d.ts +15 -0
- package/dist/reporters/consoleReporter.js +73 -0
- package/dist/reporters/consoleReporter.js.map +1 -0
- package/dist/reporters/consoleReporter.test.d.ts +1 -0
- package/dist/reporters/consoleReporter.test.js +155 -0
- package/dist/reporters/consoleReporter.test.js.map +1 -0
- package/dist/reporters/enhancedReporter.d.ts +12 -0
- package/dist/reporters/enhancedReporter.js +81 -0
- package/dist/reporters/enhancedReporter.js.map +1 -0
- package/dist/reporters/enhancedReporter.test.d.ts +1 -0
- package/dist/reporters/enhancedReporter.test.js +152 -0
- package/dist/reporters/enhancedReporter.test.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +11 -0
- package/dist/reporters/jsonReporter.js +20 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/jsonReporter.test.d.ts +1 -0
- package/dist/reporters/jsonReporter.test.js +31 -0
- package/dist/reporters/jsonReporter.test.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +11 -0
- package/dist/reporters/markdownReporter.js +55 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.test.d.ts +1 -0
- package/dist/reporters/markdownReporter.test.js +136 -0
- package/dist/reporters/markdownReporter.test.js.map +1 -0
- package/dist/runner.d.ts +9 -0
- package/dist/runner.js +265 -0
- package/dist/runner.js.map +1 -0
- package/dist/runner.test.d.ts +1 -0
- package/dist/runner.test.js +353 -0
- package/dist/runner.test.js.map +1 -0
- package/dist/scoring/freshnessScorer.d.ts +40 -0
- package/dist/scoring/freshnessScorer.js +170 -0
- package/dist/scoring/freshnessScorer.js.map +1 -0
- package/dist/scoring/freshnessScorer.test.d.ts +1 -0
- package/dist/scoring/freshnessScorer.test.js +397 -0
- package/dist/scoring/freshnessScorer.test.js.map +1 -0
- package/dist/semantic/vectorSearch.d.ts +84 -0
- package/dist/semantic/vectorSearch.js +484 -0
- package/dist/semantic/vectorSearch.js.map +1 -0
- package/dist/semantic/vectorSearch.test.d.ts +1 -0
- package/dist/semantic/vectorSearch.test.js +660 -0
- package/dist/semantic/vectorSearch.test.js.map +1 -0
- package/dist/setupTests.d.ts +4 -0
- package/dist/setupTests.js +11 -0
- package/dist/setupTests.js.map +1 -0
- package/dist/test-utils/console.d.ts +2 -0
- package/dist/test-utils/console.js +3 -0
- package/dist/test-utils/console.js.map +1 -0
- package/dist/test-utils/factories.d.ts +3 -0
- package/dist/test-utils/factories.js +25 -0
- package/dist/test-utils/factories.js.map +1 -0
- package/dist/test-utils/tempFiles.d.ts +1 -0
- package/dist/test-utils/tempFiles.js +12 -0
- package/dist/test-utils/tempFiles.js.map +1 -0
- package/dist/types.d.ts +304 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/boundedMap.d.ts +8 -0
- package/dist/utils/boundedMap.js +22 -0
- package/dist/utils/boundedMap.js.map +1 -0
- package/dist/utils/boundedMap.test.d.ts +1 -0
- package/dist/utils/boundedMap.test.js +57 -0
- package/dist/utils/boundedMap.test.js.map +1 -0
- package/dist/utils/illustrativePatterns.d.ts +28 -0
- package/dist/utils/illustrativePatterns.js +80 -0
- package/dist/utils/illustrativePatterns.js.map +1 -0
- package/dist/utils/illustrativePatterns.test.d.ts +1 -0
- package/dist/utils/illustrativePatterns.test.js +48 -0
- package/dist/utils/illustrativePatterns.test.js.map +1 -0
- package/dist/utils/incremental.d.ts +36 -0
- package/dist/utils/incremental.js +87 -0
- package/dist/utils/incremental.js.map +1 -0
- package/dist/utils/incremental.test.d.ts +1 -0
- package/dist/utils/incremental.test.js +84 -0
- package/dist/utils/incremental.test.js.map +1 -0
- package/dist/utils/parallel.d.ts +14 -0
- package/dist/utils/parallel.js +43 -0
- package/dist/utils/parallel.js.map +1 -0
- package/dist/utils/parallel.test.d.ts +1 -0
- package/dist/utils/parallel.test.js +48 -0
- package/dist/utils/parallel.test.js.map +1 -0
- package/dist/utils/pathSecurity.d.ts +12 -0
- package/dist/utils/pathSecurity.js +22 -0
- package/dist/utils/pathSecurity.js.map +1 -0
- package/dist/utils/pathSecurity.test.d.ts +1 -0
- package/dist/utils/pathSecurity.test.js +34 -0
- package/dist/utils/pathSecurity.test.js.map +1 -0
- package/dist/utils/similarity.d.ts +12 -0
- package/dist/utils/similarity.js +64 -0
- package/dist/utils/similarity.js.map +1 -0
- package/dist/utils/similarity.test.d.ts +1 -0
- package/dist/utils/similarity.test.js +49 -0
- package/dist/utils/similarity.test.js.map +1 -0
- package/dist/utils/validation.d.ts +13 -0
- package/dist/utils/validation.js +24 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +28 -0
- package/dist/utils/validation.test.js.map +1 -0
- package/dist/validators/codePatternValidator.d.ts +28 -0
- package/dist/validators/codePatternValidator.js +200 -0
- package/dist/validators/codePatternValidator.js.map +1 -0
- package/dist/validators/codePatternValidator.test.d.ts +1 -0
- package/dist/validators/codePatternValidator.test.js +86 -0
- package/dist/validators/codePatternValidator.test.js.map +1 -0
- package/dist/validators/dependencyValidator.d.ts +12 -0
- package/dist/validators/dependencyValidator.js +102 -0
- package/dist/validators/dependencyValidator.js.map +1 -0
- package/dist/validators/dependencyValidator.test.d.ts +1 -0
- package/dist/validators/dependencyValidator.test.js +179 -0
- package/dist/validators/dependencyValidator.test.js.map +1 -0
- package/dist/validators/directoryValidator.d.ts +30 -0
- package/dist/validators/directoryValidator.js +192 -0
- package/dist/validators/directoryValidator.js.map +1 -0
- package/dist/validators/directoryValidator.test.d.ts +1 -0
- package/dist/validators/directoryValidator.test.js +193 -0
- package/dist/validators/directoryValidator.test.js.map +1 -0
- package/dist/validators/fileValidator.d.ts +16 -0
- package/dist/validators/fileValidator.js +114 -0
- package/dist/validators/fileValidator.js.map +1 -0
- package/dist/validators/fileValidator.test.d.ts +1 -0
- package/dist/validators/fileValidator.test.js +108 -0
- package/dist/validators/fileValidator.test.js.map +1 -0
- package/dist/validators/urlValidator.d.ts +25 -0
- package/dist/validators/urlValidator.js +320 -0
- package/dist/validators/urlValidator.js.map +1 -0
- package/dist/validators/urlValidator.test.d.ts +1 -0
- package/dist/validators/urlValidator.test.js +252 -0
- package/dist/validators/urlValidator.test.js.map +1 -0
- package/dist/validators/validationEngine.d.ts +23 -0
- package/dist/validators/validationEngine.js +117 -0
- package/dist/validators/validationEngine.js.map +1 -0
- package/dist/validators/validationEngine.test.d.ts +1 -0
- package/dist/validators/validationEngine.test.js +82 -0
- package/dist/validators/validationEngine.test.js.map +1 -0
- package/dist/validators/versionValidator.d.ts +18 -0
- package/dist/validators/versionValidator.js +211 -0
- package/dist/validators/versionValidator.js.map +1 -0
- package/dist/validators/versionValidator.test.d.ts +1 -0
- package/dist/validators/versionValidator.test.js +308 -0
- package/dist/validators/versionValidator.test.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { VectorSearch } from './vectorSearch.js';
|
|
4
|
+
import { FlagEmbedding } from 'fastembed';
|
|
5
|
+
import { captureConsoleLog, captureConsoleWarn } from '../test-utils/console.js';
|
|
6
|
+
let embedCallCount = 0;
|
|
7
|
+
vi.mock('fastembed', () => {
|
|
8
|
+
const makeEmbedding = (seed) => {
|
|
9
|
+
const arr = new Float32Array(384);
|
|
10
|
+
for (let i = 0; i < 384; i++)
|
|
11
|
+
arr[i] = Math.sin(seed + i * 0.1);
|
|
12
|
+
return arr;
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
EmbeddingModel: { BGESmallENV15: 'BGESmallENV15' },
|
|
16
|
+
FlagEmbedding: {
|
|
17
|
+
init: vi.fn().mockResolvedValue({
|
|
18
|
+
passageEmbed: vi.fn().mockImplementation((texts) => {
|
|
19
|
+
return (async function* () {
|
|
20
|
+
const { embedCallCount: _unused, ...rest } = { embedCallCount };
|
|
21
|
+
void _unused;
|
|
22
|
+
void rest;
|
|
23
|
+
yield texts.map((_t, i) => {
|
|
24
|
+
embedCallCount++;
|
|
25
|
+
return makeEmbedding(embedCallCount + i);
|
|
26
|
+
});
|
|
27
|
+
})();
|
|
28
|
+
}),
|
|
29
|
+
queryEmbed: vi.fn().mockResolvedValue((() => {
|
|
30
|
+
const arr = new Float32Array(384);
|
|
31
|
+
for (let i = 0; i < 384; i++)
|
|
32
|
+
arr[i] = Math.sin(42 + i * 0.1);
|
|
33
|
+
return arr;
|
|
34
|
+
})()),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
function makeDoc(content, docPath = 'docs/test.md') {
|
|
40
|
+
return {
|
|
41
|
+
path: docPath,
|
|
42
|
+
absolutePath: `/project/${docPath}`,
|
|
43
|
+
content,
|
|
44
|
+
format: 'markdown',
|
|
45
|
+
lines: content.split('\n'),
|
|
46
|
+
references: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const cacheDir = '.doc-freshness-cache/vector-test';
|
|
50
|
+
function makeConfig(overrides = {}) {
|
|
51
|
+
return {
|
|
52
|
+
vectorSearch: { enabled: true, similarityThreshold: 0.3 },
|
|
53
|
+
cache: { dir: cacheDir },
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const createVS = (overrides = {}) => new VectorSearch(makeConfig(overrides));
|
|
58
|
+
async function createInitializedVS(overrides = {}) {
|
|
59
|
+
const vs = createVS(overrides);
|
|
60
|
+
await vs.initialize();
|
|
61
|
+
return vs;
|
|
62
|
+
}
|
|
63
|
+
const captureLog = captureConsoleLog;
|
|
64
|
+
const captureWarn = captureConsoleWarn;
|
|
65
|
+
describe('VectorSearch', () => {
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
embedCallCount = 0;
|
|
68
|
+
await fs.promises.unlink(path.join(cacheDir, 'embedding-cache.json')).catch(() => { });
|
|
69
|
+
});
|
|
70
|
+
afterAll(async () => {
|
|
71
|
+
await fs.promises.rm(cacheDir, { recursive: true, force: true }).catch(() => { });
|
|
72
|
+
});
|
|
73
|
+
describe('constructor', () => {
|
|
74
|
+
it('uses configured cache dir', () => {
|
|
75
|
+
const vs = createVS();
|
|
76
|
+
expect(vs.isAvailable()).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('falls back to default cache dir when not configured', () => {
|
|
79
|
+
const vs = new VectorSearch({});
|
|
80
|
+
expect(vs.isAvailable()).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('initialize', () => {
|
|
84
|
+
it('initializes the embedding model and makes it available', async () => {
|
|
85
|
+
const vs = createVS();
|
|
86
|
+
expect(vs.isAvailable()).toBe(false);
|
|
87
|
+
const result = await vs.initialize();
|
|
88
|
+
expect(result).toBe(true);
|
|
89
|
+
expect(vs.isAvailable()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('returns true immediately on subsequent calls', async () => {
|
|
92
|
+
const vs = createVS();
|
|
93
|
+
await vs.initialize();
|
|
94
|
+
const result = await vs.initialize();
|
|
95
|
+
expect(result).toBe(true);
|
|
96
|
+
expect(FlagEmbedding.init).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
it('deduplicates concurrent initialization calls', async () => {
|
|
99
|
+
const vs = createVS();
|
|
100
|
+
const [r1, r2] = await Promise.all([vs.initialize(), vs.initialize()]);
|
|
101
|
+
expect(r1).toBe(true);
|
|
102
|
+
expect(r2).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it('returns false and logs warning when model init fails', async () => {
|
|
105
|
+
vi.spyOn(fs.promises, 'mkdir').mockRejectedValueOnce(new Error('permission denied'));
|
|
106
|
+
const warnSpy = captureWarn();
|
|
107
|
+
const vs = createVS();
|
|
108
|
+
const result = await vs.initialize();
|
|
109
|
+
expect(result).toBe(false);
|
|
110
|
+
expect(vs.isAvailable()).toBe(false);
|
|
111
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('initialization failed'), expect.any(String));
|
|
112
|
+
});
|
|
113
|
+
it('logs verbose messages when verbose is enabled', async () => {
|
|
114
|
+
const logSpy = captureLog();
|
|
115
|
+
const vs = createVS({ verbose: true });
|
|
116
|
+
await vs.initialize();
|
|
117
|
+
const msgs = logSpy.mock.calls.flat().join(' ');
|
|
118
|
+
expect(msgs).toContain('model');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('isAvailable', () => {
|
|
122
|
+
it('returns false before initialization', () => {
|
|
123
|
+
expect(createVS().isAvailable()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('returns true after successful initialization', async () => {
|
|
126
|
+
const vs = createVS();
|
|
127
|
+
await vs.initialize();
|
|
128
|
+
expect(vs.isAvailable()).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('embedQuery', () => {
|
|
132
|
+
it('generates a 384-dimensional embedding', async () => {
|
|
133
|
+
const vs = await createInitializedVS();
|
|
134
|
+
const embedding = await vs.embedQuery('test query');
|
|
135
|
+
expect(embedding).toHaveLength(384);
|
|
136
|
+
expect(embedding.every((v) => typeof v === 'number')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('throws if not initialized', async () => {
|
|
139
|
+
const vs = createVS();
|
|
140
|
+
await expect(vs.embedQuery('test')).rejects.toThrow('not initialized');
|
|
141
|
+
});
|
|
142
|
+
it('throws for empty/whitespace-only text', async () => {
|
|
143
|
+
const vs = await createInitializedVS();
|
|
144
|
+
await expect(vs.embedQuery('')).rejects.toThrow('empty');
|
|
145
|
+
await expect(vs.embedQuery(' ')).rejects.toThrow('empty');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('indexDocumentation', () => {
|
|
149
|
+
it('indexes sections from documents, skipping short ones', async () => {
|
|
150
|
+
const vs = createVS();
|
|
151
|
+
const content = ['# Introduction', '', 'A'.repeat(60), '', '# Short', '', 'tiny', '', '# Details', '', 'B'.repeat(80)].join('\n');
|
|
152
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
153
|
+
const stats = vs.getCacheStats();
|
|
154
|
+
// "Introduction" section (60 chars) and "Details" section (80 chars) indexed
|
|
155
|
+
// "Short" section ("tiny" = 4 chars) skipped
|
|
156
|
+
expect(stats.indexedDocSections).toBe(2);
|
|
157
|
+
});
|
|
158
|
+
it('clears previous index on re-indexing', async () => {
|
|
159
|
+
const vs = createVS();
|
|
160
|
+
const content = '# Section\n\n' + 'X'.repeat(60);
|
|
161
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
162
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(1);
|
|
163
|
+
await vs.indexDocumentation([makeDoc(content, 'other.md')]);
|
|
164
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
it('indexes multiple documents', async () => {
|
|
167
|
+
const vs = createVS();
|
|
168
|
+
const content = '# Heading\n\n' + 'Z'.repeat(60);
|
|
169
|
+
await vs.indexDocumentation([makeDoc(content, 'docs/a.md'), makeDoc(content, 'docs/b.md')]);
|
|
170
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
it('stores section heading and truncated text in metadata', async () => {
|
|
173
|
+
const vs = createVS();
|
|
174
|
+
const content = '# My Heading\n\n' + 'Content here. '.repeat(20);
|
|
175
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
176
|
+
const stats = vs.getCacheStats();
|
|
177
|
+
expect(stats.indexedDocSections).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
it('handles content with no headings (uses "Introduction" as default)', async () => {
|
|
180
|
+
const vs = createVS();
|
|
181
|
+
const content = 'Long paragraph without any headings. '.repeat(5);
|
|
182
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
183
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
it('does nothing when initialization fails', async () => {
|
|
186
|
+
vi.spyOn(fs.promises, 'mkdir').mockRejectedValueOnce(new Error('fail'));
|
|
187
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
188
|
+
const vs = createVS();
|
|
189
|
+
const content = '# Test\n\n' + 'X'.repeat(60);
|
|
190
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
191
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('indexCodeComments', () => {
|
|
195
|
+
it('indexes TypeScript JSDoc comments', async () => {
|
|
196
|
+
const vs = await createInitializedVS();
|
|
197
|
+
await vs.indexCodeComments([
|
|
198
|
+
{
|
|
199
|
+
path: 'src/a.ts',
|
|
200
|
+
content: '/** Handles user authentication and session management */\nfunction auth() {}',
|
|
201
|
+
language: 'typescript',
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
it('indexes Python docstrings', async () => {
|
|
207
|
+
const vs = await createInitializedVS();
|
|
208
|
+
await vs.indexCodeComments([
|
|
209
|
+
{
|
|
210
|
+
path: 'src/app.py',
|
|
211
|
+
content: '"""This module handles data processing and transformation"""\ndef process(): pass',
|
|
212
|
+
language: 'python',
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
216
|
+
});
|
|
217
|
+
it('indexes Go comments', async () => {
|
|
218
|
+
const vs = await createInitializedVS();
|
|
219
|
+
await vs.indexCodeComments([
|
|
220
|
+
{
|
|
221
|
+
path: 'main.go',
|
|
222
|
+
content: '// HandleRequest processes incoming HTTP requests and returns responses\nfunc HandleRequest() {}',
|
|
223
|
+
language: 'go',
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
227
|
+
});
|
|
228
|
+
it('indexes Rust doc comments', async () => {
|
|
229
|
+
const vs = await createInitializedVS();
|
|
230
|
+
await vs.indexCodeComments([
|
|
231
|
+
{
|
|
232
|
+
path: 'lib.rs',
|
|
233
|
+
content: '/// Processes the input configuration and validates all fields\nfn process_config() {}',
|
|
234
|
+
language: 'rust',
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
238
|
+
});
|
|
239
|
+
it('falls back to JavaScript patterns for unknown languages', async () => {
|
|
240
|
+
const vs = await createInitializedVS();
|
|
241
|
+
await vs.indexCodeComments([
|
|
242
|
+
{
|
|
243
|
+
path: 'script.rb',
|
|
244
|
+
content: '// This helper utility performs string sanitization operations\nfunction sanitize() {}',
|
|
245
|
+
language: 'ruby',
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
249
|
+
});
|
|
250
|
+
it('skips comments shorter than 20 characters', async () => {
|
|
251
|
+
const vs = await createInitializedVS();
|
|
252
|
+
await vs.indexCodeComments([
|
|
253
|
+
{
|
|
254
|
+
path: 'src/a.ts',
|
|
255
|
+
content: '// short\nfunction f() {}\n/** Also short */\nfunction g() {}',
|
|
256
|
+
language: 'typescript',
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
expect(vs.getCacheStats().indexedCodeComments).toBe(0);
|
|
260
|
+
});
|
|
261
|
+
it('indexes multiple files', async () => {
|
|
262
|
+
const vs = await createInitializedVS();
|
|
263
|
+
const files = [
|
|
264
|
+
{ path: 'a.ts', content: '/** Validates user input and sanitizes data */\nfunction validate() {}', language: 'typescript' },
|
|
265
|
+
{ path: 'b.ts', content: '/** Processes batch operations for large datasets */\nfunction batch() {}', language: 'typescript' },
|
|
266
|
+
];
|
|
267
|
+
await vs.indexCodeComments(files);
|
|
268
|
+
expect(vs.getCacheStats().indexedCodeComments).toBe(2);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('findMismatches', () => {
|
|
272
|
+
it('returns empty array when no entries indexed', async () => {
|
|
273
|
+
const vs = createVS();
|
|
274
|
+
await vs.initialize();
|
|
275
|
+
const mismatches = await vs.findMismatches();
|
|
276
|
+
expect(mismatches).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
it('returns empty when only doc sections exist (no code to compare)', async () => {
|
|
279
|
+
const vs = createVS();
|
|
280
|
+
const content = '# API\n\nThis function handles authentication and returns a valid token.';
|
|
281
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
282
|
+
const mismatches = await vs.findMismatches();
|
|
283
|
+
// No code entries → bestScore stays 0, which is < threshold
|
|
284
|
+
// But content must contain technical keywords
|
|
285
|
+
expect(Array.isArray(mismatches)).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
it('reports mismatches for technical doc sections with low similarity to code', async () => {
|
|
288
|
+
const vs = createVS();
|
|
289
|
+
const docContent = '# API\n\nThis function handles authentication and returns a session token.';
|
|
290
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
291
|
+
await vs.indexCodeComments([
|
|
292
|
+
{
|
|
293
|
+
path: 'src/unrelated.ts',
|
|
294
|
+
content: '/** Handles database connection pooling and query optimization */\nfunction dbConnect() {}',
|
|
295
|
+
language: 'typescript',
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
// With different embeddings (seeded differently), similarity won't be 1.0
|
|
299
|
+
// Use a very high threshold to force mismatch detection
|
|
300
|
+
const mismatches = await vs.findMismatches(1.0);
|
|
301
|
+
expect(mismatches.length).toBeGreaterThan(0);
|
|
302
|
+
expect(mismatches[0].docPath).toBe('docs/test.md');
|
|
303
|
+
expect(mismatches[0].suggestion).toContain('Documentation may describe');
|
|
304
|
+
expect(mismatches[0].bestMatch).not.toBeNull();
|
|
305
|
+
});
|
|
306
|
+
it('ignores non-technical content even with low similarity', async () => {
|
|
307
|
+
const vs = createVS();
|
|
308
|
+
// Content without technical keywords: function, class, method, API, returns
|
|
309
|
+
const docContent = '# About\n\n' + 'This is a general overview of the project and its goals. '.repeat(3);
|
|
310
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
311
|
+
await vs.indexCodeComments([
|
|
312
|
+
{
|
|
313
|
+
path: 'src/a.ts',
|
|
314
|
+
content: '/** Handles something completely different than what the doc says */\nfunction x() {}',
|
|
315
|
+
language: 'typescript',
|
|
316
|
+
},
|
|
317
|
+
]);
|
|
318
|
+
const mismatches = await vs.findMismatches(1.0);
|
|
319
|
+
expect(mismatches).toHaveLength(0);
|
|
320
|
+
});
|
|
321
|
+
it.each(['function', 'class', 'method', 'API', 'returns'])('detects mismatch when doc contains keyword "%s"', async (keyword) => {
|
|
322
|
+
const vs = createVS();
|
|
323
|
+
const docContent = `# Reference\n\nThis section describes the ${keyword} that handles processing.` + ' '.repeat(20);
|
|
324
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
325
|
+
await vs.indexCodeComments([
|
|
326
|
+
{
|
|
327
|
+
path: 'src/other.ts',
|
|
328
|
+
content: '/** Completely unrelated documentation string here */\nfunction z() {}',
|
|
329
|
+
language: 'typescript',
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
const mismatches = await vs.findMismatches(1.0);
|
|
333
|
+
expect(mismatches.length).toBeGreaterThan(0);
|
|
334
|
+
});
|
|
335
|
+
it('uses config threshold when no explicit threshold is passed', async () => {
|
|
336
|
+
const vs = new VectorSearch(makeConfig({ vectorSearch: { similarityThreshold: 1.0 } }));
|
|
337
|
+
const docContent = '# API\n\nThis function performs data validation and returns errors.';
|
|
338
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
339
|
+
await vs.indexCodeComments([
|
|
340
|
+
{
|
|
341
|
+
path: 'src/a.ts',
|
|
342
|
+
content: '/** Handles file system operations and directory management */\nfunction fsOp() {}',
|
|
343
|
+
language: 'typescript',
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
const mismatches = await vs.findMismatches();
|
|
347
|
+
// threshold=1.0 from config, so almost everything is a mismatch
|
|
348
|
+
expect(mismatches.length).toBeGreaterThan(0);
|
|
349
|
+
});
|
|
350
|
+
it('populates mismatch fields correctly', async () => {
|
|
351
|
+
const vs = createVS();
|
|
352
|
+
const docContent = '# Auth API\n\nThis function handles user authentication and returns a JWT token.';
|
|
353
|
+
await vs.indexDocumentation([makeDoc(docContent, 'docs/auth.md')]);
|
|
354
|
+
await vs.indexCodeComments([
|
|
355
|
+
{
|
|
356
|
+
path: 'src/db.ts',
|
|
357
|
+
content: '/** Manages database connections and query execution lifecycle */\nfunction dbQuery() {}',
|
|
358
|
+
language: 'typescript',
|
|
359
|
+
},
|
|
360
|
+
]);
|
|
361
|
+
const mismatches = await vs.findMismatches(1.0);
|
|
362
|
+
expect(mismatches.length).toBeGreaterThan(0);
|
|
363
|
+
const m = mismatches[0];
|
|
364
|
+
expect(m.docPath).toBe('docs/auth.md');
|
|
365
|
+
expect(m.docSection).toBeDefined();
|
|
366
|
+
expect(m.docText).toBeDefined();
|
|
367
|
+
expect(typeof m.bestMatchScore).toBe('number');
|
|
368
|
+
expect(m.bestMatch).toHaveProperty('path', 'src/db.ts');
|
|
369
|
+
expect(m.bestMatch).toHaveProperty('type', 'code');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
describe('splitIntoSections (tested via indexDocumentation)', () => {
|
|
373
|
+
it('splits by headings of various levels', async () => {
|
|
374
|
+
const vs = createVS();
|
|
375
|
+
const content = [
|
|
376
|
+
'# H1 Section',
|
|
377
|
+
'',
|
|
378
|
+
'X'.repeat(60),
|
|
379
|
+
'',
|
|
380
|
+
'## H2 Section',
|
|
381
|
+
'',
|
|
382
|
+
'Y'.repeat(60),
|
|
383
|
+
'',
|
|
384
|
+
'### H3 Section',
|
|
385
|
+
'',
|
|
386
|
+
'Z'.repeat(60),
|
|
387
|
+
].join('\n');
|
|
388
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
389
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(3);
|
|
390
|
+
});
|
|
391
|
+
it('creates single "Introduction" section when no headings present', async () => {
|
|
392
|
+
const vs = createVS();
|
|
393
|
+
await vs.indexDocumentation([makeDoc('A'.repeat(100))]);
|
|
394
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(1);
|
|
395
|
+
});
|
|
396
|
+
it('handles document with heading but empty body', async () => {
|
|
397
|
+
const vs = createVS();
|
|
398
|
+
await vs.indexDocumentation([makeDoc('# Title\n\nshort')]);
|
|
399
|
+
// "Introduction" before heading is empty, "Title" body is "short" (< 50 chars)
|
|
400
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(0);
|
|
401
|
+
});
|
|
402
|
+
it('handles leading content before the first heading', async () => {
|
|
403
|
+
const vs = createVS();
|
|
404
|
+
const content = 'Preamble content before any heading. '.repeat(5) + '\n\n# First Heading\n\n' + 'Q'.repeat(60);
|
|
405
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
406
|
+
// Preamble as "Introduction" (~185 chars) + "First Heading" body (60 chars) = 2 sections
|
|
407
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(2);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
describe('extractComments (tested via indexCodeComments)', () => {
|
|
411
|
+
async function indexAndCount(content, language) {
|
|
412
|
+
const vs = createVS();
|
|
413
|
+
await vs.initialize();
|
|
414
|
+
await vs.indexCodeComments([{ path: 'test.file', content, language }]);
|
|
415
|
+
return vs.getCacheStats().indexedCodeComments;
|
|
416
|
+
}
|
|
417
|
+
it('extracts JavaScript JSDoc and single-line comments', async () => {
|
|
418
|
+
const content = [
|
|
419
|
+
'/** This function manages the application lifecycle and startup */\n',
|
|
420
|
+
'function start() {}\n',
|
|
421
|
+
'// This is a single-line comment describing the shutdown procedure\n',
|
|
422
|
+
'function stop() {}',
|
|
423
|
+
].join('');
|
|
424
|
+
expect(await indexAndCount(content, 'javascript')).toBe(2);
|
|
425
|
+
});
|
|
426
|
+
it('extracts Python docstrings and hash comments', async () => {
|
|
427
|
+
const content = [
|
|
428
|
+
'"""This module provides utilities for handling data transformation"""\n',
|
|
429
|
+
'# This helper function validates incoming request parameters\n',
|
|
430
|
+
'def validate(): pass',
|
|
431
|
+
].join('');
|
|
432
|
+
expect(await indexAndCount(content, 'python')).toBe(2);
|
|
433
|
+
});
|
|
434
|
+
it('extracts Java Javadoc comments', async () => {
|
|
435
|
+
const content = '/**\n * Manages the entire database connection lifecycle process\n */\nclass DbManager {}';
|
|
436
|
+
expect(await indexAndCount(content, 'java')).toBeGreaterThan(0);
|
|
437
|
+
});
|
|
438
|
+
it('strips comment delimiters and leading asterisks', async () => {
|
|
439
|
+
const vs = createVS();
|
|
440
|
+
await vs.initialize();
|
|
441
|
+
// The JSDoc has delimiters that should be stripped
|
|
442
|
+
await vs.indexCodeComments([
|
|
443
|
+
{
|
|
444
|
+
path: 'test.ts',
|
|
445
|
+
content: '/**\n * Validates authentication tokens and refreshes session data\n */\nfunction validate() {}',
|
|
446
|
+
language: 'typescript',
|
|
447
|
+
},
|
|
448
|
+
]);
|
|
449
|
+
expect(vs.getCacheStats().indexedCodeComments).toBe(1);
|
|
450
|
+
});
|
|
451
|
+
it('ignores empty comments after stripping', async () => {
|
|
452
|
+
const content = '/***/\nfunction f() {}\n//\nfunction g() {}';
|
|
453
|
+
expect(await indexAndCount(content, 'typescript')).toBe(0);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
describe('findNearestSymbol (tested via indexCodeComments metadata)', () => {
|
|
457
|
+
it('finds function name after a comment', async () => {
|
|
458
|
+
const vs = createVS();
|
|
459
|
+
await vs.initialize();
|
|
460
|
+
await vs.indexCodeComments([
|
|
461
|
+
{
|
|
462
|
+
path: 'test.ts',
|
|
463
|
+
content: '/** This function handles authentication */\nfunction authenticate() {}',
|
|
464
|
+
language: 'typescript',
|
|
465
|
+
},
|
|
466
|
+
]);
|
|
467
|
+
// We can't inspect metadata directly, but the indexing should succeed
|
|
468
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
469
|
+
});
|
|
470
|
+
it('finds class name after a comment', async () => {
|
|
471
|
+
const vs = createVS();
|
|
472
|
+
await vs.initialize();
|
|
473
|
+
await vs.indexCodeComments([
|
|
474
|
+
{
|
|
475
|
+
path: 'test.ts',
|
|
476
|
+
content: '/** Manages user session lifecycle and token refresh */\nclass SessionManager {}',
|
|
477
|
+
language: 'typescript',
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
expect(vs.getCacheStats().indexedCodeComments).toBeGreaterThan(0);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
describe('embedding cache', () => {
|
|
484
|
+
it('caches embeddings in memory for repeated content', async () => {
|
|
485
|
+
const vs = createVS();
|
|
486
|
+
const content = '# Same\n\n' + 'Identical content section here. '.repeat(5);
|
|
487
|
+
// Index twice — second run should hit cache
|
|
488
|
+
await vs.indexDocumentation([makeDoc(content, 'a.md')]);
|
|
489
|
+
const firstCount = vs.getCacheStats().cachedEmbeddings;
|
|
490
|
+
expect(firstCount).toBeGreaterThan(0);
|
|
491
|
+
// Re-indexing clears vectorIndex but embedding cache persists
|
|
492
|
+
await vs.indexDocumentation([makeDoc(content, 'b.md')]);
|
|
493
|
+
expect(vs.getCacheStats().cachedEmbeddings).toBe(firstCount);
|
|
494
|
+
});
|
|
495
|
+
it('persists cache to disk via findMismatches (saveCache)', async () => {
|
|
496
|
+
const vs = createVS();
|
|
497
|
+
const content = '# Persist Test\n\nThis function saves embeddings to the file system cache.';
|
|
498
|
+
await vs.indexDocumentation([makeDoc(content)]);
|
|
499
|
+
await vs.findMismatches();
|
|
500
|
+
const cacheFile = path.join(cacheDir, 'embedding-cache.json');
|
|
501
|
+
const exists = await fs.promises
|
|
502
|
+
.access(cacheFile)
|
|
503
|
+
.then(() => true)
|
|
504
|
+
.catch(() => false);
|
|
505
|
+
expect(exists).toBe(true);
|
|
506
|
+
const data = JSON.parse(await fs.promises.readFile(cacheFile, 'utf-8'));
|
|
507
|
+
expect(data.model).toBe('BGESmallENV15');
|
|
508
|
+
expect(data.dimensions).toBe(384);
|
|
509
|
+
expect(typeof data.timestamp).toBe('string');
|
|
510
|
+
expect(Object.keys(data.embeddings).length).toBeGreaterThan(0);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
describe('getCacheStats', () => {
|
|
514
|
+
it('returns zero counts for fresh instance', () => {
|
|
515
|
+
const stats = createVS().getCacheStats();
|
|
516
|
+
expect(stats).toEqual({ cachedEmbeddings: 0, indexedDocSections: 0, indexedCodeComments: 0 });
|
|
517
|
+
});
|
|
518
|
+
it('reflects doc and code counts separately', async () => {
|
|
519
|
+
const vs = createVS();
|
|
520
|
+
const docContent = '# Doc\n\n' + 'D'.repeat(60);
|
|
521
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
522
|
+
await vs.indexCodeComments([
|
|
523
|
+
{
|
|
524
|
+
path: 'a.ts',
|
|
525
|
+
content: '/** Handles request processing and response formatting */\nfunction handle() {}',
|
|
526
|
+
language: 'typescript',
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
const stats = vs.getCacheStats();
|
|
530
|
+
expect(stats.indexedDocSections).toBe(1);
|
|
531
|
+
expect(stats.indexedCodeComments).toBeGreaterThanOrEqual(1);
|
|
532
|
+
expect(stats.cachedEmbeddings).toBeGreaterThan(0);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
describe('clearCache', () => {
|
|
536
|
+
it('resets all internal state', async () => {
|
|
537
|
+
const vs = createVS();
|
|
538
|
+
await vs.initialize();
|
|
539
|
+
const docContent = '# Test\n\n' + 'X'.repeat(60);
|
|
540
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
541
|
+
expect(vs.getCacheStats().indexedDocSections).toBe(1);
|
|
542
|
+
expect(vs.getCacheStats().cachedEmbeddings).toBeGreaterThan(0);
|
|
543
|
+
await vs.clearCache();
|
|
544
|
+
const stats = vs.getCacheStats();
|
|
545
|
+
expect(stats.indexedDocSections).toBe(0);
|
|
546
|
+
expect(stats.indexedCodeComments).toBe(0);
|
|
547
|
+
expect(stats.cachedEmbeddings).toBe(0);
|
|
548
|
+
});
|
|
549
|
+
it('handles missing cache file without error', async () => {
|
|
550
|
+
const vs = new VectorSearch(makeConfig({ cache: { dir: '.doc-freshness-cache/no-such-dir' } }));
|
|
551
|
+
await expect(vs.clearCache()).resolves.not.toThrow();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
describe('loadCache (disk persistence)', () => {
|
|
555
|
+
it('restores embeddings and index from disk', async () => {
|
|
556
|
+
const vs1 = createVS();
|
|
557
|
+
const docContent = '# Cached\n\nThis function handles data persistence and restoration.';
|
|
558
|
+
await vs1.indexDocumentation([makeDoc(docContent)]);
|
|
559
|
+
await vs1.findMismatches();
|
|
560
|
+
const vs2 = createVS();
|
|
561
|
+
await vs2.indexDocumentation([makeDoc(docContent)]);
|
|
562
|
+
const stats = vs2.getCacheStats();
|
|
563
|
+
expect(stats.cachedEmbeddings).toBeGreaterThan(0);
|
|
564
|
+
});
|
|
565
|
+
it('logs loaded count in verbose mode', async () => {
|
|
566
|
+
const vs1 = createVS();
|
|
567
|
+
const docContent = '# Verbose Cache\n\nThis function handles authentication and session management.';
|
|
568
|
+
await vs1.indexDocumentation([makeDoc(docContent)]);
|
|
569
|
+
await vs1.findMismatches();
|
|
570
|
+
const logSpy = captureLog();
|
|
571
|
+
const vs2 = createVS({ verbose: true });
|
|
572
|
+
await vs2.indexDocumentation([makeDoc(docContent)]);
|
|
573
|
+
expect(logSpy.mock.calls.flat().join(' ')).toContain('cached embeddings');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
describe('saveCache error', () => {
|
|
577
|
+
it('logs warning when save fails in verbose mode', async () => {
|
|
578
|
+
const vs = await createInitializedVS({ verbose: true, cache: { dir: '/nonexistent/permission-denied' } });
|
|
579
|
+
const docContent = '# Save Fail\n\nThis function does something complex with data processing.';
|
|
580
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
581
|
+
const warnSpy = captureWarn();
|
|
582
|
+
await vs.findMismatches();
|
|
583
|
+
expect(warnSpy.mock.calls.flat().join(' ')).toContain('Failed to save');
|
|
584
|
+
});
|
|
585
|
+
it('silently ignores save failure in non-verbose mode', async () => {
|
|
586
|
+
const vs = await createInitializedVS({ cache: { dir: '/nonexistent/permission-denied' } });
|
|
587
|
+
const docContent = '# Silent Fail\n\nThis function returns the processed result data.';
|
|
588
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
589
|
+
const warnSpy = captureWarn();
|
|
590
|
+
await vs.findMismatches();
|
|
591
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
describe('model initialization retries', () => {
|
|
595
|
+
it('retries on initialization failure and logs in verbose', async () => {
|
|
596
|
+
const initMock = vi.mocked(FlagEmbedding.init);
|
|
597
|
+
initMock.mockRejectedValueOnce(new Error('transient'));
|
|
598
|
+
initMock.mockRejectedValueOnce(new Error('transient'));
|
|
599
|
+
initMock.mockRejectedValueOnce(new Error('still broken'));
|
|
600
|
+
const warnSpy = captureWarn();
|
|
601
|
+
const vs = createVS({ verbose: true });
|
|
602
|
+
// Only fake setTimeout so that fs.promises and other I/O remain real.
|
|
603
|
+
// Timer advances are done in a loop because initialize() performs async
|
|
604
|
+
// I/O (mkdir, readdir) before reaching the retry setTimeout. A single
|
|
605
|
+
// advanceTimersByTimeAsync call would complete before the timer is even
|
|
606
|
+
// registered. The loop alternates between advancing the fake clock and
|
|
607
|
+
// yielding to the real event loop via setImmediate, giving I/O callbacks
|
|
608
|
+
// a chance to run and register the next setTimeout between iterations.
|
|
609
|
+
vi.useFakeTimers({ toFake: ['setTimeout'] });
|
|
610
|
+
try {
|
|
611
|
+
const initPromise = vs.initialize();
|
|
612
|
+
let settled = false;
|
|
613
|
+
initPromise.finally(() => {
|
|
614
|
+
settled = true;
|
|
615
|
+
});
|
|
616
|
+
while (!settled) {
|
|
617
|
+
vi.advanceTimersByTime(5000);
|
|
618
|
+
await new Promise((r) => setImmediate(r));
|
|
619
|
+
}
|
|
620
|
+
const result = await initPromise;
|
|
621
|
+
expect(result).toBe(false);
|
|
622
|
+
const warns = warnSpy.mock.calls.flat().join(' ');
|
|
623
|
+
expect(warns).toContain('initialization failed');
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
vi.useRealTimers();
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
describe('embed edge cases', () => {
|
|
631
|
+
it('throws when embedding model is not initialized', async () => {
|
|
632
|
+
const vs = createVS();
|
|
633
|
+
await expect(vs.embedQuery('test query')).rejects.toThrow('not initialized');
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
describe('verbose doc/code indexing messages', () => {
|
|
637
|
+
it('logs indexing stats in verbose mode', async () => {
|
|
638
|
+
const logSpy = captureLog();
|
|
639
|
+
const vs = createVS({ verbose: true });
|
|
640
|
+
const docContent = '# Stats\n\nThis function validates and processes input data correctly.';
|
|
641
|
+
await vs.indexDocumentation([makeDoc(docContent)]);
|
|
642
|
+
const output = logSpy.mock.calls.flat().join(' ');
|
|
643
|
+
expect(output).toContain('Indexed documentation');
|
|
644
|
+
});
|
|
645
|
+
it('logs code comment indexing stats in verbose mode', async () => {
|
|
646
|
+
const logSpy = captureLog();
|
|
647
|
+
const vs = await createInitializedVS({ verbose: true });
|
|
648
|
+
await vs.indexCodeComments([
|
|
649
|
+
{
|
|
650
|
+
path: 'verbose.ts',
|
|
651
|
+
content: '/** Processes authentication tokens and validates session integrity */\nfunction auth() {}',
|
|
652
|
+
language: 'typescript',
|
|
653
|
+
},
|
|
654
|
+
]);
|
|
655
|
+
const output = logSpy.mock.calls.flat().join(' ');
|
|
656
|
+
expect(output).toContain('Indexed code comments');
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
//# sourceMappingURL=vectorSearch.test.js.map
|