@afterxleep/doc-bot 1.9.1 → 1.13.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 (25) hide show
  1. package/README.md +226 -294
  2. package/bin/doc-bot.js +2 -3
  3. package/package.json +8 -7
  4. package/src/__tests__/docset-integration.test.js +146 -0
  5. package/src/__tests__/temp-docs-1752689978225/test.md +5 -0
  6. package/src/__tests__/temp-docs-1752689978235/test.md +5 -0
  7. package/src/__tests__/temp-docs-1752689978241/test.md +5 -0
  8. package/src/__tests__/temp-docs-1752689978243/test.md +5 -0
  9. package/src/__tests__/temp-docs-1752689978244/test.md +5 -0
  10. package/src/__tests__/temp-docsets-1752689978244/7e2cbc65/Mock.docset/Contents/Info.plist +10 -0
  11. package/src/__tests__/temp-docsets-1752689978244/7e2cbc65/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  12. package/src/__tests__/temp-docsets-1752689978244/Mock.docset/Contents/Info.plist +10 -0
  13. package/src/__tests__/temp-docsets-1752689978244/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
  14. package/src/__tests__/temp-docsets-1752689978244/docsets.json +10 -0
  15. package/src/index.js +474 -28
  16. package/src/services/DocumentationService.js +131 -2
  17. package/src/services/UnifiedSearchService.js +214 -0
  18. package/src/services/__tests__/DocumentationService.test.js +318 -0
  19. package/src/services/__tests__/UnifiedSearchService.test.js +302 -0
  20. package/src/services/docset/ParallelSearchManager.js +158 -0
  21. package/src/services/docset/__tests__/DocsetDatabase.test.js +337 -0
  22. package/src/services/docset/__tests__/DocsetService.test.js +195 -0
  23. package/src/services/docset/__tests__/EnhancedDocsetDatabase.test.js +324 -0
  24. package/src/services/docset/database.js +474 -0
  25. package/src/services/docset/index.js +349 -0
