@afterxleep/doc-bot 1.17.0 → 1.19.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 +269 -63
- package/src/services/DocumentationService.js +26 -1
- package/src/services/PaginationService.js +378 -0
- package/src/services/__tests__/PaginationService.integration.test.js +185 -0
- package/src/services/__tests__/PaginationService.test.js +398 -0
- package/src/utils/TokenEstimator.js +134 -0
- package/prompts/file-docs.md +0 -69
- package/prompts/global-rules.md +0 -142
- package/prompts/mandatory-rules.md +0 -90
- package/prompts/search-results.md +0 -59
- package/prompts/system-prompt.md +0 -270
- 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,318 +0,0 @@
|
|
|
1
|
-
import { DocumentationService } from '../DocumentationService.js';
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname } from 'path';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
describe('DocumentationService', () => {
|
|
11
|
-
let docService;
|
|
12
|
-
let tempDocsPath;
|
|
13
|
-
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
tempDocsPath = path.join(__dirname, 'temp-docs-' + Date.now());
|
|
16
|
-
await fs.ensureDir(tempDocsPath);
|
|
17
|
-
docService = new DocumentationService(tempDocsPath);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
await fs.remove(tempDocsPath);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('extractRelevantSnippet', () => {
|
|
25
|
-
it('should extract snippet around exact phrase match', () => {
|
|
26
|
-
const content = `This is some content before the match.
|
|
27
|
-
Here we discuss how to use AlarmKit Framework effectively.
|
|
28
|
-
And this is content after the match.`;
|
|
29
|
-
|
|
30
|
-
const snippet = docService.extractRelevantSnippet(
|
|
31
|
-
content,
|
|
32
|
-
['use', 'alarmkit', 'framework'],
|
|
33
|
-
'use AlarmKit Framework'
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
expect(snippet).toContain('use AlarmKit Framework');
|
|
37
|
-
expect(snippet.length).toBeLessThanOrEqual(250);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should find best snippet with most matching terms', () => {
|
|
41
|
-
const content = `# Introduction
|
|
42
|
-
This document covers various topics.
|
|
43
|
-
|
|
44
|
-
# URLSession Configuration
|
|
45
|
-
Learn how to configure URLSession properly.
|
|
46
|
-
|
|
47
|
-
# Advanced Usage
|
|
48
|
-
Here we discuss URLSession and configuration together.
|
|
49
|
-
URLSession provides many configuration options.`;
|
|
50
|
-
|
|
51
|
-
const snippet = docService.extractRelevantSnippet(
|
|
52
|
-
content,
|
|
53
|
-
['urlsession', 'configuration'],
|
|
54
|
-
'URLSession configuration'
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
expect(snippet).toContain('URLSession');
|
|
58
|
-
expect(snippet).toContain('configuration');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should prioritize headers containing search terms', () => {
|
|
62
|
-
const content = `# Random Header
|
|
63
|
-
Some content here.
|
|
64
|
-
|
|
65
|
-
# AlarmKit Integration
|
|
66
|
-
This is the section about integration.
|
|
67
|
-
|
|
68
|
-
Some other content mentioning AlarmKit.`;
|
|
69
|
-
|
|
70
|
-
const snippet = docService.extractRelevantSnippet(
|
|
71
|
-
content,
|
|
72
|
-
['alarmkit', 'integration'],
|
|
73
|
-
'AlarmKit integration'
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
expect(snippet).toContain('# AlarmKit Integration');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should return description from metadata if no good snippet found', () => {
|
|
80
|
-
const content = `---
|
|
81
|
-
title: Test Document
|
|
82
|
-
description: This is a comprehensive guide to using the API
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
Some unrelated content here.`;
|
|
86
|
-
|
|
87
|
-
const snippet = docService.extractRelevantSnippet(
|
|
88
|
-
content,
|
|
89
|
-
['nonexistent', 'terms'],
|
|
90
|
-
'nonexistent terms'
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(snippet).toBe('This is a comprehensive guide to using the API');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should clean and format snippets properly', () => {
|
|
97
|
-
const content = `This is **bold** text with \`code\` and multiple spaces.`;
|
|
98
|
-
|
|
99
|
-
const snippet = docService.extractRelevantSnippet(
|
|
100
|
-
content,
|
|
101
|
-
['bold', 'text'],
|
|
102
|
-
'bold text'
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
expect(snippet).not.toContain('**');
|
|
106
|
-
expect(snippet).not.toContain('`');
|
|
107
|
-
expect(snippet).not.toMatch(/\s{2,}/);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('getMatchedTerms', () => {
|
|
112
|
-
it('should return terms that match in document', () => {
|
|
113
|
-
const doc = {
|
|
114
|
-
content: 'Learn about URLSession and networking in Swift',
|
|
115
|
-
metadata: {
|
|
116
|
-
title: 'Swift Networking Guide',
|
|
117
|
-
description: 'A guide to URLSession'
|
|
118
|
-
},
|
|
119
|
-
fileName: 'networking.md'
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const matched = docService.getMatchedTerms(doc, ['urlsession', 'swift', 'api']);
|
|
123
|
-
|
|
124
|
-
expect(matched).toContain('urlsession');
|
|
125
|
-
expect(matched).toContain('swift');
|
|
126
|
-
expect(matched).not.toContain('api');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should match terms in title and description', () => {
|
|
130
|
-
const doc = {
|
|
131
|
-
content: 'Some content',
|
|
132
|
-
metadata: {
|
|
133
|
-
title: 'AlarmKit Framework',
|
|
134
|
-
description: 'Learn to use AlarmKit'
|
|
135
|
-
},
|
|
136
|
-
fileName: 'guide.md'
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const matched = docService.getMatchedTerms(doc, ['alarmkit', 'framework', 'use']);
|
|
140
|
-
|
|
141
|
-
expect(matched).toEqual(['alarmkit', 'framework', 'use']);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('searchDocuments with enhanced features', () => {
|
|
146
|
-
beforeEach(async () => {
|
|
147
|
-
// Create test documents
|
|
148
|
-
await fs.writeFile(
|
|
149
|
-
path.join(tempDocsPath, 'high-relevance.md'),
|
|
150
|
-
`---
|
|
151
|
-
title: AlarmKit Framework Guide
|
|
152
|
-
description: Complete guide to using AlarmKit Framework
|
|
153
|
-
keywords: [alarmkit, framework, ios]
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
# AlarmKit Framework
|
|
157
|
-
|
|
158
|
-
Learn how to use AlarmKit Framework effectively.
|
|
159
|
-
AlarmKit provides powerful alarm functionality.`
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
await fs.writeFile(
|
|
163
|
-
path.join(tempDocsPath, 'medium-relevance.md'),
|
|
164
|
-
`---
|
|
165
|
-
title: iOS Development
|
|
166
|
-
description: General iOS development guide
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
# iOS Development
|
|
170
|
-
|
|
171
|
-
This guide covers various frameworks including AlarmKit.`
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
await fs.writeFile(
|
|
175
|
-
path.join(tempDocsPath, 'low-relevance.md'),
|
|
176
|
-
`---
|
|
177
|
-
title: Random Document
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
# Random Content
|
|
181
|
-
|
|
182
|
-
This has nothing to do with alarms or kits.`
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
await docService.initialize();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should return results with snippets and matched terms', async () => {
|
|
189
|
-
const results = await docService.searchDocuments('AlarmKit Framework');
|
|
190
|
-
|
|
191
|
-
expect(results.length).toBeGreaterThan(0);
|
|
192
|
-
expect(results[0].snippet).toBeDefined();
|
|
193
|
-
expect(results[0].matchedTerms).toBeDefined();
|
|
194
|
-
expect(results[0].matchedTerms).toContain('alarmkit');
|
|
195
|
-
expect(results[0].matchedTerms).toContain('framework');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should filter out low relevance results', async () => {
|
|
199
|
-
const results = await docService.searchDocuments('AlarmKit Framework');
|
|
200
|
-
|
|
201
|
-
// Should not include the "Random Document" with no relevant content
|
|
202
|
-
const hasLowRelevance = results.some(r => r.metadata?.title === 'Random Document');
|
|
203
|
-
expect(hasLowRelevance).toBe(false);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should prioritize exact phrase matches', async () => {
|
|
207
|
-
const results = await docService.searchDocuments('AlarmKit Framework');
|
|
208
|
-
|
|
209
|
-
expect(results[0].metadata?.title).toBe('AlarmKit Framework Guide');
|
|
210
|
-
expect(results[0].relevanceScore).toBeGreaterThan(50);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should boost keyword matches', async () => {
|
|
214
|
-
const results = await docService.searchDocuments('alarmkit');
|
|
215
|
-
|
|
216
|
-
// Document with alarmkit in keywords should score higher
|
|
217
|
-
expect(results[0].metadata?.keywords).toContain('alarmkit');
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('calculateAdvancedRelevanceScore', () => {
|
|
222
|
-
it('should give high score for exact phrase match', () => {
|
|
223
|
-
const doc = {
|
|
224
|
-
content: 'Learn how to use AlarmKit Framework in your iOS app',
|
|
225
|
-
metadata: { title: 'iOS Guide' },
|
|
226
|
-
fileName: 'guide.md'
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const score = docService.calculateAdvancedRelevanceScore(
|
|
230
|
-
doc,
|
|
231
|
-
['alarmkit', 'framework'],
|
|
232
|
-
'AlarmKit Framework'
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
expect(score).toBeGreaterThan(20); // Exact phrase bonus
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should boost matches in title', () => {
|
|
239
|
-
const doc1 = {
|
|
240
|
-
content: 'Some content about URLSession',
|
|
241
|
-
metadata: { title: 'URLSession Guide' },
|
|
242
|
-
fileName: 'guide1.md'
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const doc2 = {
|
|
246
|
-
content: 'URLSession is mentioned here',
|
|
247
|
-
metadata: { title: 'Random Guide' },
|
|
248
|
-
fileName: 'guide2.md'
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const score1 = docService.calculateAdvancedRelevanceScore(
|
|
252
|
-
doc1,
|
|
253
|
-
['urlsession'],
|
|
254
|
-
'URLSession'
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const score2 = docService.calculateAdvancedRelevanceScore(
|
|
258
|
-
doc2,
|
|
259
|
-
['urlsession'],
|
|
260
|
-
'URLSession'
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
expect(score1).toBeGreaterThan(score2);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should apply term coverage bonus', () => {
|
|
267
|
-
const doc = {
|
|
268
|
-
content: 'URLSession configuration and usage',
|
|
269
|
-
metadata: { title: 'Networking' },
|
|
270
|
-
fileName: 'net.md'
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const score1 = docService.calculateAdvancedRelevanceScore(
|
|
274
|
-
doc,
|
|
275
|
-
['urlsession'],
|
|
276
|
-
'URLSession'
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const score2 = docService.calculateAdvancedRelevanceScore(
|
|
280
|
-
doc,
|
|
281
|
-
['urlsession', 'configuration'],
|
|
282
|
-
'URLSession configuration'
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// Matching both terms should score higher
|
|
286
|
-
expect(score2).toBeGreaterThan(score1);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should cap content match frequency to prevent spam', () => {
|
|
290
|
-
const spamDoc = {
|
|
291
|
-
content: 'test '.repeat(100),
|
|
292
|
-
metadata: { title: 'Spam' },
|
|
293
|
-
fileName: 'spam.md'
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const normalDoc = {
|
|
297
|
-
content: 'This is a test document with normal content',
|
|
298
|
-
metadata: { title: 'Normal' },
|
|
299
|
-
fileName: 'normal.md'
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const spamScore = docService.calculateAdvancedRelevanceScore(
|
|
303
|
-
spamDoc,
|
|
304
|
-
['test'],
|
|
305
|
-
'test'
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const normalScore = docService.calculateAdvancedRelevanceScore(
|
|
309
|
-
normalDoc,
|
|
310
|
-
['test'],
|
|
311
|
-
'test'
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
// Spam score should be capped, not drastically higher
|
|
315
|
-
expect(spamScore / normalScore).toBeLessThan(5);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
});
|
|
@@ -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
|
-
});
|