@ngao/search 0.1.1 → 0.1.2
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
CHANGED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { NgaoSearchService } from '../../src/backend/services/ngao-search-service';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
describe('Agent Tools Functionality', () => {
|
|
7
|
+
let service: NgaoSearchService;
|
|
8
|
+
let testProjectDir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
// Create a temporary test project directory
|
|
12
|
+
testProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ngao-agent-test-'));
|
|
13
|
+
|
|
14
|
+
// Create test files to index
|
|
15
|
+
const srcDir = path.join(testProjectDir, 'src');
|
|
16
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
// Create sample files
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(srcDir, 'auth.ts'),
|
|
21
|
+
`
|
|
22
|
+
export function authenticate(username: string, password: string): boolean {
|
|
23
|
+
if (!username || !password) return false;
|
|
24
|
+
return validateCredentials(username, password);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateCredentials(user: string, pass: string): boolean {
|
|
28
|
+
// Validation logic here
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class AuthService {
|
|
33
|
+
private token: string = '';
|
|
34
|
+
|
|
35
|
+
login(credentials: any): void {
|
|
36
|
+
// Login implementation
|
|
37
|
+
this.token = generateToken();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logout(): void {
|
|
41
|
+
this.token = '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private generateToken(): string {
|
|
45
|
+
return 'token-' + Date.now();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(srcDir, 'database.ts'),
|
|
53
|
+
`
|
|
54
|
+
export async function queryDatabase(sql: string): Promise<any[]> {
|
|
55
|
+
const connection = await createConnection();
|
|
56
|
+
try {
|
|
57
|
+
const results = await connection.query(sql);
|
|
58
|
+
return results;
|
|
59
|
+
} finally {
|
|
60
|
+
await connection.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function createConnection(): Promise<any> {
|
|
65
|
+
return {
|
|
66
|
+
query: async (sql: string) => [],
|
|
67
|
+
close: async () => {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class DatabasePool {
|
|
72
|
+
private pool: any[] = [];
|
|
73
|
+
|
|
74
|
+
async getConnection() {
|
|
75
|
+
return this.pool.pop() || await createConnection();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
releaseConnection(conn: any) {
|
|
79
|
+
this.pool.push(conn);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(srcDir, 'API.md'),
|
|
87
|
+
`
|
|
88
|
+
# API Documentation
|
|
89
|
+
|
|
90
|
+
## Authentication Endpoints
|
|
91
|
+
|
|
92
|
+
### POST /auth/login
|
|
93
|
+
Authenticates a user with credentials.
|
|
94
|
+
|
|
95
|
+
### POST /auth/logout
|
|
96
|
+
Logs out the current user.
|
|
97
|
+
|
|
98
|
+
### GET /auth/profile
|
|
99
|
+
Returns the authenticated user profile.
|
|
100
|
+
|
|
101
|
+
## Database Endpoints
|
|
102
|
+
|
|
103
|
+
### GET /api/users
|
|
104
|
+
Retrieves all users from the database.
|
|
105
|
+
|
|
106
|
+
### POST /api/users
|
|
107
|
+
Creates a new user in the database.
|
|
108
|
+
|
|
109
|
+
### PUT /api/users/:id
|
|
110
|
+
Updates a user record.
|
|
111
|
+
|
|
112
|
+
### DELETE /api/users/:id
|
|
113
|
+
Deletes a user record.
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Create service instance with test project
|
|
118
|
+
service = new NgaoSearchService(testProjectDir);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterAll(() => {
|
|
122
|
+
// Clean up test files
|
|
123
|
+
if (fs.existsSync(testProjectDir)) {
|
|
124
|
+
fs.rmSync(testProjectDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Agent Tool: search', () => {
|
|
129
|
+
beforeAll(async () => {
|
|
130
|
+
// Index the project before running search tests
|
|
131
|
+
await service.index({
|
|
132
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should perform semantic search', async () => {
|
|
137
|
+
const result = await service.search({
|
|
138
|
+
query: 'user authentication',
|
|
139
|
+
maxResults: 10,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
if (result.success) {
|
|
144
|
+
expect(typeof result.resultCount).toBe('number');
|
|
145
|
+
expect(typeof result.executionTimeMs).toBe('number');
|
|
146
|
+
expect(Array.isArray(result.results)).toBe(true);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should respect maxResults parameter', async () => {
|
|
151
|
+
const result = await service.search({
|
|
152
|
+
query: 'authenticate',
|
|
153
|
+
maxResults: 3,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
if (result.success) {
|
|
158
|
+
expect(result.results.length).toBeLessThanOrEqual(3);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should track search in session', async () => {
|
|
163
|
+
const session = service.createSession();
|
|
164
|
+
const sessionId = session.sessionId;
|
|
165
|
+
|
|
166
|
+
const searchResult = await service.search({
|
|
167
|
+
query: 'database',
|
|
168
|
+
sessionId,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(searchResult.success).toBe(true);
|
|
172
|
+
|
|
173
|
+
const history = service.getSearchHistory({ sessionId });
|
|
174
|
+
expect(history.success).toBe(true);
|
|
175
|
+
if (history.success && history.data) {
|
|
176
|
+
expect((history.data as any[]).length).toBeGreaterThan(0);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return ranked results with relevance scores', async () => {
|
|
181
|
+
const result = await service.search({
|
|
182
|
+
query: 'login',
|
|
183
|
+
maxResults: 5,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (result.success && result.results && result.results.length > 0) {
|
|
187
|
+
const firstResult = result.results[0];
|
|
188
|
+
expect(firstResult.rank).toBeDefined();
|
|
189
|
+
expect(typeof firstResult.score).toBe('number');
|
|
190
|
+
expect(firstResult.score).toBeGreaterThan(0);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Agent Tool: index', () => {
|
|
196
|
+
it('should index a directory successfully', async () => {
|
|
197
|
+
const result = await service.index({
|
|
198
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
if (result.success) {
|
|
203
|
+
expect(result.message).toBeDefined();
|
|
204
|
+
expect(result.stats).toBeDefined();
|
|
205
|
+
expect(result.stats.totalFiles).toBeGreaterThan(0);
|
|
206
|
+
expect(result.stats.totalBlocks).toBeGreaterThan(0);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return indexing statistics', async () => {
|
|
211
|
+
const result = await service.index({
|
|
212
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (result.success) {
|
|
216
|
+
const stats = result.stats;
|
|
217
|
+
expect(stats.totalFiles).toBeGreaterThanOrEqual(0);
|
|
218
|
+
expect(stats.totalBlocks).toBeGreaterThanOrEqual(0);
|
|
219
|
+
expect(stats.lastUpdated).toBeDefined();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Agent Tool: get_stats', () => {
|
|
225
|
+
beforeAll(async () => {
|
|
226
|
+
await service.index({
|
|
227
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return global indexing statistics', async () => {
|
|
232
|
+
const result = await service.getStats();
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(true);
|
|
235
|
+
if (result.success) {
|
|
236
|
+
expect(result.stats).toBeDefined();
|
|
237
|
+
expect(typeof result.stats.totalFiles).toBe('number');
|
|
238
|
+
expect(typeof result.stats.totalBlocks).toBe('number');
|
|
239
|
+
expect(result.stats.lastUpdated).toBeDefined();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should show that files have been indexed', async () => {
|
|
244
|
+
const result = await service.getStats();
|
|
245
|
+
|
|
246
|
+
if (result.success) {
|
|
247
|
+
expect(result.stats.totalBlocks).toBeGreaterThan(0);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('Agent Tool: get_index_info', () => {
|
|
253
|
+
beforeAll(async () => {
|
|
254
|
+
await service.index({
|
|
255
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should return detailed index information', async () => {
|
|
260
|
+
const result = await service.getIndexInfo();
|
|
261
|
+
|
|
262
|
+
expect(result.success).toBe(true);
|
|
263
|
+
if (result.success) {
|
|
264
|
+
expect(result.data).toBeDefined();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('Agent Tool: get_file_statistics', () => {
|
|
270
|
+
beforeAll(async () => {
|
|
271
|
+
await service.index({
|
|
272
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should return file statistics', async () => {
|
|
277
|
+
const result = await service.getFileStatistics({});
|
|
278
|
+
|
|
279
|
+
expect(result.success).toBe(true);
|
|
280
|
+
if (result.success) {
|
|
281
|
+
expect(result.data).toBeDefined();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('Agent Tool: get_search_history', () => {
|
|
287
|
+
it('should retrieve search history for a session', async () => {
|
|
288
|
+
const session = service.createSession();
|
|
289
|
+
const sessionId = session.sessionId;
|
|
290
|
+
|
|
291
|
+
// Perform searches
|
|
292
|
+
await service.search({ query: 'auth', sessionId });
|
|
293
|
+
await service.search({ query: 'database', sessionId });
|
|
294
|
+
|
|
295
|
+
// Get history
|
|
296
|
+
const result = service.getSearchHistory({ sessionId });
|
|
297
|
+
|
|
298
|
+
expect(result.success).toBe(true);
|
|
299
|
+
if (result.success && result.data) {
|
|
300
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
301
|
+
expect((result.data as any[]).length).toBeGreaterThanOrEqual(2);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should include query details in search history', async () => {
|
|
306
|
+
const session = service.createSession();
|
|
307
|
+
const sessionId = session.sessionId;
|
|
308
|
+
|
|
309
|
+
await service.search({ query: 'login', sessionId });
|
|
310
|
+
|
|
311
|
+
const result = service.getSearchHistory({ sessionId });
|
|
312
|
+
|
|
313
|
+
if (result.success && result.data) {
|
|
314
|
+
const data = result.data as any[];
|
|
315
|
+
if (data.length > 0) {
|
|
316
|
+
const entry = data[0];
|
|
317
|
+
expect(entry).toHaveProperty('query');
|
|
318
|
+
expect(entry).toHaveProperty('timestamp');
|
|
319
|
+
expect(entry).toHaveProperty('resultCount');
|
|
320
|
+
expect(entry).toHaveProperty('executionTimeMs');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should return error for invalid session', () => {
|
|
326
|
+
const result = service.getSearchHistory({ sessionId: 'invalid-session-id' });
|
|
327
|
+
|
|
328
|
+
expect(result.success).toBe(false);
|
|
329
|
+
if (!result.success) {
|
|
330
|
+
expect(result.error).toBeDefined();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('Agent Tool: start_reindex', () => {
|
|
336
|
+
it('should initiate reindex operation', async () => {
|
|
337
|
+
const result = await service.startReindex({
|
|
338
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
339
|
+
mode: 'incremental',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result.success).toBe(true);
|
|
343
|
+
if (result.success) {
|
|
344
|
+
expect(result.data.reindexId).toBeDefined();
|
|
345
|
+
expect(result.data.dirPath).toBe(path.join(testProjectDir, 'src'));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('Agent Tool: get_reindex_status', () => {
|
|
351
|
+
it('should return reindex operation status', async () => {
|
|
352
|
+
const startResult = await service.startReindex({
|
|
353
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (startResult.success) {
|
|
357
|
+
const statusResult = service.getReindexStatus({
|
|
358
|
+
reindexId: startResult.data.reindexId,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(statusResult.success).toBe(true);
|
|
362
|
+
if (statusResult.success) {
|
|
363
|
+
expect(statusResult.data).toBeDefined();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('Agent Tool: list_indexed_dirs', () => {
|
|
370
|
+
beforeAll(async () => {
|
|
371
|
+
await service.index({
|
|
372
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should list indexed directories', async () => {
|
|
377
|
+
const result = await service.listIndexedDirs();
|
|
378
|
+
|
|
379
|
+
expect(result.success).toBe(true);
|
|
380
|
+
if (result.success && result.data) {
|
|
381
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('Session Management (Agent Support)', () => {
|
|
387
|
+
it('should create a unique session per request', () => {
|
|
388
|
+
const session1 = service.createSession();
|
|
389
|
+
const session2 = service.createSession();
|
|
390
|
+
|
|
391
|
+
expect(session1.sessionId).not.toEqual(session2.sessionId);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should maintain separate search history per session', async () => {
|
|
395
|
+
await service.index({
|
|
396
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const session1 = service.createSession();
|
|
400
|
+
const session2 = service.createSession();
|
|
401
|
+
|
|
402
|
+
await service.search({ query: 'auth', sessionId: session1.sessionId });
|
|
403
|
+
await service.search({ query: 'database', sessionId: session2.sessionId });
|
|
404
|
+
|
|
405
|
+
const history1 = service.getSearchHistory({ sessionId: session1.sessionId });
|
|
406
|
+
const history2 = service.getSearchHistory({ sessionId: session2.sessionId });
|
|
407
|
+
|
|
408
|
+
if (history1.success && history1.data && history2.success && history2.data) {
|
|
409
|
+
expect((history1.data as any[]).length).toBeGreaterThan(0);
|
|
410
|
+
expect((history2.data as any[]).length).toBeGreaterThan(0);
|
|
411
|
+
expect(history1.data).not.toEqual(history2.data);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('Agent Workflow', () => {
|
|
417
|
+
it('should complete full agent workflow: index -> search -> stats -> history', async () => {
|
|
418
|
+
// Step 1: Create session
|
|
419
|
+
const session = service.createSession();
|
|
420
|
+
const sessionId = session.sessionId;
|
|
421
|
+
|
|
422
|
+
// Step 2: Index directory
|
|
423
|
+
const indexResult = await service.index({
|
|
424
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
425
|
+
});
|
|
426
|
+
expect(indexResult.success).toBe(true);
|
|
427
|
+
|
|
428
|
+
// Step 3: Perform search with session
|
|
429
|
+
const searchResult = await service.search({
|
|
430
|
+
query: 'authenticate user',
|
|
431
|
+
sessionId,
|
|
432
|
+
});
|
|
433
|
+
expect(searchResult.success).toBe(true);
|
|
434
|
+
|
|
435
|
+
// Step 4: Get stats
|
|
436
|
+
const statsResult = await service.getStats();
|
|
437
|
+
expect(statsResult.success).toBe(true);
|
|
438
|
+
if (statsResult.success) {
|
|
439
|
+
expect(statsResult.stats.totalBlocks).toBeGreaterThan(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Step 5: Get search history
|
|
443
|
+
const historyResult = service.getSearchHistory({ sessionId });
|
|
444
|
+
expect(historyResult.success).toBe(true);
|
|
445
|
+
if (historyResult.success && historyResult.data) {
|
|
446
|
+
expect((historyResult.data as any[]).length).toBeGreaterThan(0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Step 6: Get index info
|
|
450
|
+
const infoResult = await service.getIndexInfo();
|
|
451
|
+
expect(infoResult.success).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { NgaoSearchService } from '../../src/backend/services/ngao-search-service';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
describe('Search Integration Tests', () => {
|
|
7
|
+
let service: NgaoSearchService;
|
|
8
|
+
let testProjectDir: string;
|
|
9
|
+
let testCacheDir: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
// Create a temporary test project directory
|
|
13
|
+
testProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ngao-search-test-'));
|
|
14
|
+
testCacheDir = path.join(testProjectDir, '.ngao');
|
|
15
|
+
|
|
16
|
+
// Create test files to index
|
|
17
|
+
const srcDir = path.join(testProjectDir, 'src');
|
|
18
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Create a sample TypeScript file
|
|
21
|
+
fs.writeFileSync(
|
|
22
|
+
path.join(srcDir, 'example.ts'),
|
|
23
|
+
`
|
|
24
|
+
export function calculateSum(a: number, b: number): number {
|
|
25
|
+
return a + b;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Calculator {
|
|
29
|
+
multiply(x: number, y: number): number {
|
|
30
|
+
return x * y;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
divide(x: number, y: number): number {
|
|
34
|
+
if (y === 0) throw new Error('Division by zero');
|
|
35
|
+
return x / y;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const PI = 3.14159;
|
|
40
|
+
`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Create a sample JavaScript file
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(srcDir, 'utils.js'),
|
|
46
|
+
`
|
|
47
|
+
function greet(name) {
|
|
48
|
+
return \`Hello, \${name}!\`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function farewell(name) {
|
|
52
|
+
return \`Goodbye, \${name}!\`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { greet, farewell };
|
|
56
|
+
`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Create a sample Markdown file
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(srcDir, 'README.md'),
|
|
62
|
+
`
|
|
63
|
+
# Test Project
|
|
64
|
+
|
|
65
|
+
This is a test project for ngao-search integration testing.
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- Fast semantic search
|
|
70
|
+
- Multi-language support
|
|
71
|
+
- Indexing with embeddings
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
\`\`\`typescript
|
|
76
|
+
const service = new NgaoSearchService();
|
|
77
|
+
await service.index('./src');
|
|
78
|
+
const results = await service.search({ query: 'calculate' });
|
|
79
|
+
\`\`\`
|
|
80
|
+
`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Create service instance with test project
|
|
84
|
+
service = new NgaoSearchService(testProjectDir);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterAll(() => {
|
|
88
|
+
// Clean up test files
|
|
89
|
+
if (fs.existsSync(testProjectDir)) {
|
|
90
|
+
fs.rmSync(testProjectDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Index and Search Workflow', () => {
|
|
95
|
+
it('should index a directory and return stats', async () => {
|
|
96
|
+
const indexResult = await service.index({
|
|
97
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(indexResult.success).toBe(true);
|
|
101
|
+
if (indexResult.success) {
|
|
102
|
+
expect(indexResult.stats).toBeDefined();
|
|
103
|
+
expect(indexResult.stats.totalFiles).toBeGreaterThan(0);
|
|
104
|
+
expect(indexResult.stats.totalBlocks).toBeGreaterThan(0);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should search for indexed content after indexing', async () => {
|
|
109
|
+
// First ensure index is populated
|
|
110
|
+
await service.index({
|
|
111
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Search for something that should be indexed
|
|
115
|
+
const searchResult = await service.search({
|
|
116
|
+
query: 'calculate',
|
|
117
|
+
maxResults: 5,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(searchResult.success).toBe(true);
|
|
121
|
+
if (searchResult.success) {
|
|
122
|
+
expect(searchResult.resultCount).toBeGreaterThanOrEqual(0);
|
|
123
|
+
expect(searchResult.results).toBeDefined();
|
|
124
|
+
expect(Array.isArray(searchResult.results)).toBe(true);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return results with expected structure', async () => {
|
|
129
|
+
await service.index({
|
|
130
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const searchResult = await service.search({
|
|
134
|
+
query: 'function',
|
|
135
|
+
maxResults: 10,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (searchResult.success && searchResult.results && searchResult.results.length > 0) {
|
|
139
|
+
const result = searchResult.results[0];
|
|
140
|
+
expect(result).toHaveProperty('rank');
|
|
141
|
+
expect(result).toHaveProperty('score');
|
|
142
|
+
expect(result).toHaveProperty('type');
|
|
143
|
+
expect(result).toHaveProperty('name');
|
|
144
|
+
expect(result).toHaveProperty('file');
|
|
145
|
+
expect(result).toHaveProperty('lineRange');
|
|
146
|
+
expect(result).toHaveProperty('scope');
|
|
147
|
+
expect(result).toHaveProperty('content');
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should get statistics about indexed content', async () => {
|
|
152
|
+
await service.index({
|
|
153
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const stats = await service.getStats();
|
|
157
|
+
|
|
158
|
+
expect(stats.success).toBe(true);
|
|
159
|
+
if (stats.success) {
|
|
160
|
+
expect(stats.stats).toBeDefined();
|
|
161
|
+
expect(stats.stats.totalFiles).toBeGreaterThan(0);
|
|
162
|
+
expect(stats.stats.totalBlocks).toBeGreaterThan(0);
|
|
163
|
+
expect(stats.stats.lastUpdated).toBeDefined();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should create a session and track search history', () => {
|
|
168
|
+
const sessionResponse = service.createSession();
|
|
169
|
+
|
|
170
|
+
expect(sessionResponse.sessionId).toBeDefined();
|
|
171
|
+
expect(typeof sessionResponse.sessionId).toBe('string');
|
|
172
|
+
expect(sessionResponse.sessionId.length).toBeGreaterThan(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should track searches in session history', async () => {
|
|
176
|
+
await service.index({
|
|
177
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Create a session
|
|
181
|
+
const sessionResponse = service.createSession();
|
|
182
|
+
const sessionId = sessionResponse.sessionId;
|
|
183
|
+
|
|
184
|
+
// Perform a search with session ID
|
|
185
|
+
const searchResult = await service.search({
|
|
186
|
+
query: 'test',
|
|
187
|
+
sessionId,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(searchResult.success).toBe(true);
|
|
191
|
+
|
|
192
|
+
// Get search history
|
|
193
|
+
const historyResult = service.getSearchHistory({ sessionId });
|
|
194
|
+
|
|
195
|
+
expect(historyResult.success).toBe(true);
|
|
196
|
+
if (historyResult.success && historyResult.data) {
|
|
197
|
+
expect(Array.isArray(historyResult.data)).toBe(true);
|
|
198
|
+
expect((historyResult.data as any[]).length).toBeGreaterThan(0);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle searches with no results gracefully', async () => {
|
|
203
|
+
await service.index({
|
|
204
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const searchResult = await service.search({
|
|
208
|
+
query: 'xyzabc123nonexistentquery',
|
|
209
|
+
maxResults: 5,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(searchResult.success).toBe(true);
|
|
213
|
+
if (searchResult.success) {
|
|
214
|
+
expect(searchResult.resultCount).toBe(0);
|
|
215
|
+
expect(Array.isArray(searchResult.results)).toBe(true);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should list indexed directories', async () => {
|
|
220
|
+
await service.index({
|
|
221
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const listResult = await service.listIndexedDirs();
|
|
225
|
+
|
|
226
|
+
expect(listResult.success).toBe(true);
|
|
227
|
+
if (listResult.success && listResult.data) {
|
|
228
|
+
expect(Array.isArray(listResult.data)).toBe(true);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should get index information', async () => {
|
|
233
|
+
await service.index({
|
|
234
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const infoResult = await service.getIndexInfo();
|
|
238
|
+
|
|
239
|
+
expect(infoResult.success).toBe(true);
|
|
240
|
+
if (infoResult.success) {
|
|
241
|
+
expect(infoResult.data).toBeDefined();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Cache Management', () => {
|
|
247
|
+
it('should use cache directory for indexed data', async () => {
|
|
248
|
+
await service.index({
|
|
249
|
+
dirPath: path.join(testProjectDir, 'src'),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Check that cache directory was created
|
|
253
|
+
expect(fs.existsSync(testCacheDir)).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|