@@ -0,0 +1,337 @@
1
+ import { DocsetDatabase, MultiDocsetDatabase } from '../database.js';
2
+ import Database from 'better-sqlite3';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
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('DocsetDatabase', () => {
12
+ let tempDir;
13
+ let testDbPath;
14
+ let testDb;
15
+ let docsetInfo;
16
+
17
+ beforeEach(async () => {
18
+ // Create temporary directory and test database
19
+ tempDir = path.join(__dirname, 'temp-db-' + Date.now());
20
+ await fs.ensureDir(tempDir);
21
+
22
+ // Create docset structure
23
+ const docsetPath = path.join(tempDir, 'Test.docset');
24
+ const resourcesPath = path.join(docsetPath, 'Contents', 'Resources');
25
+ await fs.ensureDir(resourcesPath);
26
+
27
+ testDbPath = path.join(resourcesPath, 'docSet.dsidx');
28
+
29
+ // Create test SQLite database with docset schema
30
+ testDb = new Database(testDbPath);
31
+ testDb.exec(`
32
+ CREATE TABLE searchIndex(
33
+ id INTEGER PRIMARY KEY,
34
+ name TEXT,
35
+ type TEXT,
36
+ path TEXT
37
+ );
38
+ CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);
39
+ `);
40
+
41
+ // Insert test data
42
+ const insertStmt = testDb.prepare('INSERT INTO searchIndex (name, type, path) VALUES (?, ?, ?)');
43
+ insertStmt.run('UIViewController', 'Class', 'UIKit/UIViewController.html');
44
+ insertStmt.run('viewDidLoad', 'Method', 'UIKit/UIViewController.html#viewDidLoad');
45
+ insertStmt.run('NSString', 'Class', 'Foundation/NSString.html');
46
+ insertStmt.run('stringWithFormat:', 'Method', 'Foundation/NSString.html#stringWithFormat');
47
+ insertStmt.run('iOS App Lifecycle', 'Guide', 'Guides/AppLifecycle.html');
48
+ testDb.close();
49
+
50
+ // Create docset info
51
+ docsetInfo = {
52
+ id: 'test-docset',
53
+ name: 'Test Docset',
54
+ path: docsetPath
55
+ };
56
+ });
57
+
58
+ afterEach(async () => {
59
+ // Clean up
60
+ await fs.remove(tempDir);
61
+ });
62
+
63
+ describe('constructor', () => {
64
+ it('should open database in readonly mode', () => {
65
+ const docsetDb = new DocsetDatabase(docsetInfo);
66
+ expect(docsetDb.docsetInfo).toEqual(docsetInfo);
67
+ expect(docsetDb.db.readonly).toBe(true);
68
+ docsetDb.close();
69
+ });
70
+ });
71
+
72
+ describe('search', () => {
73
+ it('should search by query with case-insensitive matching', () => {
74
+ const docsetDb = new DocsetDatabase(docsetInfo);
75
+
76
+ const results = docsetDb.search('view');
77
+ expect(results).toHaveLength(2);
78
+ expect(results[0].name).toBe('viewDidLoad');
79
+ expect(results[1].name).toBe('UIViewController');
80
+
81
+ docsetDb.close();
82
+ });
83
+
84
+ it('should filter by type when provided', () => {
85
+ const docsetDb = new DocsetDatabase(docsetInfo);
86
+
87
+ const results = docsetDb.search('i', 'Class');
88
+ expect(results).toHaveLength(2); // UIViewController and NSString
89
+ expect(results.every(r => r.type === 'Class')).toBe(true);
90
+
91
+ docsetDb.close();
92
+ });
93
+
94
+ it('should respect limit parameter', () => {
95
+ const docsetDb = new DocsetDatabase(docsetInfo);
96
+
97
+ const results = docsetDb.search('i', null, 2);
98
+ expect(results).toHaveLength(2);
99
+
100
+ docsetDb.close();
101
+ });
102
+
103
+ it('should include docset metadata in results', () => {
104
+ const docsetDb = new DocsetDatabase(docsetInfo);
105
+
106
+ const results = docsetDb.search('UIViewController');
107
+ expect(results[0]).toMatchObject({
108
+ name: 'UIViewController',
109
+ type: 'Class',
110
+ url: 'UIKit/UIViewController.html',
111
+ docsetId: 'test-docset',
112
+ docsetName: 'Test Docset'
113
+ });
114
+
115
+ docsetDb.close();
116
+ });
117
+ });
118
+
119
+ describe('searchExact', () => {
120
+ it('should find exact name match', () => {
121
+ const docsetDb = new DocsetDatabase(docsetInfo);
122
+
123
+ const result = docsetDb.searchExact('UIViewController');
124
+ expect(result).toMatchObject({
125
+ name: 'UIViewController',
126
+ type: 'Class',
127
+ url: 'UIKit/UIViewController.html'
128
+ });
129
+
130
+ docsetDb.close();
131
+ });
132
+
133
+ it('should return null when no exact match found', () => {
134
+ const docsetDb = new DocsetDatabase(docsetInfo);
135
+
136
+ const result = docsetDb.searchExact('NonExistent');
137
+ expect(result).toBeNull();
138
+
139
+ docsetDb.close();
140
+ });
141
+
142
+ it('should filter by type when provided', () => {
143
+ const docsetDb = new DocsetDatabase(docsetInfo);
144
+
145
+ // Should not find UIViewController when searching for Method type
146
+ const result = docsetDb.searchExact('UIViewController', 'Method');
147
+ expect(result).toBeNull();
148
+
149
+ docsetDb.close();
150
+ });
151
+ });
152
+
153
+ describe('statistics methods', () => {
154
+ it('should get all unique types', () => {
155
+ const docsetDb = new DocsetDatabase(docsetInfo);
156
+
157
+ const types = docsetDb.getTypes();
158
+ expect(types).toEqual(['Class', 'Guide', 'Method']);
159
+
160
+ docsetDb.close();
161
+ });
162
+
163
+ it('should count entries by type', () => {
164
+ const docsetDb = new DocsetDatabase(docsetInfo);
165
+
166
+ expect(docsetDb.getTypeCount('Class')).toBe(2);
167
+ expect(docsetDb.getTypeCount('Method')).toBe(2);
168
+ expect(docsetDb.getTypeCount('Guide')).toBe(1);
169
+
170
+ docsetDb.close();
171
+ });
172
+
173
+ it('should get total entry count', () => {
174
+ const docsetDb = new DocsetDatabase(docsetInfo);
175
+
176
+ expect(docsetDb.getEntryCount()).toBe(5);
177
+
178
+ docsetDb.close();
179
+ });
180
+ });
181
+ });
182
+
183
+ describe('MultiDocsetDatabase', () => {
184
+ let multiDb;
185
+ let tempDir;
186
+ let docset1Info;
187
+ let docset2Info;
188
+
189
+ beforeEach(async () => {
190
+ multiDb = new MultiDocsetDatabase();
191
+ tempDir = path.join(__dirname, 'temp-multi-' + Date.now());
192
+ await fs.ensureDir(tempDir);
193
+
194
+ // Create two test docsets
195
+ const createDocset = async (name, entries) => {
196
+ const docsetPath = path.join(tempDir, `${name}.docset`);
197
+ const resourcesPath = path.join(docsetPath, 'Contents', 'Resources');
198
+ await fs.ensureDir(resourcesPath);
199
+
200
+ const dbPath = path.join(resourcesPath, 'docSet.dsidx');
201
+ const db = new Database(dbPath);
202
+ db.exec(`
203
+ CREATE TABLE searchIndex(
204
+ id INTEGER PRIMARY KEY,
205
+ name TEXT,
206
+ type TEXT,
207
+ path TEXT
208
+ );
209
+ `);
210
+
211
+ const insertStmt = db.prepare('INSERT INTO searchIndex (name, type, path) VALUES (?, ?, ?)');
212
+ entries.forEach(entry => insertStmt.run(entry.name, entry.type, entry.path));
213
+ db.close();
214
+
215
+ return {
216
+ id: name.toLowerCase(),
217
+ name: name,
218
+ path: docsetPath
219
+ };
220
+ };
221
+
222
+ docset1Info = await createDocset('iOS', [
223
+ { name: 'UIView', type: 'Class', path: 'UIKit/UIView.html' },
224
+ { name: 'UIViewController', type: 'Class', path: 'UIKit/UIViewController.html' }
225
+ ]);
226
+
227
+ docset2Info = await createDocset('Swift', [
228
+ { name: 'Array', type: 'Structure', path: 'Swift/Array.html' },
229
+ { name: 'String', type: 'Structure', path: 'Swift/String.html' }
230
+ ]);
231
+ });
232
+
233
+ afterEach(async () => {
234
+ multiDb.closeAll();
235
+ await fs.remove(tempDir);
236
+ });
237
+
238
+ describe('docset management', () => {
239
+ it('should add and remove docsets', () => {
240
+ multiDb.addDocset(docset1Info);
241
+ expect(multiDb.databases.size).toBe(1);
242
+
243
+ multiDb.addDocset(docset2Info);
244
+ expect(multiDb.databases.size).toBe(2);
245
+
246
+ multiDb.removeDocset('ios');
247
+ expect(multiDb.databases.size).toBe(1);
248
+ expect(multiDb.databases.has('swift')).toBe(true);
249
+ });
250
+
251
+ it('should replace existing docset when adding with same ID', () => {
252
+ multiDb.addDocset(docset1Info);
253
+ const firstDb = multiDb.databases.get('ios');
254
+
255
+ // Add again with same ID
256
+ multiDb.addDocset(docset1Info);
257
+ const secondDb = multiDb.databases.get('ios');
258
+
259
+ expect(firstDb).not.toBe(secondDb);
260
+ expect(multiDb.databases.size).toBe(1);
261
+ });
262
+ });
263
+
264
+ describe('search across docsets', () => {
265
+ beforeEach(() => {
266
+ multiDb.addDocset(docset1Info);
267
+ multiDb.addDocset(docset2Info);
268
+ });
269
+
270
+ it('should search across all docsets', () => {
271
+ const results = multiDb.search('i');
272
+ expect(results).toHaveLength(3); // UIView, UIViewController, String contain 'i'
273
+
274
+ // Check results come from both docsets
275
+ const docsetIds = [...new Set(results.map(r => r.docsetId))];
276
+ expect(docsetIds).toHaveLength(2);
277
+ });
278
+
279
+ it('should limit search to specific docset when ID provided', () => {
280
+ const results = multiDb.search('i', { docsetId: 'ios' });
281
+ expect(results).toHaveLength(2);
282
+ expect(results.every(r => r.docsetId === 'ios')).toBe(true);
283
+ });
284
+
285
+ it('should filter by type across all docsets', () => {
286
+ const results = multiDb.search('', { type: 'Structure' });
287
+ expect(results).toHaveLength(2);
288
+ expect(results.every(r => r.type === 'Structure')).toBe(true);
289
+ });
290
+
291
+ it('should respect global limit', () => {
292
+ const results = multiDb.search('', { limit: 3 });
293
+ expect(results).toHaveLength(3);
294
+ });
295
+ });
296
+
297
+ describe('exact search', () => {
298
+ beforeEach(() => {
299
+ multiDb.addDocset(docset1Info);
300
+ multiDb.addDocset(docset2Info);
301
+ });
302
+
303
+ it('should find exact match across all docsets', () => {
304
+ const result = multiDb.searchExact('Array');
305
+ expect(result).toMatchObject({
306
+ name: 'Array',
307
+ type: 'Structure',
308
+ docsetId: 'swift'
309
+ });
310
+ });
311
+
312
+ it('should limit to specific docset when ID provided', () => {
313
+ const result = multiDb.searchExact('Array', { docsetId: 'ios' });
314
+ expect(result).toBeNull();
315
+ });
316
+ });
317
+
318
+ describe('statistics', () => {
319
+ beforeEach(() => {
320
+ multiDb.addDocset(docset1Info);
321
+ multiDb.addDocset(docset2Info);
322
+ });
323
+
324
+ it('should get stats for all docsets', () => {
325
+ const stats = multiDb.getStats();
326
+ expect(stats).toHaveLength(2);
327
+
328
+ const iosStats = stats.find(s => s.docsetId === 'ios');
329
+ expect(iosStats.entryCount).toBe(2);
330
+ expect(iosStats.types).toEqual({ Class: 2 });
331
+
332
+ const swiftStats = stats.find(s => s.docsetId === 'swift');
333
+ expect(swiftStats.entryCount).toBe(2);
334
+ expect(swiftStats.types).toEqual({ Structure: 2 });
335
+ });
336
+ });
337
+ });
@@ -0,0 +1,195 @@
1
+ import { DocsetService } from '../index.js';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import os from 'os';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ describe('DocsetService', () => {
12
+ let docsetService;
13
+ let tempStoragePath;
14
+
15
+ beforeEach(async () => {
16
+ // Create a temporary directory for test docsets
17
+ tempStoragePath = path.join(__dirname, 'temp-docsets-' + Date.now());
18
+ await fs.ensureDir(tempStoragePath);
19
+
20
+ docsetService = new DocsetService(tempStoragePath);
21
+ });
22
+
23
+ afterEach(async () => {
24
+ // Clean up temporary directory
25
+ await fs.remove(tempStoragePath);
26
+ });
27
+
28
+ describe('constructor', () => {
29
+ it('should create instance with custom storage path', () => {
30
+ expect(docsetService.storagePath).toBe(tempStoragePath);
31
+ expect(docsetService.metadataPath).toBe(path.join(tempStoragePath, 'docsets.json'));
32
+ });
33
+
34
+ it('should create instance with default storage path when none provided', () => {
35
+ const defaultService = new DocsetService();
36
+ const expectedPath = path.join(os.homedir(), 'Developer', 'DocSets');
37
+ expect(defaultService.storagePath).toBe(expectedPath);
38
+ });
39
+ });
40
+
41
+ describe('initialize', () => {
42
+ it('should create storage directory if it does not exist', async () => {
43
+ // Remove the directory first
44
+ await fs.remove(tempStoragePath);
45
+ expect(await fs.pathExists(tempStoragePath)).toBe(false);
46
+
47
+ // Initialize should create it
48
+ await docsetService.initialize();
49
+ expect(await fs.pathExists(tempStoragePath)).toBe(true);
50
+ });
51
+
52
+ it('should load existing metadata on initialization', async () => {
53
+ // Create test metadata
54
+ const testMetadata = [
55
+ {
56
+ id: 'test-docset-1',
57
+ name: 'Test Docset',
58
+ downloadedAt: new Date().toISOString()
59
+ }
60
+ ];
61
+ await fs.writeJson(docsetService.metadataPath, testMetadata);
62
+
63
+ // Create the docset directory
64
+ await fs.ensureDir(path.join(tempStoragePath, 'test-docset-1'));
65
+
66
+ // Initialize and check if metadata is loaded
67
+ await docsetService.initialize();
68
+ expect(docsetService.docsets.size).toBe(1);
69
+ expect(docsetService.docsets.has('test-docset-1')).toBe(true);
70
+ });
71
+
72
+ it('should skip docsets that no longer exist on disk', async () => {
73
+ // Create metadata for non-existent docset
74
+ const testMetadata = [
75
+ {
76
+ id: 'missing-docset',
77
+ name: 'Missing Docset',
78
+ downloadedAt: new Date().toISOString()
79
+ }
80
+ ];
81
+ await fs.writeJson(docsetService.metadataPath, testMetadata);
82
+
83
+ // Initialize and check that missing docset is not loaded
84
+ await docsetService.initialize();
85
+ expect(docsetService.docsets.size).toBe(0);
86
+ });
87
+ });
88
+
89
+ describe('listDocsets', () => {
90
+ it('should return empty array when no docsets are installed', async () => {
91
+ await docsetService.initialize();
92
+ const docsets = await docsetService.listDocsets();
93
+ expect(docsets).toEqual([]);
94
+ });
95
+
96
+ it('should return list of installed docsets', async () => {
97
+ // Add test docsets to internal map
98
+ docsetService.docsets.set('test-1', {
99
+ id: 'test-1',
100
+ name: 'Test Docset 1',
101
+ downloadedAt: new Date()
102
+ });
103
+ docsetService.docsets.set('test-2', {
104
+ id: 'test-2',
105
+ name: 'Test Docset 2',
106
+ downloadedAt: new Date()
107
+ });
108
+
109
+ const docsets = await docsetService.listDocsets();
110
+ expect(docsets).toHaveLength(2);
111
+ expect(docsets[0].name).toBe('Test Docset 1');
112
+ expect(docsets[1].name).toBe('Test Docset 2');
113
+ });
114
+ });
115
+
116
+ describe('getDocset', () => {
117
+ it('should return docset by ID', () => {
118
+ const testDocset = {
119
+ id: 'test-docset',
120
+ name: 'Test Docset',
121
+ downloadedAt: new Date()
122
+ };
123
+ docsetService.docsets.set('test-docset', testDocset);
124
+
125
+ const result = docsetService.getDocset('test-docset');
126
+ expect(result).toEqual(testDocset);
127
+ });
128
+
129
+ it('should return undefined for non-existent docset', () => {
130
+ const result = docsetService.getDocset('non-existent');
131
+ expect(result).toBeUndefined();
132
+ });
133
+ });
134
+
135
+ describe('removeDocset', () => {
136
+ it('should throw error when docset does not exist', async () => {
137
+ await expect(docsetService.removeDocset('non-existent'))
138
+ .rejects.toThrow('Docset non-existent not found');
139
+ });
140
+
141
+ it('should remove docset directory and metadata', async () => {
142
+ // Create a test docset
143
+ const docsetId = 'test-remove';
144
+ const docsetPath = path.join(tempStoragePath, docsetId, 'Test.docset');
145
+ await fs.ensureDir(docsetPath);
146
+
147
+ docsetService.docsets.set(docsetId, {
148
+ id: docsetId,
149
+ name: 'Test Docset',
150
+ path: docsetPath,
151
+ downloadedAt: new Date()
152
+ });
153
+ await docsetService.saveMetadata();
154
+
155
+ // Remove the docset
156
+ await docsetService.removeDocset(docsetId);
157
+
158
+ // Check that directory is removed
159
+ expect(await fs.pathExists(path.join(tempStoragePath, docsetId))).toBe(false);
160
+
161
+ // Check that metadata is updated
162
+ expect(docsetService.docsets.has(docsetId)).toBe(false);
163
+
164
+ // Check that metadata file is updated
165
+ const metadata = await fs.readJson(docsetService.metadataPath);
166
+ expect(metadata).toHaveLength(0);
167
+ });
168
+ });
169
+
170
+ describe('download progress tracking', () => {
171
+ it('should track download progress for a docset', () => {
172
+ const docsetId = 'test-download';
173
+ const progress = {
174
+ docsetId,
175
+ url: 'https://example.com/test.docset',
176
+ percentage: 50
177
+ };
178
+
179
+ docsetService.downloadProgress.set(docsetId, progress);
180
+
181
+ const result = docsetService.getDownloadProgress(docsetId);
182
+ expect(result).toEqual(progress);
183
+ });
184
+
185
+ it('should return all download progress', () => {
186
+ docsetService.downloadProgress.set('download-1', { percentage: 25 });
187
+ docsetService.downloadProgress.set('download-2', { percentage: 75 });
188
+
189
+ const allProgress = docsetService.getAllDownloadProgress();
190
+ expect(allProgress).toHaveLength(2);
191
+ expect(allProgress[0].percentage).toBe(25);
192
+ expect(allProgress[1].percentage).toBe(75);
193
+ });
194
+ });
195
+ });