nano-brain 2026.1.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 (79) hide show
  1. package/AGENTS_SNIPPET.md +36 -0
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +281 -0
  4. package/SKILL.md +153 -0
  5. package/bin/cli.js +18 -0
  6. package/index.html +929 -0
  7. package/nano-brain +4 -0
  8. package/opencode-mcp.json +9 -0
  9. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/.openspec.yaml +2 -0
  10. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/design.md +68 -0
  11. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/proposal.md +27 -0
  12. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-integration-testing/spec.md +50 -0
  13. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-server/spec.md +40 -0
  14. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/search-pipeline/spec.md +29 -0
  15. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/tasks.md +37 -0
  16. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/.openspec.yaml +2 -0
  17. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/design.md +111 -0
  18. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/proposal.md +30 -0
  19. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/mcp-server/spec.md +33 -0
  20. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/storage-limits/spec.md +90 -0
  21. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/workspace-scoping/spec.md +66 -0
  22. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/tasks.md +199 -0
  23. package/openspec/changes/codebase-indexing/.openspec.yaml +2 -0
  24. package/openspec/changes/codebase-indexing/design.md +169 -0
  25. package/openspec/changes/codebase-indexing/proposal.md +30 -0
  26. package/openspec/changes/codebase-indexing/specs/codebase-collection/spec.md +187 -0
  27. package/openspec/changes/codebase-indexing/specs/mcp-server/spec.md +36 -0
  28. package/openspec/changes/codebase-indexing/tasks.md +56 -0
  29. package/openspec/specs/mcp-integration-testing/spec.md +50 -0
  30. package/openspec/specs/mcp-server/spec.md +75 -0
  31. package/openspec/specs/search-pipeline/spec.md +29 -0
  32. package/openspec/specs/storage-limits/spec.md +94 -0
  33. package/openspec/specs/workspace-scoping/spec.md +70 -0
  34. package/package.json +34 -0
  35. package/site/build.js +66 -0
  36. package/site/partials/_api.html +83 -0
  37. package/site/partials/_compare.html +100 -0
  38. package/site/partials/_config.html +23 -0
  39. package/site/partials/_features.html +43 -0
  40. package/site/partials/_footer.html +6 -0
  41. package/site/partials/_hero.html +9 -0
  42. package/site/partials/_how-it-works.html +26 -0
  43. package/site/partials/_models.html +18 -0
  44. package/site/partials/_quick-start.html +15 -0
  45. package/site/partials/_stats.html +1 -0
  46. package/site/partials/_tech-stack.html +13 -0
  47. package/site/script.js +12 -0
  48. package/site/shell.html +44 -0
  49. package/site/styles.css +548 -0
  50. package/src/chunker.ts +427 -0
  51. package/src/codebase.ts +331 -0
  52. package/src/collections.ts +192 -0
  53. package/src/embeddings.ts +293 -0
  54. package/src/expansion.ts +79 -0
  55. package/src/harvester.ts +306 -0
  56. package/src/index.ts +503 -0
  57. package/src/reranker.ts +103 -0
  58. package/src/search.ts +294 -0
  59. package/src/server.ts +664 -0
  60. package/src/storage.ts +221 -0
  61. package/src/store.ts +623 -0
  62. package/src/types.ts +202 -0
  63. package/src/watcher.ts +384 -0
  64. package/test/chunker.test.ts +479 -0
  65. package/test/cli.test.ts +309 -0
  66. package/test/codebase-chunker.test.ts +446 -0
  67. package/test/codebase.test.ts +678 -0
  68. package/test/collections.test.ts +571 -0
  69. package/test/harvester.test.ts +636 -0
  70. package/test/integration.test.ts +150 -0
  71. package/test/llm.test.ts +322 -0
  72. package/test/search.test.ts +572 -0
  73. package/test/server.test.ts +541 -0
  74. package/test/storage.test.ts +302 -0
  75. package/test/store.test.ts +465 -0
  76. package/test/watcher.test.ts +656 -0
  77. package/test/workspace.test.ts +239 -0
  78. package/tsconfig.json +19 -0
  79. package/vitest.config.ts +16 -0
