@afterxleep/doc-bot 1.16.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.
- package/package.json +7 -4
- package/src/__tests__/temp-docs-1756129972061/test.md +5 -0
- package/src/__tests__/temp-docs-1756129972071/test.md +5 -0
- package/src/__tests__/temp-docs-1756129972075/test.md +5 -0
- package/src/__tests__/temp-docs-1756129972077/test.md +5 -0
- package/src/__tests__/temp-docs-1756129972079/test.md +5 -0
- package/src/__tests__/temp-docs-1756130189361/test.md +5 -0
- package/src/__tests__/temp-docs-1756130189372/test.md +5 -0
- package/src/__tests__/temp-docs-1756130189375/test.md +5 -0
- package/src/__tests__/temp-docs-1756130189378/test.md +5 -0
- package/src/__tests__/temp-docs-1756130189379/test.md +5 -0
- package/src/__tests__/temp-docs-1756130271128/test.md +5 -0
- package/src/__tests__/temp-docs-1756130271139/test.md +5 -0
- package/src/__tests__/temp-docs-1756130271142/test.md +5 -0
- package/src/__tests__/temp-docs-1756130271145/test.md +5 -0
- package/src/__tests__/temp-docs-1756130271146/test.md +5 -0
- package/src/__tests__/temp-docs-1756130687030/test.md +5 -0
- package/src/__tests__/temp-docs-1756130687044/test.md +5 -0
- package/src/__tests__/temp-docs-1756130687048/test.md +5 -0
- package/src/__tests__/temp-docs-1756130687051/test.md +5 -0
- package/src/__tests__/temp-docs-1756130687053/test.md +5 -0
- package/src/__tests__/temp-docs-1756131694925/test.md +5 -0
- package/src/__tests__/temp-docs-1756131694937/test.md +5 -0
- package/src/__tests__/temp-docs-1756131694941/test.md +5 -0
- package/src/__tests__/temp-docs-1756131694944/test.md +5 -0
- package/src/__tests__/temp-docs-1756131694946/test.md +5 -0
- package/src/__tests__/temp-docs-1756133998710/test.md +5 -0
- package/src/__tests__/temp-docs-1756133998721/test.md +5 -0
- package/src/__tests__/temp-docs-1756133998724/test.md +5 -0
- package/src/__tests__/temp-docs-1756133998727/test.md +5 -0
- package/src/__tests__/temp-docs-1756133998729/test.md +5 -0
- package/src/__tests__/temp-docs-1756134345935/test.md +5 -0
- package/src/__tests__/temp-docs-1756134345948/test.md +5 -0
- package/src/__tests__/temp-docs-1756134345952/test.md +5 -0
- package/src/__tests__/temp-docs-1756134345954/test.md +5 -0
- package/src/__tests__/temp-docs-1756134345957/test.md +5 -0
- package/src/__tests__/temp-docsets-1756129972079/2e443167/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756129972079/2e443167/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756129972079/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756129972079/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756129972079/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756130189379/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130189379/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130189379/a4934c14/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130189379/a4934c14/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130189379/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756130271146/3f8acbb2/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130271146/3f8acbb2/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130271146/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130271146/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130271146/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756130687053/6810e6bd/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130687053/6810e6bd/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130687053/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756130687053/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756130687053/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756131694946/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756131694946/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756131694946/dd703046/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756131694946/dd703046/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756131694946/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756133998729/9e061136/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756133998729/9e061136/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756133998729/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756133998729/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756133998729/docsets.json +10 -0
- package/src/__tests__/temp-docsets-1756134345957/03e730af/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756134345957/03e730af/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756134345957/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1756134345957/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1756134345957/docsets.json +10 -0
- package/src/index.js +18 -11
- package/src/services/DocumentationService.js +26 -1
- package/prompts/file-docs.txt +0 -52
- package/prompts/global-rules.txt +0 -83
- package/prompts/mandatory-rules.txt +0 -117
- package/prompts/search-results.txt +0 -48
- package/prompts/system-prompt.txt +0 -210
- package/src/__tests__/docset-integration.test.js +0 -146
- package/src/services/__tests__/DocumentationService.test.js +0 -318
- package/src/services/__tests__/UnifiedSearchService.test.js +0 -302
- 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
|
-
});
|