@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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find:*)",
5
+ "Bash(xargs:*)",
6
+ "Bash(npm run build:*)",
7
+ "Bash(npm test:*)"
8
+ ]
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngao/search",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Search engine CLI and API with file watching",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Ngao Search Contributors",
@@ -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
+ });