|
| 1 | +import { describe, expect, it, beforeEach, afterEach } from 'bun:test' |
| 2 | +import { |
| 3 | + addGlobalEntity, |
| 4 | + searchGlobalGraph, |
| 5 | + resetGlobalGraph, |
| 6 | + clearMemoryOnly |
| 7 | +} from './knowledgeGraph.js' |
| 8 | +import { mkdtempSync, rmSync } from 'fs' |
| 9 | +import { tmpdir } from 'os' |
| 10 | +import { join } from 'path' |
| 11 | +import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js' |
| 12 | +import { setClaudeConfigHomeDirForTesting } from './envUtils.js' |
| 13 | + |
| 14 | +describe('KnowledgeGraph Search Optimizations', () => { |
| 15 | + const originalConfigDir = process.env.CLAUDE_CONFIG_DIR |
| 16 | + let configDir: string | undefined |
| 17 | + |
| 18 | + beforeEach(async () => { |
| 19 | + await acquireEnvMutex() |
| 20 | + configDir = mkdtempSync(join(tmpdir(), 'openclaude-opt-test-')) |
| 21 | + process.env.CLAUDE_CONFIG_DIR = configDir |
| 22 | + process.env.OPENCLAUDE_KNOWLEDGE_ORAMA = '1' |
| 23 | + setClaudeConfigHomeDirForTesting(configDir) |
| 24 | + resetGlobalGraph() |
| 25 | + }) |
| 26 | + |
| 27 | + afterEach(() => { |
| 28 | + try { |
| 29 | + resetGlobalGraph() |
| 30 | + clearMemoryOnly() |
| 31 | + if (originalConfigDir === undefined) { |
| 32 | + delete process.env.CLAUDE_CONFIG_DIR |
| 33 | + } else { |
| 34 | + process.env.CLAUDE_CONFIG_DIR = originalConfigDir |
| 35 | + } |
| 36 | + setClaudeConfigHomeDirForTesting(undefined) |
| 37 | + } finally { |
| 38 | + if (configDir) { |
| 39 | + try { rmSync(configDir, { recursive: true, force: true }) } catch {} |
| 40 | + } |
| 41 | + releaseEnvMutex() |
| 42 | + } |
| 43 | + }) |
| 44 | + |
| 45 | + it('finds entities even with minor typos (Typo Tolerance)', async () => { |
| 46 | + await addGlobalEntity('tool', 'AuthenticationManager', { desc: 'Handles user login' }) |
| 47 | + |
| 48 | + // Search with a small typo: "Authenticatin" (missing 'o') |
| 49 | + const result = await searchGlobalGraph('Authenticatin') |
| 50 | + expect(result).toContain('AuthenticationManager') |
| 51 | + |
| 52 | + // Search with another typo: "Managerr" (extra 'r') |
| 53 | + const result2 = await searchGlobalGraph('Managerr') |
| 54 | + expect(result2).toContain('AuthenticationManager') |
| 55 | + }) |
| 56 | + |
| 57 | + it('ranks matches in name higher than matches in content (Field Boosting)', async () => { |
| 58 | + // Entity A has the term in the name |
| 59 | + await addGlobalEntity('service', 'BillingService', { desc: 'Internal system' }) |
| 60 | + // Entity B has the term only in the description |
| 61 | + await addGlobalEntity('doc', 'README', { desc: 'Information about billing' }) |
| 62 | + |
| 63 | + const result = await searchGlobalGraph('billing') |
| 64 | + |
| 65 | + // We expect BillingService to be listed before README because of the boost on 'name' |
| 66 | + const billingPos = result.indexOf('BillingService') |
| 67 | + const readmePos = result.indexOf('README') |
| 68 | + |
| 69 | + expect(billingPos).toBeGreaterThan(-1) |
| 70 | + expect(readmePos).toBeGreaterThan(-1) |
| 71 | + expect(billingPos).toBeLessThan(readmePos) |
| 72 | + }) |
| 73 | + |
| 74 | + it('ranks matches in type higher than matches in content (Field Boosting)', async () => { |
| 75 | + // Entity A has the term in the type |
| 76 | + await addGlobalEntity('Database', 'Store', { desc: 'Main storage' }) |
| 77 | + // Entity B has the term only in the description |
| 78 | + await addGlobalEntity('note', 'Todo', { desc: 'Upgrade the database' }) |
| 79 | + |
| 80 | + const result = await searchGlobalGraph('database') |
| 81 | + |
| 82 | + const dbPos = result.indexOf('[Database] Store') |
| 83 | + const todoPos = result.indexOf('Todo') |
| 84 | + |
| 85 | + expect(dbPos).toBeGreaterThan(-1) |
| 86 | + expect(todoPos).toBeGreaterThan(-1) |
| 87 | + expect(dbPos).toBeLessThan(todoPos) |
| 88 | + }) |
| 89 | +}) |
0 commit comments