@afterxleep/doc-bot 1.17.0 → 1.18.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 (82) hide show
  1. package/package.json +7 -4
  2. package/src/__tests__/temp-docs-1756129972061/test.md +5 -0
  3. package/src/__tests__/temp-docs-1756129972071/test.md +5 -0
  4. package/src/__tests__/temp-docs-1756129972075/test.md +5 -0
  5. package/src/__tests__/temp-docs-1756129972077/test.md +5 -0
  6. package/src/__tests__/temp-docs-1756129972079/test.md +5 -0
  7. package/src/__tests__/temp-docs-1756130189361/test.md +5 -0
  8. package/src/__tests__/temp-docs-1756130189372/test.md +5 -0
  9. package/src/__tests__/temp-docs-1756130189375/test.md +5 -0
  10. package/src/__tests__/temp-docs-1756130189378/test.md +5 -0
  11. package/src/__tests__/temp-docs-1756130189379/test.md +5 -0
  12. package/src/__tests__/temp-docs-1756130271128/test.md +5 -0
  13. package/src/__tests__/temp-docs-1756130271139/test.md +5 -0
  14. package/src/__tests__/temp-docs-1756130271142/test.md +5 -0
  15. package/src/__tests__/temp-docs-1756130271145/test.md +5 -0
  16. package/src/__tests__/temp-docs-1756130271146/test.md +5 -0
  17. package/src/__tests__/temp-docs-1756130687030/test.md +5 -0
  18. package/src/__tests__/temp-docs-1756130687044/test.md +5 -0
  19. package/src/__tests__/temp-docs-1756130687048/test.md +5 -0
  20. package/src/__tests__/temp-docs-1756130687051/test.md +5 -0
  21. package/src/__tests__/temp-docs-1756130687053/test.md +5 -0
  22. package/src/__tests__/temp-docs-1756131694925/test.md +5 -0
  23. package/src/__tests__/temp-docs-1756131694937/test.md +5 -0
  24. package/src/__tests__/temp-docs-1756131694941/test.md +5 -0
  25. package/src/__tests__/temp-docs-1756131694944/test.md +5 -0
  26. package/src/__tests__/temp-docs-1756131694946/test.md +5 -0
  27. package/src/__tests__/temp-docs-1756133998710/test.md +5 -0
  28. package/src/__tests__/temp-docs-1756133998721/test.md +5 -0
  29. package/src/__tests__/temp-docs-1756133998724/test.md +5 -0
  30. package/src/__tests__/temp-docs-1756133998727/test.md +5 -0
  31. package/src/__tests__/temp-docs-1756133998729/test.md +5 -0
  32. package/src/__tests__/temp-docs-1756134345935/test.md +5 -0
  33. package/src/__tests__/temp-docs-1756134345948/test.md +5 -0
  34. package/src/__tests__/temp-docs-1756134345952/test.md +5 -0
  35. package/src/__tests__/temp-docs-1756134345954/test.md +5 -0
  36. package/src/__tests__/temp-docs-1756134345957/test.md +5 -0
  37. package/src/__tests__/temp-docsets-1756129972079/2e443167/Mock.docset/Contents/Info.plist +10 -0
  38. package/src/__tests__/temp-docsets-1756129972079/2e443167/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  39. package/src/__tests__/temp-docsets-1756129972079/Mock.docset/Contents/Info.plist +10 -0
  40. package/src/__tests__/temp-docsets-1756129972079/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  41. package/src/__tests__/temp-docsets-1756129972079/docsets.json +10 -0
  42. package/src/__tests__/temp-docsets-1756130189379/Mock.docset/Contents/Info.plist +10 -0
  43. package/src/__tests__/temp-docsets-1756130189379/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  44. package/src/__tests__/temp-docsets-1756130189379/a4934c14/Mock.docset/Contents/Info.plist +10 -0
  45. package/src/__tests__/temp-docsets-1756130189379/a4934c14/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  46. package/src/__tests__/temp-docsets-1756130189379/docsets.json +10 -0
  47. package/src/__tests__/temp-docsets-1756130271146/3f8acbb2/Mock.docset/Contents/Info.plist +10 -0
  48. package/src/__tests__/temp-docsets-1756130271146/3f8acbb2/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  49. package/src/__tests__/temp-docsets-1756130271146/Mock.docset/Contents/Info.plist +10 -0
  50. package/src/__tests__/temp-docsets-1756130271146/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  51. package/src/__tests__/temp-docsets-1756130271146/docsets.json +10 -0
  52. package/src/__tests__/temp-docsets-1756130687053/6810e6bd/Mock.docset/Contents/Info.plist +10 -0
  53. package/src/__tests__/temp-docsets-1756130687053/6810e6bd/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  54. package/src/__tests__/temp-docsets-1756130687053/Mock.docset/Contents/Info.plist +10 -0
  55. package/src/__tests__/temp-docsets-1756130687053/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  56. package/src/__tests__/temp-docsets-1756130687053/docsets.json +10 -0
  57. package/src/__tests__/temp-docsets-1756131694946/Mock.docset/Contents/Info.plist +10 -0
  58. package/src/__tests__/temp-docsets-1756131694946/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  59. package/src/__tests__/temp-docsets-1756131694946/dd703046/Mock.docset/Contents/Info.plist +10 -0
  60. package/src/__tests__/temp-docsets-1756131694946/dd703046/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  61. package/src/__tests__/temp-docsets-1756131694946/docsets.json +10 -0
  62. package/src/__tests__/temp-docsets-1756133998729/9e061136/Mock.docset/Contents/Info.plist +10 -0
  63. package/src/__tests__/temp-docsets-1756133998729/9e061136/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  64. package/src/__tests__/temp-docsets-1756133998729/Mock.docset/Contents/Info.plist +10 -0
  65. package/src/__tests__/temp-docsets-1756133998729/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  66. package/src/__tests__/temp-docsets-1756133998729/docsets.json +10 -0
  67. package/src/__tests__/temp-docsets-1756134345957/03e730af/Mock.docset/Contents/Info.plist +10 -0
  68. package/src/__tests__/temp-docsets-1756134345957/03e730af/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  69. package/src/__tests__/temp-docsets-1756134345957/Mock.docset/Contents/Info.plist +10 -0
  70. package/src/__tests__/temp-docsets-1756134345957/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  71. package/src/__tests__/temp-docsets-1756134345957/docsets.json +10 -0
  72. package/src/index.js +4 -6
  73. package/src/services/DocumentationService.js +26 -1
  74. package/prompts/file-docs.md +0 -69
  75. package/prompts/global-rules.md +0 -142
  76. package/prompts/mandatory-rules.md +0 -90
  77. package/prompts/search-results.md +0 -59
  78. package/prompts/system-prompt.md +0 -270
  79. package/src/__tests__/docset-integration.test.js +0 -146
  80. package/src/services/__tests__/DocumentationService.test.js +0 -318
  81. package/src/services/__tests__/UnifiedSearchService.test.js +0 -302
  82. package/src/services/docset/__tests__/EnhancedDocsetDatabase.test.js +0 -324