@@ -0,0 +1,541 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createMcpServer, formatSearchResults, formatStatus } from '../src/server.js';
3
+ import type { Store, SearchResult, IndexHealth, Collection } from '../src/types.js';
4
+ import type { SearchProviders } from '../src/search.js';
5
+
6
+ function createMockResult(id: string, score: number, snippet: string = 'test snippet'): SearchResult {
7
+ return {
8
+ id,
9
+ path: `path/${id}`,
10
+ collection: 'test',
11
+ title: `Title ${id}`,
12
+ snippet,
13
+ score,
14
+ startLine: 1,
15
+ endLine: 10,
16
+ docid: id.substring(0, 6),
17
+ };
18
+ }
19
+
20
+ function createMockStore(): Store {
21
+ return {
22
+ searchFTS: vi.fn().mockReturnValue([]),
23
+ searchVec: vi.fn().mockReturnValue([]),
24
+ getCachedResult: vi.fn().mockReturnValue(null),
25
+ setCachedResult: vi.fn(),
26
+ close: vi.fn(),
27
+ insertDocument: vi.fn(),
28
+ findDocument: vi.fn(),
29
+ getDocumentBody: vi.fn(),
30
+ deactivateDocument: vi.fn(),
31
+ bulkDeactivateExcept: vi.fn(),
32
+ insertContent: vi.fn(),
33
+ insertEmbedding: vi.fn(),
34
+ ensureVecTable: vi.fn(),
35
+ getIndexHealth: vi.fn().mockReturnValue({
36
+ documentCount: 100,
37
+ chunkCount: 500,
38
+ pendingEmbeddings: 10,
39
+ collections: [
40
+ { name: 'docs', documentCount: 50, path: '/path/to/docs' },
41
+ { name: 'notes', documentCount: 50, path: '/path/to/notes' },
42
+ ],
43
+ databaseSize: 1024 * 1024 * 5,
44
+ modelStatus: {
45
+ embedding: 'loaded',
46
+ reranker: 'available',
47
+ expander: 'missing',
48
+ },
49
+ workspaceStats: [
50
+ { projectHash: 'abc123def456', count: 30 },
51
+ { projectHash: 'global', count: 20 },
52
+ ],
53
+ }),
54
+ getHashesNeedingEmbedding: vi.fn().mockReturnValue([]),
55
+ getWorkspaceStats: vi.fn().mockReturnValue([]),
56
+ deleteDocumentsByPath: vi.fn().mockReturnValue(0),
57
+ cleanOrphanedEmbeddings: vi.fn().mockReturnValue(0),
58
+ modelStatus: {
59
+ embedding: 'loaded',
60
+ reranker: 'available',
61
+ expander: 'missing',
62
+ },
63
+ } as unknown as Store;
64
+ }
65
+
66
+ function createMockProviders(): SearchProviders {
67
+ return {
68
+ embedder: {
69
+ embed: vi.fn().mockResolvedValue({ embedding: new Array(384).fill(0.1) }),
70
+ },
71
+ reranker: null,
72
+ expander: null,
73
+ };
74
+ }
75
+
76
+ describe('Server', () => {
77
+ describe('formatSearchResults', () => {
78
+ it('should format search results correctly', () => {
79
+ const results = [
80
+ createMockResult('doc1', 0.95, 'This is a test snippet'),
81
+ createMockResult('doc2', 0.85, 'Another test snippet'),
82
+ ];
83
+
84
+ const formatted = formatSearchResults(results);
85
+
86
+ expect(formatted).toContain('### 1. Title doc1 (doc1)');
87
+ expect(formatted).toContain('**Path:** path/doc1');
88
+ expect(formatted).toContain('**Score:** 0.950');
89
+ expect(formatted).toContain('**Lines:** 1-10');
90
+ expect(formatted).toContain('This is a test snippet');
91
+ expect(formatted).toContain('### 2. Title doc2 (doc2)');
92
+ expect(formatted).toContain('---');
93
+ });
94
+
95
+ it('should handle empty results', () => {
96
+ const formatted = formatSearchResults([]);
97
+ expect(formatted).toBe('No results found.');
98
+ });
99
+
100
+ it('should format single result without separator', () => {
101
+ const results = [createMockResult('doc1', 0.95)];
102
+ const formatted = formatSearchResults(results);
103
+
104
+ expect(formatted).toContain('### 1. Title doc1');
105
+ expect(formatted).not.toContain('### 2.');
106
+ });
107
+ });
108
+
109
+ describe('formatStatus', () => {
110
+ it('should format health status correctly', () => {
111
+ const health: IndexHealth = {
112
+ documentCount: 100,
113
+ chunkCount: 500,
114
+ pendingEmbeddings: 10,
115
+ collections: [
116
+ { name: 'docs', documentCount: 50, path: '/path/to/docs' },
117
+ { name: 'notes', documentCount: 50, path: '/path/to/notes' },
118
+ ],
119
+ databaseSize: 1024 * 1024 * 5,
120
+ modelStatus: {
121
+ embedding: 'loaded',
122
+ reranker: 'available',
123
+ expander: 'missing',
124
+ },
125
+ };
126
+
127
+ const formatted = formatStatus(health);
128
+
129
+ expect(formatted).toContain('📊 **Memory Index Status**');
130
+ expect(formatted).toContain('Documents: 100');
131
+ expect(formatted).toContain('Chunks: 500');
132
+ expect(formatted).toContain('Pending embeddings: 10');
133
+ expect(formatted).toContain('Database size: 5.0 MB');
134
+ expect(formatted).toContain('**Collections:**');
135
+ expect(formatted).toContain('- docs: 50 docs (/path/to/docs)');
136
+ expect(formatted).toContain('- notes: 50 docs (/path/to/notes)');
137
+ expect(formatted).toContain('**Models:**');
138
+ expect(formatted).toContain('- Embedding: loaded');
139
+ expect(formatted).toContain('- Reranker: available');
140
+ expect(formatted).toContain('- Expander: missing');
141
+ });
142
+
143
+ it('should handle empty collections', () => {
144
+ const health: IndexHealth = {
145
+ documentCount: 0,
146
+ chunkCount: 0,
147
+ pendingEmbeddings: 0,
148
+ collections: [],
149
+ databaseSize: 0,
150
+ modelStatus: {
151
+ embedding: 'missing',
152
+ reranker: 'missing',
153
+ expander: 'missing',
154
+ },
155
+ };
156
+
157
+ const formatted = formatStatus(health);
158
+
159
+ expect(formatted).toContain('Documents: 0');
160
+ expect(formatted).toContain('Database size: 0.0 MB');
161
+ });
162
+ });
163
+
164
+ describe('createMcpServer', () => {
165
+ let mockStore: Store;
166
+ let mockProviders: SearchProviders;
167
+ let collections: Collection[];
168
+
169
+ beforeEach(() => {
170
+ mockStore = createMockStore();
171
+ mockProviders = createMockProviders();
172
+ collections = [
173
+ { name: 'docs', path: '/path/to/docs', pattern: '**/*.md' },
174
+ ];
175
+ });
176
+
177
+ it('should create server instance', () => {
178
+ const server = createMcpServer({
179
+ store: mockStore,
180
+ providers: mockProviders,
181
+ collections,
182
+ configPath: '/path/to/config.yaml',
183
+ outputDir: '/tmp/output',
184
+ currentProjectHash: 'testws123456',
185
+ });
186
+ expect(server).toBeDefined();
187
+ expect(server.server).toBeDefined();
188
+ });
189
+ });
190
+
191
+ describe('memory_search tool logic', () => {
192
+ it('should call store.searchFTS with correct params', () => {
193
+ const mockStore = createMockStore();
194
+ const results = [createMockResult('doc1', 0.95)];
195
+ vi.mocked(mockStore.searchFTS).mockReturnValue(results);
196
+
197
+ const searchResults = mockStore.searchFTS('test query', 5, 'docs');
198
+
199
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test query', 5, 'docs');
200
+ expect(searchResults).toHaveLength(1);
201
+ expect(searchResults[0].id).toBe('doc1');
202
+ });
203
+
204
+ it('should format results correctly', () => {
205
+ const mockStore = createMockStore();
206
+ const results = [createMockResult('doc1', 0.95)];
207
+ vi.mocked(mockStore.searchFTS).mockReturnValue(results);
208
+
209
+ const searchResults = mockStore.searchFTS('test', 10, undefined);
210
+ const formatted = formatSearchResults(searchResults);
211
+
212
+ expect(formatted).toContain('Title doc1');
213
+ });
214
+ });
215
+
216
+ describe('memory_vsearch tool logic', () => {
217
+ it('should use embedder when available', async () => {
218
+ const mockStore = createMockStore();
219
+ const mockProviders = createMockProviders();
220
+ const results = [createMockResult('doc1', 0.95)];
221
+ vi.mocked(mockStore.searchVec).mockReturnValue(results);
222
+
223
+ const { embedding } = await mockProviders.embedder!.embed('test query');
224
+ const searchResults = mockStore.searchVec('test query', embedding, 5, undefined);
225
+
226
+ expect(mockProviders.embedder!.embed).toHaveBeenCalledWith('test query');
227
+ expect(mockStore.searchVec).toHaveBeenCalled();
228
+ expect(searchResults).toHaveLength(1);
229
+ });
230
+
231
+ it('should fall back to FTS when embedder not available', () => {
232
+ const mockStore = createMockStore();
233
+ const results = [createMockResult('doc1', 0.95)];
234
+ vi.mocked(mockStore.searchFTS).mockReturnValue(results);
235
+
236
+ const searchResults = mockStore.searchFTS('test query', 10, undefined);
237
+
238
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test query', 10, undefined);
239
+ expect(searchResults).toHaveLength(1);
240
+ });
241
+
242
+ it('should handle embedding failure', async () => {
243
+ const mockProviders = createMockProviders();
244
+ vi.mocked(mockProviders.embedder!.embed).mockRejectedValue(new Error('Embedding failed'));
245
+
246
+ await expect(mockProviders.embedder!.embed('test query')).rejects.toThrow('Embedding failed');
247
+ });
248
+ });
249
+
250
+ describe('memory_get tool logic', () => {
251
+ it('should retrieve document by path', () => {
252
+ const mockStore = createMockStore();
253
+ vi.mocked(mockStore.findDocument).mockReturnValue({
254
+ id: 1,
255
+ collection: 'docs',
256
+ path: '/path/to/doc.md',
257
+ title: 'Test Doc',
258
+ hash: 'abc123',
259
+ createdAt: '2024-01-01T00:00:00Z',
260
+ modifiedAt: '2024-01-01T00:00:00Z',
261
+ active: true,
262
+ });
263
+ vi.mocked(mockStore.getDocumentBody).mockReturnValue('Document content here');
264
+
265
+ const doc = mockStore.findDocument('/path/to/doc.md');
266
+ const body = mockStore.getDocumentBody(doc!.hash, undefined, undefined);
267
+
268
+ expect(mockStore.findDocument).toHaveBeenCalledWith('/path/to/doc.md');
269
+ expect(mockStore.getDocumentBody).toHaveBeenCalledWith('abc123', undefined, undefined);
270
+ expect(body).toBe('Document content here');
271
+ });
272
+
273
+ it('should handle # prefix in docid', () => {
274
+ const mockStore = createMockStore();
275
+ vi.mocked(mockStore.findDocument).mockReturnValue({
276
+ id: 1,
277
+ collection: 'docs',
278
+ path: '/path/to/doc.md',
279
+ title: 'Test Doc',
280
+ hash: 'abc123',
281
+ createdAt: '2024-01-01T00:00:00Z',
282
+ modifiedAt: '2024-01-01T00:00:00Z',
283
+ active: true,
284
+ });
285
+
286
+ const id = '#abc123';
287
+ const docid = id.startsWith('#') ? id.slice(1) : id;
288
+ mockStore.findDocument(docid);
289
+
290
+ expect(mockStore.findDocument).toHaveBeenCalledWith('abc123');
291
+ });
292
+
293
+ it('should return null for missing document', () => {
294
+ const mockStore = createMockStore();
295
+ vi.mocked(mockStore.findDocument).mockReturnValue(null);
296
+
297
+ const doc = mockStore.findDocument('nonexistent');
298
+
299
+ expect(doc).toBeNull();
300
+ });
301
+
302
+ it('should pass fromLine and maxLines parameters', () => {
303
+ const mockStore = createMockStore();
304
+ vi.mocked(mockStore.findDocument).mockReturnValue({
305
+ id: 1,
306
+ collection: 'docs',
307
+ path: '/path/to/doc.md',
308
+ title: 'Test Doc',
309
+ hash: 'abc123',
310
+ createdAt: '2024-01-01T00:00:00Z',
311
+ modifiedAt: '2024-01-01T00:00:00Z',
312
+ active: true,
313
+ });
314
+ vi.mocked(mockStore.getDocumentBody).mockReturnValue('Partial content');
315
+
316
+ const doc = mockStore.findDocument('doc.md');
317
+ mockStore.getDocumentBody(doc!.hash, 10, 20);
318
+
319
+ expect(mockStore.getDocumentBody).toHaveBeenCalledWith('abc123', 10, 20);
320
+ });
321
+ });
322
+
323
+ describe('memory_multi_get tool logic', () => {
324
+ it('should retrieve multiple documents', () => {
325
+ const mockStore = createMockStore();
326
+
327
+ vi.mocked(mockStore.findDocument)
328
+ .mockReturnValueOnce({
329
+ id: 1,
330
+ collection: 'docs',
331
+ path: '/path/to/doc1.md',
332
+ title: 'Doc 1',
333
+ hash: 'hash1',
334
+ createdAt: '2024-01-01T00:00:00Z',
335
+ modifiedAt: '2024-01-01T00:00:00Z',
336
+ active: true,
337
+ })
338
+ .mockReturnValueOnce({
339
+ id: 2,
340
+ collection: 'docs',
341
+ path: '/path/to/doc2.md',
342
+ title: 'Doc 2',
343
+ hash: 'hash2',
344
+ createdAt: '2024-01-01T00:00:00Z',
345
+ modifiedAt: '2024-01-01T00:00:00Z',
346
+ active: true,
347
+ });
348
+
349
+ vi.mocked(mockStore.getDocumentBody)
350
+ .mockReturnValueOnce('Content 1')
351
+ .mockReturnValueOnce('Content 2');
352
+
353
+ const ids = 'doc1,doc2'.split(',').map(s => s.trim());
354
+ const results: string[] = [];
355
+
356
+ for (const id of ids) {
357
+ const doc = mockStore.findDocument(id);
358
+ if (doc) {
359
+ const body = mockStore.getDocumentBody(doc.hash);
360
+ results.push(`### ${doc.title}\n\n${body}\n\n`);
361
+ }
362
+ }
363
+
364
+ const combined = results.join('');
365
+ expect(combined).toContain('### Doc 1');
366
+ expect(combined).toContain('Content 1');
367
+ expect(combined).toContain('### Doc 2');
368
+ expect(combined).toContain('Content 2');
369
+ });
370
+
371
+ it('should handle missing documents gracefully', () => {
372
+ const mockStore = createMockStore();
373
+ vi.mocked(mockStore.findDocument).mockReturnValue(null);
374
+
375
+ const doc1 = mockStore.findDocument('missing1');
376
+ const doc2 = mockStore.findDocument('missing2');
377
+
378
+ expect(doc1).toBeNull();
379
+ expect(doc2).toBeNull();
380
+ });
381
+
382
+ it('should respect maxBytes limit', () => {
383
+ const mockStore = createMockStore();
384
+
385
+ vi.mocked(mockStore.findDocument).mockReturnValue({
386
+ id: 1,
387
+ collection: 'docs',
388
+ path: '/path/to/doc.md',
389
+ title: 'Doc',
390
+ hash: 'hash1',
391
+ createdAt: '2024-01-01T00:00:00Z',
392
+ modifiedAt: '2024-01-01T00:00:00Z',
393
+ active: true,
394
+ });
395
+
396
+ vi.mocked(mockStore.getDocumentBody).mockReturnValue('x'.repeat(1000));
397
+
398
+ const maxBytes = 100;
399
+ let totalBytes = 0;
400
+ const results: string[] = [];
401
+
402
+ for (const id of ['doc1', 'doc2', 'doc3']) {
403
+ const doc = mockStore.findDocument(id);
404
+ if (doc) {
405
+ const body = mockStore.getDocumentBody(doc.hash);
406
+ const docText = `### ${doc.title}\n\n${body}\n\n`;
407
+
408
+ if (totalBytes + docText.length > maxBytes) {
409
+ results.push('⚠️ Reached maxBytes limit');
410
+ break;
411
+ }
412
+
413
+ results.push(docText);
414
+ totalBytes += docText.length;
415
+ }
416
+ }
417
+
418
+ expect(results.join('')).toContain('⚠️ Reached maxBytes limit');
419
+ });
420
+ });
421
+
422
+ describe('memory_status tool logic', () => {
423
+ it('should return formatted health info', () => {
424
+ const mockStore = createMockStore();
425
+
426
+ const health = mockStore.getIndexHealth();
427
+ const formatted = formatStatus(health);
428
+
429
+ expect(mockStore.getIndexHealth).toHaveBeenCalled();
430
+ expect(formatted).toContain('📊 **Memory Index Status**');
431
+ expect(formatted).toContain('Documents: 100');
432
+ expect(formatted).toContain('Chunks: 500');
433
+ });
434
+ });
435
+
436
+ describe('error handling', () => {
437
+ it('should handle errors in store operations', () => {
438
+ const mockStore = createMockStore();
439
+ vi.mocked(mockStore.searchFTS).mockImplementation(() => {
440
+ throw new Error('Database error');
441
+ });
442
+
443
+ expect(() => mockStore.searchFTS('test', 10, undefined)).toThrow('Database error');
444
+ });
445
+ });
446
+
447
+ describe('server watcher integration', () => {
448
+ it('should export startServer that accepts watcher options', async () => {
449
+ const { startServer } = await import('../src/server.js');
450
+ expect(startServer).toBeDefined();
451
+ expect(typeof startServer).toBe('function');
452
+ });
453
+ });
454
+
455
+ describe('workspace scoping', () => {
456
+ it('should pass workspace parameter to searchFTS', () => {
457
+ const mockStore = createMockStore();
458
+ const currentProjectHash = 'testws123456';
459
+
460
+ const workspace = 'all';
461
+ const effectiveWorkspace = workspace === 'all' ? 'all' : (workspace || currentProjectHash);
462
+ mockStore.searchFTS('test', 10, undefined, effectiveWorkspace);
463
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test', 10, undefined, 'all');
464
+ });
465
+
466
+ it('should default to currentProjectHash when no workspace', () => {
467
+ const mockStore = createMockStore();
468
+ const currentProjectHash = 'testws123456';
469
+
470
+ const workspace = undefined;
471
+ const effectiveWorkspace = workspace === 'all' ? 'all' : (workspace || currentProjectHash);
472
+ mockStore.searchFTS('test', 10, undefined, effectiveWorkspace);
473
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test', 10, undefined, 'testws123456');
474
+ });
475
+
476
+ it('should pass workspace to searchVec', async () => {
477
+ const mockStore = createMockStore();
478
+ const mockProviders = createMockProviders();
479
+ const currentProjectHash = 'testws123456';
480
+
481
+ const workspace = 'all';
482
+ const effectiveWorkspace = workspace === 'all' ? 'all' : (workspace || currentProjectHash);
483
+ const { embedding } = await mockProviders.embedder!.embed('test');
484
+ mockStore.searchVec('test', embedding, 10, undefined, effectiveWorkspace);
485
+ expect(mockStore.searchVec).toHaveBeenCalledWith(
486
+ 'test',
487
+ expect.any(Array),
488
+ 10,
489
+ undefined,
490
+ 'all'
491
+ );
492
+ });
493
+
494
+ it('should pass workspace to searchFTS via hybridSearch logic', () => {
495
+ const mockStore = createMockStore();
496
+ const currentProjectHash = 'testws123456';
497
+
498
+ const workspace = 'all';
499
+ const effectiveWorkspace = workspace === 'all' ? 'all' : (workspace || currentProjectHash);
500
+ mockStore.searchFTS('test', 20, undefined, effectiveWorkspace);
501
+
502
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test', 20, undefined, 'all');
503
+ });
504
+
505
+ it('should include workspaceStats in formatStatus output', () => {
506
+ const health = {
507
+ documentCount: 100,
508
+ chunkCount: 500,
509
+ pendingEmbeddings: 10,
510
+ collections: [],
511
+ databaseSize: 1024 * 1024 * 5,
512
+ modelStatus: {
513
+ embedding: 'loaded',
514
+ reranker: 'available',
515
+ expander: 'missing',
516
+ },
517
+ workspaceStats: [
518
+ { projectHash: 'abc123def456', count: 30 },
519
+ { projectHash: 'global', count: 20 },
520
+ ],
521
+ };
522
+
523
+ const formatted = formatStatus(health);
524
+
525
+ expect(formatted).toContain('**Workspaces:**');
526
+ expect(formatted).toContain('abc123def456: 30 docs');
527
+ expect(formatted).toContain('global: 20 docs');
528
+ });
529
+
530
+ it('should handle explicit workspace hash', () => {
531
+ const mockStore = createMockStore();
532
+ const currentProjectHash = 'testws123456';
533
+
534
+ const workspace = 'otherws789012';
535
+ const effectiveWorkspace = workspace === 'all' ? 'all' : (workspace || currentProjectHash);
536
+ mockStore.searchFTS('test', 10, undefined, effectiveWorkspace);
537
+
538
+ expect(mockStore.searchFTS).toHaveBeenCalledWith('test', 10, undefined, 'otherws789012');
539
+ });
540
+ });
541
+ });