@softerist/heuristic-mcp 2.1.47 → 3.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.
Files changed (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { EmbeddingsCache } from '../lib/cache.js';
3
+ import fs from 'fs/promises';
4
+
5
+ vi.mock('fs/promises');
6
+
7
+ // Basic mock index for stats
8
+ const mockIndex = {
9
+ setEf: vi.fn(),
10
+ efConstruction: 200,
11
+ m: 48,
12
+ getK: vi.fn(),
13
+ };
14
+
15
+ describe('EmbeddingsCache Helper Methods', () => {
16
+ let cache;
17
+ let config;
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ config = {
22
+ enableCache: true,
23
+ cacheDirectory: '/mock/cache',
24
+ annEnabled: true,
25
+ annEfSearch: 10,
26
+
27
+ callGraphEnabled: true,
28
+ callGraphMaxHops: 2,
29
+ verbose: true,
30
+ };
31
+ cache = new EmbeddingsCache(config);
32
+
33
+ // Spy on console
34
+ vi.spyOn(console, 'error').mockImplementation(() => {});
35
+ });
36
+
37
+ describe('EF Search Configuration', () => {
38
+ it('should validate efSearch input', () => {
39
+ expect(cache.setEfSearch('invalid').success).toBe(false);
40
+ expect(cache.setEfSearch(0).success).toBe(false);
41
+ expect(cache.setEfSearch(1001).success).toBe(false);
42
+ });
43
+
44
+ it('should update config when index not loaded', () => {
45
+ const result = cache.setEfSearch(100);
46
+ expect(result.success).toBe(true);
47
+ expect(result.applied).toBe(false);
48
+ expect(cache.config.annEfSearch).toBe(100);
49
+ });
50
+
51
+ it('should update active index if loaded', () => {
52
+ cache.annIndex = mockIndex;
53
+ const result = cache.setEfSearch(50);
54
+ expect(result.success).toBe(true);
55
+ expect(result.applied).toBe(true);
56
+ expect(mockIndex.setEf).toHaveBeenCalledWith(50);
57
+ });
58
+ });
59
+
60
+ describe('ANN Stats', () => {
61
+ it('should return stats with no index', () => {
62
+ const stats = cache.getAnnStats();
63
+ expect(stats.indexLoaded).toBe(false);
64
+ expect(stats.enabled).toBe(true);
65
+ });
66
+
67
+ it('should return stats with index meta', () => {
68
+ cache.annMeta = {
69
+ metric: 'cosine',
70
+ dim: 128,
71
+ count: 10,
72
+ m: 16,
73
+ efConstruction: 100,
74
+ };
75
+ cache.annIndex = {};
76
+ const stats = cache.getAnnStats();
77
+ expect(stats.indexLoaded).toBe(true);
78
+ expect(stats.config.metric).toBe('cosine');
79
+ });
80
+ });
81
+
82
+ describe('Vector Store Helpers', () => {
83
+ it('normalizes vectors when setting the store', () => {
84
+ const store = [{ file: 'a.js', vector: [1, 2, 3] }];
85
+ cache.setVectorStore(store);
86
+
87
+ const [chunk] = cache.getVectorStore();
88
+ expect(chunk.vector).toBeInstanceOf(Float32Array);
89
+ });
90
+ });
91
+
92
+ describe('Call Graph Helper Methods', () => {
93
+ it('should manage file call data', () => {
94
+ const file = 'test.js';
95
+ const data = { valid: true };
96
+
97
+ cache.setFileCallData(file, data);
98
+ expect(cache.getFileCallData(file)).toBe(data);
99
+ expect(cache.callGraph).toBeNull(); // invalidation
100
+
101
+ cache.removeFileCallData(file);
102
+ expect(cache.getFileCallData(file)).toBeUndefined();
103
+ });
104
+
105
+ it('should clear call graph data and file', async () => {
106
+ cache.setFileCallData('a.js', {});
107
+ await cache.clearCallGraphData({ removeFile: true });
108
+
109
+ expect(cache.getFileCallDataCount()).toBe(0);
110
+ expect(fs.rm).toHaveBeenCalledWith(
111
+ expect.stringContaining('call-graph.json'),
112
+ expect.any(Object)
113
+ );
114
+ });
115
+
116
+ it('should handle pruning', () => {
117
+ cache.setFileCallData('a.js', {});
118
+ cache.setFileCallData('b.js', {});
119
+
120
+ const validFiles = new Set(['a.js']);
121
+ const pruned = cache.pruneCallGraphData(validFiles);
122
+
123
+ expect(pruned).toBe(1);
124
+ expect(cache.getFileCallData('b.js')).toBeUndefined();
125
+ expect(cache.getFileCallData('a.js')).toBeDefined();
126
+ });
127
+
128
+ it('should handle pruning with no valid files set (guard)', () => {
129
+ expect(cache.pruneCallGraphData(null)).toBe(0);
130
+ });
131
+ });
132
+
133
+ describe('Call Graph Lazy Loading', () => {
134
+ beforeEach(() => {
135
+ vi.resetModules();
136
+ // We need to re-import CodebaseIndexer or Cache if we were testing its internal dynamic imports,
137
+ // but we are testing cache.js which does dynamic imports of call-graph.js.
138
+ // We mock call-graph.js
139
+ const fakeGraph = { defines: new Map(), calledBy: new Map() };
140
+ vi.doMock('../lib/call-graph.js', () => ({
141
+ buildCallGraph: vi.fn(() => fakeGraph),
142
+ getRelatedFiles: vi.fn(() => new Map([['related.js', 1]])),
143
+ }));
144
+ });
145
+
146
+ it('rebuildCallGraph should handle import and build', async () => {
147
+ // Re-instantiate to ensure clean state
148
+ const { EmbeddingsCache } = await import('../lib/cache.js');
149
+ const cache = new EmbeddingsCache({ ...config, verbose: true });
150
+
151
+ cache.setFileCallData('f.js', {});
152
+
153
+ // Spy on console to verify success
154
+ const logSpy = vi.spyOn(console, 'info');
155
+
156
+ // Trigger rebuild
157
+ await cache.rebuildCallGraph();
158
+
159
+ // Wait for microtask resolution of dynamic import
160
+ await new Promise((r) => setTimeout(r, 10));
161
+
162
+ expect(cache.callGraph).toBeDefined();
163
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Built graph'));
164
+ });
165
+
166
+ it('rebuildCallGraph should handle errors', async () => {
167
+ vi.doMock('../lib/call-graph.js', () => ({
168
+ buildCallGraph: vi.fn(() => {
169
+ throw new Error('Build failed');
170
+ }),
171
+ }));
172
+
173
+ const { EmbeddingsCache } = await import('../lib/cache.js');
174
+ const cache = new EmbeddingsCache({ ...config, verbose: true });
175
+
176
+ const logSpy = vi.spyOn(console, 'error');
177
+ cache.rebuildCallGraph();
178
+
179
+ await new Promise((r) => setTimeout(r, 10));
180
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to build'));
181
+ });
182
+
183
+ it('getRelatedFiles should rebuild graph if missing', async () => {
184
+ const { EmbeddingsCache } = await import('../lib/cache.js');
185
+ const cache = new EmbeddingsCache(config);
186
+ cache.setFileCallData('f.js', {});
187
+
188
+ const result = await cache.getRelatedFiles(['sym']);
189
+
190
+ expect(result.size).toBe(1); // Mock returns 1 item
191
+ expect(cache.callGraph).toBeDefined();
192
+ });
193
+
194
+ it('getRelatedFiles should return empty if disabled or empty', async () => {
195
+ const { EmbeddingsCache } = await import('../lib/cache.js');
196
+ // Disabled
197
+ let c = new EmbeddingsCache({ ...config, callGraphEnabled: false });
198
+ expect((await c.getRelatedFiles(['s'])).size).toBe(0);
199
+
200
+ // Empty symbols
201
+ c = new EmbeddingsCache(config);
202
+ expect((await c.getRelatedFiles([])).size).toBe(0);
203
+ });
204
+ });
205
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Mock fs to avoid actual file I/O
4
+ vi.mock('fs/promises');
5
+
6
+ // Mock hnswlib-node to fail loading
7
+ vi.mock('hnswlib-node', () => {
8
+ throw new Error('Module not found or load error');
9
+ });
10
+
11
+ describe('EmbeddingsCache HNSW Failures', () => {
12
+ it('should handle hnswlib import failure gracefully', async () => {
13
+ // Need to dynamic import cache to trigger the hnswlib import attempt (if it wasn't already cached by other tests, but Vitest isolates files)
14
+ const { EmbeddingsCache } = await import('../lib/cache.js');
15
+
16
+ const config = {
17
+ enableCache: true,
18
+ annEnabled: true,
19
+ annMinChunks: 1,
20
+ cacheDirectory: '/tmp/test-hnsw-fail',
21
+ fileExtensions: ['js'],
22
+ embeddingModel: 'test',
23
+ };
24
+ const cache = new EmbeddingsCache(config);
25
+
26
+ // Add chunks to trigger ANN condition
27
+ cache.vectorStore = [
28
+ { file: 'a.js', vector: [1, 0] },
29
+ { file: 'b.js', vector: [0, 1] },
30
+ ];
31
+
32
+ // Attempt to ensure index
33
+ const index = await cache.ensureAnnIndex();
34
+
35
+ // Should be null because hnswlib failed to load
36
+ expect(index).toBeNull();
37
+
38
+ // Should also check console.error behavior if desired
39
+ });
40
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { EventEmitter } from 'events';
3
+
4
+ const baseConfig = {
5
+ enableCache: true,
6
+ cacheDirectory: '/cache',
7
+ embeddingModel: 'test-model',
8
+ fileExtensions: ['js'],
9
+ excludePatterns: [],
10
+ annEnabled: false,
11
+ callGraphEnabled: false,
12
+ verbose: true,
13
+ };
14
+
15
+ afterEach(() => {
16
+ vi.resetModules();
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ describe('EmbeddingsCache JSON worker parsing', () => {
21
+ it('uses a worker to parse large JSON files', async () => {
22
+ const Worker = vi.fn(function (_url, options) {
23
+ const worker = new EventEmitter();
24
+ const filePath = options.workerData.filePath;
25
+ const data = filePath.endsWith('embeddings.json') ? [] : {};
26
+ setImmediate(() => {
27
+ worker.emit('message', { ok: true, data });
28
+ });
29
+ return worker;
30
+ });
31
+
32
+ const fsMock = {
33
+ mkdir: vi.fn().mockResolvedValue(undefined),
34
+ stat: vi.fn().mockResolvedValue({ size: 6 * 1024 * 1024 }),
35
+ readFile: vi.fn((filePath) => {
36
+ if (filePath.endsWith('meta.json')) {
37
+ return Promise.resolve(
38
+ JSON.stringify({ version: 1, embeddingModel: baseConfig.embeddingModel })
39
+ );
40
+ }
41
+ return Promise.reject(new Error('missing'));
42
+ }),
43
+ };
44
+
45
+ vi.doMock('worker_threads', () => ({ Worker }));
46
+ vi.doMock('fs/promises', () => ({
47
+ default: fsMock,
48
+ ...fsMock,
49
+ }));
50
+
51
+ const { EmbeddingsCache } = await import('../lib/cache.js');
52
+ const cache = new EmbeddingsCache(baseConfig);
53
+
54
+ await cache.load();
55
+
56
+ expect(Worker).toHaveBeenCalledTimes(2);
57
+ expect(cache.getVectorStore()).toEqual([]);
58
+ });
59
+
60
+ it('logs when the JSON worker reports a parse error', async () => {
61
+ const Worker = vi.fn(function () {
62
+ const worker = new EventEmitter();
63
+ setImmediate(() => {
64
+ worker.emit('message', { ok: false, error: 'bad json' });
65
+ });
66
+ return worker;
67
+ });
68
+
69
+ const fsMock = {
70
+ mkdir: vi.fn().mockResolvedValue(undefined),
71
+ stat: vi.fn().mockResolvedValue({ size: 6 * 1024 * 1024 }),
72
+ readFile: vi.fn((filePath) => {
73
+ if (filePath.endsWith('meta.json')) {
74
+ return Promise.resolve(
75
+ JSON.stringify({ version: 1, embeddingModel: baseConfig.embeddingModel })
76
+ );
77
+ }
78
+ return Promise.reject(new Error('missing'));
79
+ }),
80
+ };
81
+
82
+ vi.doMock('worker_threads', () => ({ Worker }));
83
+ vi.doMock('fs/promises', () => ({
84
+ default: fsMock,
85
+ ...fsMock,
86
+ }));
87
+
88
+ const { EmbeddingsCache } = await import('../lib/cache.js');
89
+ const cache = new EmbeddingsCache(baseConfig);
90
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
91
+
92
+ await cache.load();
93
+
94
+ const hasParseError = consoleSpy.mock.calls.some(
95
+ (call) => typeof call[0] === 'string' && call[0].includes('Failed to parse embeddings.json')
96
+ );
97
+ expect(hasParseError).toBe(true);
98
+
99
+ consoleSpy.mockRestore();
100
+ });
101
+
102
+ it('logs when the JSON worker exits with a failure code', async () => {
103
+ const Worker = vi.fn(function () {
104
+ const worker = new EventEmitter();
105
+ setImmediate(() => {
106
+ worker.emit('exit', 1);
107
+ });
108
+ return worker;
109
+ });
110
+
111
+ const fsMock = {
112
+ mkdir: vi.fn().mockResolvedValue(undefined),
113
+ stat: vi.fn().mockResolvedValue({ size: 6 * 1024 * 1024 }),
114
+ readFile: vi.fn((filePath) => {
115
+ if (filePath.endsWith('meta.json')) {
116
+ return Promise.resolve(
117
+ JSON.stringify({ version: 1, embeddingModel: baseConfig.embeddingModel })
118
+ );
119
+ }
120
+ return Promise.reject(new Error('missing'));
121
+ }),
122
+ };
123
+
124
+ vi.doMock('worker_threads', () => ({ Worker }));
125
+ vi.doMock('fs/promises', () => ({
126
+ default: fsMock,
127
+ ...fsMock,
128
+ }));
129
+
130
+ const { EmbeddingsCache } = await import('../lib/cache.js');
131
+ const cache = new EmbeddingsCache(baseConfig);
132
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
133
+
134
+ await cache.load();
135
+
136
+ const hasExitError = consoleSpy.mock.calls.some(
137
+ (call) => typeof call[0] === 'string' && call[0].includes('JSON worker exited with code')
138
+ );
139
+ expect(hasExitError).toBe(true);
140
+
141
+ consoleSpy.mockRestore();
142
+ });
143
+
144
+ it('ignores subsequent events after settlement (covers settled guard)', async () => {
145
+ // This targets the "if (settled) return;" line in the finish function.
146
+ // We manually invoke the message handler twice to force a second finish call.
147
+ const Worker = vi.fn(function () {
148
+ const worker = {
149
+ once(event, handler) {
150
+ if (event === 'message') {
151
+ setImmediate(() => {
152
+ handler({ ok: true, data: [] });
153
+ handler({ ok: false, error: 'late' });
154
+ });
155
+ }
156
+ return worker;
157
+ },
158
+ removeAllListeners: vi.fn(),
159
+ terminate: vi.fn(() => Promise.resolve()),
160
+ };
161
+ return worker;
162
+ });
163
+
164
+ const fsMock = {
165
+ mkdir: vi.fn().mockResolvedValue(undefined),
166
+ stat: vi.fn().mockResolvedValue({ size: 6 * 1024 * 1024 }),
167
+ readFile: vi.fn((filePath) => {
168
+ if (filePath.endsWith('meta.json')) {
169
+ return Promise.resolve(
170
+ JSON.stringify({ version: 1, embeddingModel: baseConfig.embeddingModel })
171
+ );
172
+ }
173
+ return Promise.reject(new Error('missing'));
174
+ }),
175
+ };
176
+
177
+ vi.doMock('worker_threads', () => ({ Worker }));
178
+ vi.doMock('fs/promises', () => ({
179
+ default: fsMock,
180
+ ...fsMock,
181
+ }));
182
+
183
+ const { EmbeddingsCache } = await import('../lib/cache.js');
184
+ const cache = new EmbeddingsCache(baseConfig);
185
+ // Should not throw or log an error about double-resolution
186
+ await cache.load();
187
+
188
+ expect(Worker).toHaveBeenCalled();
189
+ });
190
+ });
@@ -0,0 +1,102 @@
1
+
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { EmbeddingsCache } from '../lib/cache.js';
4
+
5
+ // Hoist the mock worker
6
+ const { mockWorker } = vi.hoisted(() => {
7
+ const worker = {
8
+ on: vi.fn(),
9
+ once: vi.fn(),
10
+ removeAllListeners: vi.fn(),
11
+ terminate: vi.fn(),
12
+ postMessage: vi.fn(),
13
+ unref: vi.fn()
14
+ };
15
+ return { mockWorker: worker };
16
+ });
17
+
18
+ // Use a shared variable to track execution
19
+ let terminateCatchCalled = false;
20
+
21
+ // Mock fs
22
+ vi.mock('fs/promises', async () => {
23
+ return {
24
+ default: {
25
+ stat: vi.fn().mockResolvedValue({
26
+ size: 6 * 1024 * 1024,
27
+ isDirectory: () => false
28
+ }),
29
+ readFile: vi.fn().mockResolvedValue('[]'),
30
+ mkdir: vi.fn().mockResolvedValue(),
31
+ writeFile: vi.fn().mockResolvedValue(),
32
+ rm: vi.fn().mockResolvedValue(),
33
+ }
34
+ }
35
+ });
36
+
37
+ // Mock worker_threads
38
+ vi.mock('worker_threads', () => {
39
+ return {
40
+ Worker: class {
41
+ constructor() {
42
+ return mockWorker;
43
+ }
44
+ }
45
+ };
46
+ });
47
+
48
+ describe('Cache Worker Termination', () => {
49
+ let cache;
50
+ const config = {
51
+ enableCache: true,
52
+ cacheDirectory: '/test/cache',
53
+ fileExtensions: ['js'],
54
+ embeddingModel: 'test-model'
55
+ };
56
+
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ terminateCatchCalled = false;
60
+
61
+ // Setup worker to simulate successful message
62
+ mockWorker.once.mockImplementation((event, handler) => {
63
+ if (event === 'message') {
64
+ setTimeout(() => {
65
+ // cache.js expects ok: true
66
+ handler({ ok: true, data: [] });
67
+ }, 10);
68
+ }
69
+ });
70
+
71
+ mockWorker.on.mockImplementation((event, handler) => {
72
+ if (event === 'message') {
73
+ setTimeout(() => handler({ ok: true, data: [] }), 10);
74
+ }
75
+ });
76
+
77
+ // Default terminate behavior
78
+ mockWorker.terminate.mockResolvedValue(undefined);
79
+
80
+ cache = new EmbeddingsCache(config);
81
+ });
82
+
83
+ it('should handle worker termination errors (line 29 coverage)', async () => {
84
+ // Setup terminate to return an object with catch() that sets the flag
85
+ // We use mockReturnValue to ensure it returns exactly this object
86
+ const fakePromise = {
87
+ catch: (cb) => {
88
+ terminateCatchCalled = true;
89
+ if (cb) cb();
90
+ return Promise.resolve();
91
+ },
92
+ then: (cb) => { if (cb) cb(); return Promise.resolve(); }
93
+ };
94
+
95
+ mockWorker.terminate.mockReturnValue(fakePromise);
96
+
97
+ await cache.load();
98
+
99
+ expect(mockWorker.terminate).toHaveBeenCalled();
100
+ expect(terminateCatchCalled).toBe(true);
101
+ });
102
+ });