@@ -1,302 +0,0 @@
1
- import { UnifiedSearchService } from '../UnifiedSearchService.js';
2
- import { DocumentationService } from '../DocumentationService.js';
3
- import { MultiDocsetDatabase } from '../docset/database.js';
4
- import { jest } from '@jest/globals';
5
-
6
- describe('UnifiedSearchService', () => {
7
- let unifiedSearchService;
8
- let mockDocumentationService;
9
- let mockMultiDocsetDatabase;
10
-
11
- beforeEach(() => {
12
- // Mock DocumentationService
13
- mockDocumentationService = {
14
- searchDocuments: jest.fn().mockResolvedValue([]),
15
- documents: new Map()
16
- };
17
-
18
- // Mock MultiDocsetDatabase
19
- mockMultiDocsetDatabase = {
20
- searchWithTerms: jest.fn().mockReturnValue([]),
21
- databases: new Map(),
22
- getStats: jest.fn().mockReturnValue([])
23
- };
24
-
25
- unifiedSearchService = new UnifiedSearchService(
26
- mockDocumentationService,
27
- mockMultiDocsetDatabase
28
- );
29
- });
30
-
31
- describe('parseQuery', () => {
32
- it('should parse query into terms removing stop words', () => {
33
- const terms = unifiedSearchService.parseQuery('How to use AlarmKit Framework');
34
- expect(terms).toEqual(['use', 'alarmkit', 'framework']);
35
- });
36
-
37
- it('should handle queries with special characters', () => {
38
- const terms = unifiedSearchService.parseQuery('URLSession.shared configuration!');
39
- expect(terms).toEqual(['urlsessionshared', 'configuration']);
40
- });
41
-
42
- it('should filter out short terms', () => {
43
- const terms = unifiedSearchService.parseQuery('a I URLSession x');
44
- expect(terms).toEqual(['urlsession']);
45
- });
46
-
47
- it('should preserve dots, dashes and underscores', () => {
48
- const terms = unifiedSearchService.parseQuery('URLSession.shared my-function test_var');
49
- expect(terms).toEqual(['urlsession.shared', 'my-function', 'test_var']);
50
- });
51
-
52
- it('should handle empty query', () => {
53
- const terms = unifiedSearchService.parseQuery('');
54
- expect(terms).toEqual([]);
55
- });
56
- });
57
-
58
- describe('search', () => {
59
- it('should return empty array for empty query', async () => {
60
- const results = await unifiedSearchService.search('');
61
- expect(results).toEqual([]);
62
- });
63
-
64
- it('should return empty array for query with only stop words', async () => {
65
- const results = await unifiedSearchService.search('the is are was');
66
- expect(results).toEqual([]);
67
- });
68
-
69
- it('should search both local and docset documentation', async () => {
70
- const localResults = [
71
- {
72
- fileName: 'test.md',
73
- content: 'Test content',
74
- metadata: { title: 'Test' },
75
- relevanceScore: 50
76
- }
77
- ];
78
-
79
- const docsetResults = [
80
- {
81
- name: 'TestClass',
82
- type: 'Class',
83
- path: 'test.html',
84
- docsetId: 'mock',
85
- docsetName: 'Mock',
86
- relevanceScore: 40
87
- }
88
- ];
89
-
90
- mockDocumentationService.searchDocuments.mockResolvedValue(localResults);
91
- mockMultiDocsetDatabase.searchWithTerms.mockReturnValue(docsetResults);
92
-
93
- const results = await unifiedSearchService.search('test');
94
-
95
- expect(mockDocumentationService.searchDocuments).toHaveBeenCalledWith('test');
96
- expect(mockMultiDocsetDatabase.searchWithTerms).toHaveBeenCalledWith(
97
- ['test'],
98
- { type: undefined, docsetId: undefined, limit: 10 }
99
- );
100
-
101
- expect(results).toHaveLength(2);
102
- // Local result should be boosted and come first
103
- expect(results[0].type).toBe('local');
104
- expect(results[0].relevanceScore).toBe(250); // 50 * 5
105
- expect(results[1].type).toBe('docset');
106
- });
107
-
108
- it('should not search local docs when docsetId is specified', async () => {
109
- mockMultiDocsetDatabase.searchWithTerms.mockReturnValue([]);
110
-
111
- await unifiedSearchService.search('test', { docsetId: 'specific-docset' });
112
-
113
- expect(mockDocumentationService.searchDocuments).not.toHaveBeenCalled();
114
- expect(mockMultiDocsetDatabase.searchWithTerms).toHaveBeenCalledWith(
115
- ['test'],
116
- { type: undefined, docsetId: 'specific-docset', limit: 10 }
117
- );
118
- });
119
-
120
- it('should apply quality filtering when high-quality results exist', async () => {
121
- const mixedResults = [
122
- {
123
- fileName: 'high-quality.md',
124
- content: 'Highly relevant',
125
- metadata: { title: 'Perfect Match' },
126
- relevanceScore: 80
127
- },
128
- {
129
- fileName: 'medium.md',
130
- content: 'Somewhat relevant',
131
- metadata: { title: 'Partial Match' },
132
- relevanceScore: 30
133
- },
134
- {
135
- fileName: 'low.md',
136
- content: 'Barely relevant',
137
- metadata: { title: 'Weak Match' },
138
- relevanceScore: 5
139
- }
140
- ];
141
-
142
- mockDocumentationService.searchDocuments.mockResolvedValue(mixedResults);
143
-
144
- const results = await unifiedSearchService.search('test', { limit: 10 });
145
-
146
- // Should only include high-quality results (score >= 50 after boosting)
147
- expect(results.length).toBe(2);
148
- expect(results[0].relevanceScore).toBe(400); // 80 * 5
149
- expect(results[1].relevanceScore).toBe(150); // 30 * 5
150
- });
151
-
152
- it('should handle search errors gracefully', async () => {
153
- mockDocumentationService.searchDocuments.mockRejectedValue(new Error('Search failed'));
154
- mockMultiDocsetDatabase.searchWithTerms.mockImplementation(() => {
155
- throw new Error('Database error');
156
- });
157
-
158
- const results = await unifiedSearchService.search('test');
159
- expect(results).toEqual([]);
160
- });
161
- });
162
-
163
- describe('normalizeLocalResults', () => {
164
- it('should normalize local documentation results', () => {
165
- const localResults = [
166
- {
167
- fileName: 'guide.md',
168
- content: 'Full content here',
169
- metadata: {
170
- title: 'User Guide',
171
- description: 'A comprehensive guide'
172
- },
173
- relevanceScore: 75,
174
- snippet: 'This is a snippet...',
175
- matchedTerms: ['guide', 'user']
176
- }
177
- ];
178
-
179
- const normalized = unifiedSearchService.normalizeLocalResults(localResults);
180
-
181
- expect(normalized[0]).toEqual({
182
- id: 'guide.md',
183
- title: 'User Guide',
184
- description: 'A comprehensive guide',
185
- type: 'local',
186
- source: 'project',
187
- path: 'guide.md',
188
- url: 'guide.md',
189
- relevanceScore: 75,
190
- metadata: localResults[0].metadata,
191
- content: 'Full content here',
192
- snippet: 'This is a snippet...',
193
- matchedTerms: ['guide', 'user']
194
- });
195
- });
196
-
197
- it('should handle missing metadata gracefully', () => {
198
- const localResults = [
199
- {
200
- fileName: 'readme.md',
201
- content: 'Content',
202
- relevanceScore: 50
203
- }
204
- ];
205
-
206
- const normalized = unifiedSearchService.normalizeLocalResults(localResults);
207
-
208
- expect(normalized[0].title).toBe('readme.md');
209
- expect(normalized[0].description).toBe('');
210
- expect(normalized[0].matchedTerms).toEqual([]);
211
- });
212
- });
213
-
214
- describe('normalizeDocsetResults', () => {
215
- it('should normalize and deduplicate docset results', () => {
216
- const docsetResults = [
217
- {
218
- name: 'URLSession',
219
- type: 'Class',
220
- path: 'path1.html',
221
- url: 'https://example.com/path1.html',
222
- docsetId: 'apple',
223
- docsetName: 'Apple',
224
- relevanceScore: 60
225
- },
226
- {
227
- name: 'URLSession',
228
- type: 'Class',
229
- path: 'path2.html?language=swift',
230
- url: 'https://example.com/path2.html?language=swift',
231
- docsetId: 'apple',
232
- docsetName: 'Apple',
233
- relevanceScore: 50
234
- }
235
- ];
236
-
237
- const normalized = unifiedSearchService.normalizeDocsetResults(docsetResults);
238
-
239
- // Should deduplicate and prefer Swift entry
240
- expect(normalized).toHaveLength(1);
241
- expect(normalized[0].url).toContain('language=swift');
242
- });
243
-
244
- it('should keep higher score when neither is Swift', () => {
245
- const docsetResults = [
246
- {
247
- name: 'TestClass',
248
- type: 'Class',
249
- path: 'path1.html',
250
- url: 'https://example.com/path1.html',
251
- docsetId: 'mock',
252
- docsetName: 'Mock',
253
- relevanceScore: 70
254
- },
255
- {
256
- name: 'TestClass',
257
- type: 'Class',
258
- path: 'path2.html',
259
- url: 'https://example.com/path2.html',
260
- docsetId: 'mock',
261
- docsetName: 'Mock',
262
- relevanceScore: 80
263
- }
264
- ];
265
-
266
- const normalized = unifiedSearchService.normalizeDocsetResults(docsetResults);
267
-
268
- expect(normalized).toHaveLength(1);
269
- expect(normalized[0].relevanceScore).toBe(80);
270
- });
271
- });
272
-
273
- describe('getSources', () => {
274
- it('should return summary of available documentation sources', async () => {
275
- mockDocumentationService.documents.set('doc1.md', {});
276
- mockDocumentationService.documents.set('doc2.md', {});
277
-
278
- mockMultiDocsetDatabase.databases.set('apple', {});
279
- mockMultiDocsetDatabase.databases.set('mock', {});
280
- mockMultiDocsetDatabase.getStats.mockReturnValue([
281
- { docsetId: 'apple', docsetName: 'Apple', entryCount: 1000 },
282
- { docsetId: 'mock', docsetName: 'Mock', entryCount: 50 }
283
- ]);
284
-
285
- const sources = await unifiedSearchService.getSources();
286
-
287
- expect(sources).toEqual({
288
- local: {
289
- documentCount: 2,
290
- indexed: true
291
- },
292
- docsets: {
293
- count: 2,
294
- details: [
295
- { docsetId: 'apple', docsetName: 'Apple', entryCount: 1000 },
296
- { docsetId: 'mock', docsetName: 'Mock', entryCount: 50 }
297
- ]
298
- }
299
- });
300
- });
301
- });
302
- });
@@ -1,324 +0,0 @@
1
- import { DocsetDatabase, MultiDocsetDatabase } from '../database.js';
2
- import Database from 'better-sqlite3';
3
- import path from 'path';
4
- import fs from 'fs-extra';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
-
11
- describe('Enhanced DocsetDatabase', () => {
12
- let tempDir;
13
- let docsetInfo;
14
- let docsetDb;
15
- let sqliteDb;
16
-
17
- beforeEach(async () => {
18
- // Create temporary docset structure
19
- tempDir = path.join(__dirname, 'temp-docset-' + Date.now());
20
- const resourcesPath = path.join(tempDir, 'Contents', 'Resources');
21
- await fs.ensureDir(resourcesPath);
22
-
23
- // Create SQLite database
24
- const dbPath = path.join(resourcesPath, 'docSet.dsidx');
25
- sqliteDb = new Database(dbPath);
26
- sqliteDb.exec(`
27
- CREATE TABLE searchIndex(
28
- id INTEGER PRIMARY KEY,
29
- name TEXT,
30
- type TEXT,
31
- path TEXT
32
- );
33
- `);
34
-
35
- // Insert test data
36
- const stmt = sqliteDb.prepare('INSERT INTO searchIndex (name, type, path) VALUES (?, ?, ?)');
37
- stmt.run('AlarmKit', 'Framework', 'alarmkit.html');
38
- stmt.run('AlarmKit.Alarm', 'Class', 'alarmkit/alarm.html');
39
- stmt.run('AlarmKit.AlarmManager', 'Class', 'alarmkit/manager.html');
40
- stmt.run('URLSession', 'Class', 'urlsession.html');
41
- stmt.run('URLSession.shared', 'Property', 'urlsession/shared.html');
42
- stmt.run('URLSessionConfiguration', 'Class', 'urlsessionconfig.html');
43
- stmt.run('TestFramework', 'Framework', 'test.html');
44
- sqliteDb.close();
45
-
46
- docsetInfo = {
47
- id: 'test-docset',
48
- name: 'Test Docset',
49
- path: tempDir
50
- };
51
-
52
- docsetDb = new DocsetDatabase(docsetInfo);
53
- });
54
-
55
- afterEach(async () => {
56
- docsetDb.close();
57
- await fs.remove(tempDir);
58
- });
59
-
60
- describe('searchWithTerms', () => {
61
- it('should find exact phrase matches with high score', () => {
62
- const results = docsetDb.searchWithTerms(['alarmkit', 'alarm'], null, 10);
63
-
64
- const exactMatch = results.find(r => r.name === 'AlarmKit.Alarm');
65
- expect(exactMatch).toBeDefined();
66
- expect(exactMatch.isExactPhrase).toBe(true);
67
- expect(exactMatch.relevanceScore).toBeGreaterThan(90);
68
- });
69
-
70
- it('should find entries containing all search terms', () => {
71
- const results = docsetDb.searchWithTerms(['urlsession', 'configuration'], null, 10);
72
-
73
- // Should find URLSessionConfiguration since it contains both terms
74
- const configResult = results.find(r => r.name === 'URLSessionConfiguration');
75
- expect(configResult).toBeDefined();
76
- expect(configResult.matchedTerms).toBe(2);
77
- });
78
-
79
- it('should prioritize entries with all terms over partial matches', () => {
80
- const results = docsetDb.searchWithTerms(['urlsession', 'shared'], null, 10);
81
-
82
- // URLSession.shared should rank first as it contains both terms
83
- expect(results[0].name).toBe('URLSession.shared');
84
- expect(results[0].matchedTerms).toBe(2);
85
- });
86
-
87
- it('should handle single term searches', () => {
88
- const results = docsetDb.searchWithTerms(['alarmkit'], null, 10);
89
-
90
- expect(results.length).toBeGreaterThan(0);
91
- const frameworkResult = results.find(r => r.name === 'AlarmKit' && r.type === 'Framework');
92
- expect(frameworkResult).toBeDefined();
93
- });
94
-
95
- it('should respect type filter', () => {
96
- const results = docsetDb.searchWithTerms(['alarmkit'], 'Class', 10);
97
-
98
- expect(results.every(r => r.type === 'Class')).toBe(true);
99
- expect(results.some(r => r.name.includes('AlarmKit'))).toBe(true);
100
- });
101
-
102
- it('should prefer shorter names for equal relevance', () => {
103
- const results = docsetDb.searchWithTerms(['test'], null, 10);
104
-
105
- if (results.length > 1 && results[0].relevanceScore === results[1].relevanceScore) {
106
- expect(results[0].name.length).toBeLessThanOrEqual(results[1].name.length);
107
- }
108
- });
109
-
110
- it('should score exact name matches highest', () => {
111
- const results = docsetDb.searchWithTerms(['urlsession'], null, 10);
112
-
113
- const exactMatch = results.find(r => r.name.toLowerCase() === 'urlsession');
114
- if (exactMatch) {
115
- expect(results.indexOf(exactMatch)).toBeLessThan(3); // Should be in top 3
116
- }
117
- });
118
-
119
- it('should handle no matches gracefully', () => {
120
- const results = docsetDb.searchWithTerms(['nonexistent', 'terms'], null, 10);
121
-
122
- expect(results).toEqual([]);
123
- });
124
- });
125
- });
126
-
127
- describe('MultiDocsetDatabase enhanced features', () => {
128
- let multiDb;
129
- let tempDirs;
130
-
131
- beforeEach(async () => {
132
- multiDb = new MultiDocsetDatabase();
133
- tempDirs = [];
134
-
135
- // Create multiple mock docsets
136
- for (let i = 0; i < 2; i++) {
137
- const tempDir = path.join(__dirname, `temp-docset-${i}-${Date.now()}`);
138
- tempDirs.push(tempDir);
139
-
140
- const resourcesPath = path.join(tempDir, 'Contents', 'Resources');
141
- await fs.ensureDir(resourcesPath);
142
-
143
- const dbPath = path.join(resourcesPath, 'docSet.dsidx');
144
- const db = new Database(dbPath);
145
- db.exec(`
146
- CREATE TABLE searchIndex(
147
- id INTEGER PRIMARY KEY,
148
- name TEXT,
149
- type TEXT,
150
- path TEXT
151
- );
152
- `);
153
-
154
- // Insert different data in each docset
155
- const stmt = db.prepare('INSERT INTO searchIndex (name, type, path) VALUES (?, ?, ?)');
156
- if (i === 0) {
157
- stmt.run('AlarmKit', 'Framework', 'alarmkit.html?language=swift');
158
- stmt.run('AlarmKit', 'Framework', 'alarmkit.html'); // Duplicate without Swift
159
- stmt.run('AlarmKit.Alarm', 'Class', 'alarm.html');
160
- } else {
161
- stmt.run('NotificationKit', 'Framework', 'notifkit.html');
162
- stmt.run('AlarmKit.Schedule', 'Class', 'schedule.html');
163
- }
164
- db.close();
165
-
166
- multiDb.addDocset({
167
- id: `docset-${i}`,
168
- name: `Docset ${i}`,
169
- path: tempDir
170
- });
171
- }
172
- });
173
-
174
- afterEach(async () => {
175
- multiDb.closeAll();
176
- for (const dir of tempDirs) {
177
- await fs.remove(dir);
178
- }
179
- });
180
-
181
- describe('searchWithTerms across multiple docsets', () => {
182
- it('should search all docsets and combine results', () => {
183
- const results = multiDb.searchWithTerms(['alarmkit'], { limit: 10 });
184
-
185
- expect(results.length).toBeGreaterThan(0);
186
- // Should find entries from both docsets
187
- const docsetIds = new Set(results.map(r => r.docsetId));
188
- expect(docsetIds.size).toBe(2);
189
- });
190
-
191
- it('should deduplicate results preferring Swift entries', () => {
192
- const results = multiDb.searchWithTerms(['alarmkit'], { limit: 10 });
193
-
194
- // Count AlarmKit Framework entries
195
- const alarmKitFrameworks = results.filter(r =>
196
- r.name === 'AlarmKit' && r.type === 'Framework'
197
- );
198
-
199
- // Should only have one after deduplication
200
- expect(alarmKitFrameworks.length).toBe(1);
201
- // And it should be the Swift version
202
- expect(alarmKitFrameworks[0].url).toContain('language=swift');
203
- });
204
-
205
- it('should respect docsetId filter', () => {
206
- const results = multiDb.searchWithTerms(['alarmkit'], {
207
- docsetId: 'docset-0',
208
- limit: 10
209
- });
210
-
211
- expect(results.every(r => r.docsetId === 'docset-0')).toBe(true);
212
- });
213
-
214
- it('should sort by relevance score', () => {
215
- const results = multiDb.searchWithTerms(['alarmkit'], { limit: 10 });
216
-
217
- for (let i = 1; i < results.length; i++) {
218
- expect(results[i].relevanceScore).toBeLessThanOrEqual(results[i-1].relevanceScore);
219
- }
220
- });
221
- });
222
-
223
- describe('exploreAPI', () => {
224
- it('should find framework and related entries', () => {
225
- const exploration = multiDb.exploreAPI('AlarmKit');
226
-
227
- expect(exploration.framework).toBeDefined();
228
- expect(exploration.framework.name).toBe('AlarmKit');
229
- expect(exploration.classes.length).toBeGreaterThan(0);
230
- expect(exploration.classes.some(c => c.name === 'AlarmKit.Alarm')).toBe(true);
231
- });
232
-
233
- it('should categorize entries by type', () => {
234
- const exploration = multiDb.exploreAPI('AlarmKit');
235
-
236
- // Check that entries are properly categorized
237
- expect(exploration.classes.every(e => e.type === 'Class')).toBe(true);
238
- expect(exploration.framework?.type).toBe('Framework');
239
- });
240
-
241
- it('should search across all docsets', () => {
242
- const exploration = multiDb.exploreAPI('AlarmKit');
243
-
244
- // Should find AlarmKit.Schedule from second docset
245
- expect(exploration.classes.some(c => c.name === 'AlarmKit.Schedule')).toBe(true);
246
- });
247
-
248
- it('should respect docsetId option', () => {
249
- const exploration = multiDb.exploreAPI('AlarmKit', { docsetId: 'docset-0' });
250
-
251
- // Should not find AlarmKit.Schedule which is only in docset-1
252
- expect(exploration.classes.some(c => c.name === 'AlarmKit.Schedule')).toBe(false);
253
- });
254
-
255
- it('should handle entries without exact framework match', () => {
256
- const exploration = multiDb.exploreAPI('URLSession');
257
-
258
- // Even without a Framework entry, should find related classes
259
- expect(exploration.framework).toBeNull();
260
- expect(exploration.classes.length).toBe(0); // No URLSession in our test data
261
- });
262
-
263
- it('should include only specified types', () => {
264
- const exploration = multiDb.exploreAPI('AlarmKit', {
265
- includeTypes: ['Framework', 'Class']
266
- });
267
-
268
- // Should only have frameworks and classes
269
- const allEntries = [
270
- exploration.framework,
271
- ...exploration.classes,
272
- ...exploration.methods,
273
- ...exploration.properties
274
- ].filter(Boolean);
275
-
276
- expect(allEntries.every(e => ['Framework', 'Class'].includes(e.type))).toBe(true);
277
- });
278
- });
279
-
280
- describe('ParallelSearchManager integration', () => {
281
- beforeEach(async () => {
282
- // Add more docsets to trigger parallel search
283
- for (let i = 2; i < 5; i++) {
284
- const tempDir = path.join(__dirname, `temp-docset-${i}-${Date.now()}`);
285
- tempDirs.push(tempDir);
286
-
287
- const resourcesPath = path.join(tempDir, 'Contents', 'Resources');
288
- await fs.ensureDir(resourcesPath);
289
-
290
- const dbPath = path.join(resourcesPath, 'docSet.dsidx');
291
- const db = new Database(dbPath);
292
- db.exec(`
293
- CREATE TABLE searchIndex(
294
- id INTEGER PRIMARY KEY,
295
- name TEXT,
296
- type TEXT,
297
- path TEXT
298
- );
299
- `);
300
-
301
- db.prepare('INSERT INTO searchIndex (name, type, path) VALUES (?, ?, ?)')
302
- .run(`TestClass${i}`, 'Class', `test${i}.html`);
303
- db.close();
304
-
305
- multiDb.addDocset({
306
- id: `docset-${i}`,
307
- name: `Docset ${i}`,
308
- path: tempDir
309
- });
310
- }
311
- });
312
-
313
- it('should use parallel search for multiple docsets', async () => {
314
- // With 5 docsets, should trigger parallel search (threshold is >3)
315
- expect(multiDb.databases.size).toBe(5);
316
-
317
- const results = await multiDb.searchWithTerms(['test'], { limit: 20 });
318
-
319
- // Should find results from multiple docsets
320
- const docsetIds = new Set(results.map(r => r.docsetId));
321
- expect(docsetIds.size).toBeGreaterThanOrEqual(3);
322
- });
323
- });
324
- });