@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.
- package/README.md +226 -294
- package/bin/doc-bot.js +2 -3
- package/package.json +8 -7
- package/src/__tests__/docset-integration.test.js +146 -0
- package/src/__tests__/temp-docs-1752689978225/test.md +5 -0
- package/src/__tests__/temp-docs-1752689978235/test.md +5 -0
- package/src/__tests__/temp-docs-1752689978241/test.md +5 -0
- package/src/__tests__/temp-docs-1752689978243/test.md +5 -0
- package/src/__tests__/temp-docs-1752689978244/test.md +5 -0
- package/src/__tests__/temp-docsets-1752689978244/7e2cbc65/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1752689978244/7e2cbc65/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1752689978244/Mock.docset/Contents/Info.plist +10 -0
- package/src/__tests__/temp-docsets-1752689978244/Mock.docset/Contents/Resources/docSet.dsidx +0 -0
- package/src/__tests__/temp-docsets-1752689978244/docsets.json +10 -0
- package/src/index.js +474 -28
- package/src/services/DocumentationService.js +131 -2
- package/src/services/UnifiedSearchService.js +214 -0
- package/src/services/__tests__/DocumentationService.test.js +318 -0
- package/src/services/__tests__/UnifiedSearchService.test.js +302 -0
- package/src/services/docset/ParallelSearchManager.js +158 -0
- package/src/services/docset/__tests__/DocsetDatabase.test.js +337 -0
- package/src/services/docset/__tests__/DocsetService.test.js +195 -0
- package/src/services/docset/__tests__/EnhancedDocsetDatabase.test.js +324 -0
- package/src/services/docset/database.js +474 -0
- 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
|
+
});
|