|
3 | 3 | */ |
4 | 4 |
|
5 | 5 | import { describe, it, expect } from 'vitest'; |
6 | | -import { searchSpecs } from './engine.js'; |
| 6 | +import { searchSpecs, advancedSearchSpecs } from './engine.js'; |
7 | 7 | import type { SearchableSpec } from './engine.js'; |
8 | 8 |
|
9 | 9 | describe('Search Engine', () => { |
@@ -405,3 +405,214 @@ Basic endpoint documentation. |
405 | 405 | }); |
406 | 406 | }); |
407 | 407 | }); |
| 408 | + |
| 409 | +describe('Advanced Search (spec 124 Phase 2)', () => { |
| 410 | + const sampleSpecs: SearchableSpec[] = [ |
| 411 | + { |
| 412 | + path: '042-oauth2-implementation', |
| 413 | + name: '042-oauth2-implementation', |
| 414 | + status: 'in-progress', |
| 415 | + priority: 'high', |
| 416 | + tags: ['api', 'security', 'auth'], |
| 417 | + title: 'OAuth2 Authentication Flow', |
| 418 | + description: 'Implement OAuth2 authentication with token refresh', |
| 419 | + content: 'OAuth2 flow supports authorization code grant with PKCE.', |
| 420 | + created: '2025-11-01', |
| 421 | + updated: '2025-11-15', |
| 422 | + assignee: 'marvin', |
| 423 | + }, |
| 424 | + { |
| 425 | + path: '038-jwt-token-service', |
| 426 | + name: '038-jwt-token-service', |
| 427 | + status: 'complete', |
| 428 | + priority: 'medium', |
| 429 | + tags: ['api', 'auth'], |
| 430 | + title: 'JWT Token Service', |
| 431 | + description: 'JWT-based authentication service', |
| 432 | + content: 'JWT authentication flow with RS256 signing.', |
| 433 | + created: '2025-10-15', |
| 434 | + updated: '2025-11-10', |
| 435 | + assignee: 'alice', |
| 436 | + }, |
| 437 | + { |
| 438 | + path: '051-user-session-management', |
| 439 | + name: '051-user-session-management', |
| 440 | + status: 'planned', |
| 441 | + priority: 'medium', |
| 442 | + tags: ['api', 'users'], |
| 443 | + title: 'User Session Management', |
| 444 | + description: 'Handle user sessions and authentication state', |
| 445 | + content: 'Manage user sessions across multiple devices.', |
| 446 | + created: '2025-11-20', |
| 447 | + assignee: 'bob', |
| 448 | + }, |
| 449 | + { |
| 450 | + path: '025-api-rate-limiting', |
| 451 | + name: '025-api-rate-limiting', |
| 452 | + status: 'complete', |
| 453 | + priority: 'high', |
| 454 | + tags: ['api', 'security'], |
| 455 | + title: 'API Rate Limiting', |
| 456 | + description: 'Rate limiting for API endpoints (deprecated)', |
| 457 | + content: 'Implement rate limiting to prevent abuse.', |
| 458 | + created: '2025-09-01', |
| 459 | + updated: '2025-09-15', |
| 460 | + }, |
| 461 | + ]; |
| 462 | + |
| 463 | + describe('Boolean operators', () => { |
| 464 | + it('should support AND operator', () => { |
| 465 | + const result = advancedSearchSpecs('api AND authentication', sampleSpecs); |
| 466 | + |
| 467 | + // Should find specs with both "api" and "authentication" |
| 468 | + expect(result.results.length).toBeGreaterThan(0); |
| 469 | + // OAuth2 spec should match (has both in various fields) |
| 470 | + const names = result.results.map(r => r.spec.name); |
| 471 | + expect(names).toContain('042-oauth2-implementation'); |
| 472 | + }); |
| 473 | + |
| 474 | + it('should support OR operator', () => { |
| 475 | + const result = advancedSearchSpecs('session OR token', sampleSpecs); |
| 476 | + |
| 477 | + // Should find specs with either "session" or "token" |
| 478 | + expect(result.results.length).toBeGreaterThan(0); |
| 479 | + const names = result.results.map(r => r.spec.name); |
| 480 | + expect(names).toContain('051-user-session-management'); |
| 481 | + expect(names).toContain('038-jwt-token-service'); |
| 482 | + }); |
| 483 | + |
| 484 | + it('should support NOT operator', () => { |
| 485 | + const result = advancedSearchSpecs('api NOT deprecated', sampleSpecs); |
| 486 | + |
| 487 | + // Should find specs with "api" but not "deprecated" |
| 488 | + const names = result.results.map(r => r.spec.name); |
| 489 | + expect(names).not.toContain('025-api-rate-limiting'); |
| 490 | + expect(names).toContain('042-oauth2-implementation'); |
| 491 | + }); |
| 492 | + |
| 493 | + it('should support parentheses for grouping', () => { |
| 494 | + const result = advancedSearchSpecs('(session OR token) AND authentication', sampleSpecs); |
| 495 | + |
| 496 | + // Should find specs matching (session OR token) AND authentication |
| 497 | + expect(result.results.length).toBeGreaterThan(0); |
| 498 | + }); |
| 499 | + }); |
| 500 | + |
| 501 | + describe('Field-specific search', () => { |
| 502 | + it('should filter by status', () => { |
| 503 | + const result = advancedSearchSpecs('status:in-progress', sampleSpecs); |
| 504 | + |
| 505 | + expect(result.results.length).toBe(1); |
| 506 | + expect(result.results[0].spec.status).toBe('in-progress'); |
| 507 | + }); |
| 508 | + |
| 509 | + it('should filter by tag', () => { |
| 510 | + const result = advancedSearchSpecs('tag:security', sampleSpecs); |
| 511 | + |
| 512 | + expect(result.results.length).toBe(2); |
| 513 | + for (const r of result.results) { |
| 514 | + expect(r.spec.tags).toContain('security'); |
| 515 | + } |
| 516 | + }); |
| 517 | + |
| 518 | + it('should filter by priority', () => { |
| 519 | + const result = advancedSearchSpecs('priority:high', sampleSpecs); |
| 520 | + |
| 521 | + expect(result.results.length).toBe(2); |
| 522 | + for (const r of result.results) { |
| 523 | + expect(r.spec.priority).toBe('high'); |
| 524 | + } |
| 525 | + }); |
| 526 | + |
| 527 | + it('should combine field filters with search terms', () => { |
| 528 | + const result = advancedSearchSpecs('tag:api status:planned', sampleSpecs); |
| 529 | + |
| 530 | + expect(result.results.length).toBe(1); |
| 531 | + expect(result.results[0].spec.name).toBe('051-user-session-management'); |
| 532 | + }); |
| 533 | + |
| 534 | + it('should filter by title', () => { |
| 535 | + const result = advancedSearchSpecs('title:OAuth2', sampleSpecs); |
| 536 | + |
| 537 | + expect(result.results.length).toBe(1); |
| 538 | + expect(result.results[0].spec.title).toContain('OAuth2'); |
| 539 | + }); |
| 540 | + }); |
| 541 | + |
| 542 | + describe('Date range filters', () => { |
| 543 | + it('should filter by created date (greater than)', () => { |
| 544 | + const result = advancedSearchSpecs('created:>2025-11-01', sampleSpecs); |
| 545 | + |
| 546 | + // Should find specs created after Nov 1, 2025 |
| 547 | + expect(result.results.length).toBeGreaterThan(0); |
| 548 | + for (const r of result.results) { |
| 549 | + const spec = sampleSpecs.find(s => s.name === r.spec.name); |
| 550 | + expect(spec?.created).toBeDefined(); |
| 551 | + expect(spec!.created! > '2025-11-01').toBe(true); |
| 552 | + } |
| 553 | + }); |
| 554 | + |
| 555 | + it('should filter by created date (less than)', () => { |
| 556 | + const result = advancedSearchSpecs('created:<2025-10-01', sampleSpecs); |
| 557 | + |
| 558 | + // Should find specs created before Oct 1, 2025 |
| 559 | + expect(result.results.length).toBe(1); |
| 560 | + expect(result.results[0].spec.name).toBe('025-api-rate-limiting'); |
| 561 | + }); |
| 562 | + |
| 563 | + it('should filter by date range', () => { |
| 564 | + const result = advancedSearchSpecs('created:2025-10-01..2025-11-01', sampleSpecs); |
| 565 | + |
| 566 | + // Should find specs created between Oct 1 and Nov 1, 2025 |
| 567 | + expect(result.results.length).toBeGreaterThan(0); |
| 568 | + for (const r of result.results) { |
| 569 | + const spec = sampleSpecs.find(s => s.name === r.spec.name); |
| 570 | + expect(spec?.created).toBeDefined(); |
| 571 | + expect(spec!.created! >= '2025-10-01').toBe(true); |
| 572 | + expect(spec!.created! <= '2025-11-01').toBe(true); |
| 573 | + } |
| 574 | + }); |
| 575 | + }); |
| 576 | + |
| 577 | + describe('Fuzzy matching', () => { |
| 578 | + it('should find specs with typo-tolerant search', () => { |
| 579 | + const result = advancedSearchSpecs('authetication~', sampleSpecs); |
| 580 | + |
| 581 | + // Should find specs with "authentication" despite the typo |
| 582 | + expect(result.results.length).toBeGreaterThan(0); |
| 583 | + }); |
| 584 | + |
| 585 | + it('should not fuzzy match completely different words', () => { |
| 586 | + const result = advancedSearchSpecs('completely_different_word~', sampleSpecs); |
| 587 | + |
| 588 | + // Should not find any specs |
| 589 | + expect(result.results.length).toBe(0); |
| 590 | + }); |
| 591 | + }); |
| 592 | + |
| 593 | + describe('Complex queries', () => { |
| 594 | + it('should combine field filters, boolean operators, and search terms', () => { |
| 595 | + const result = advancedSearchSpecs('tag:api status:in-progress oauth', sampleSpecs); |
| 596 | + |
| 597 | + expect(result.results.length).toBe(1); |
| 598 | + expect(result.results[0].spec.name).toBe('042-oauth2-implementation'); |
| 599 | + }); |
| 600 | + |
| 601 | + it('should handle quoted phrases', () => { |
| 602 | + const result = advancedSearchSpecs('"token refresh"', sampleSpecs); |
| 603 | + |
| 604 | + // Should find specs with exact phrase "token refresh" |
| 605 | + expect(result.results.length).toBeGreaterThan(0); |
| 606 | + }); |
| 607 | + }); |
| 608 | + |
| 609 | + describe('Backward compatibility', () => { |
| 610 | + it('should work with simple queries (no advanced syntax)', () => { |
| 611 | + const result = advancedSearchSpecs('authentication', sampleSpecs); |
| 612 | + |
| 613 | + // Should behave like regular search |
| 614 | + expect(result.results.length).toBeGreaterThan(0); |
| 615 | + expect(result.metadata.query).toBe('authentication'); |
| 616 | + }); |
| 617 | + }); |
| 618 | +}); |
0 commit comments