mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-21 00:21:30 +00:00
Merge branch 'main' into copilot/migrate-java-scope-resolution
This commit is contained in:
commit
2db403f5b1
2 changed files with 113 additions and 4 deletions
|
|
@ -86,6 +86,9 @@ async function findRepoForCwd(cwd: string): Promise<{
|
|||
export async function augment(pattern: string, cwd?: string): Promise<string> {
|
||||
if (!pattern || pattern.length < 3) return '';
|
||||
|
||||
const patternFirstWord = pattern.trim().replace(/'/g, "''").split(/\s+/)[0];
|
||||
if (!patternFirstWord || patternFirstWord.length < 2) return '';
|
||||
|
||||
const workDir = cwd || process.cwd();
|
||||
|
||||
try {
|
||||
|
|
@ -104,9 +107,7 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
}
|
||||
|
||||
// Step 1: BM25 search (fast, no embeddings)
|
||||
const { results: bm25Results } = await searchFTSFromLbug(pattern, 10, repoId);
|
||||
|
||||
if (bm25Results.length === 0) return '';
|
||||
const { results: bm25Results, ftsAvailable } = await searchFTSFromLbug(pattern, 10, repoId);
|
||||
|
||||
// Step 2: Map BM25 file results to symbols
|
||||
const symbolMatches: Array<{
|
||||
|
|
@ -124,7 +125,7 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
repoId,
|
||||
`
|
||||
MATCH (n) WHERE n.filePath = '${escaped}'
|
||||
AND n.name CONTAINS '${pattern.replace(/'/g, "''").split(/\s+/)[0]}'
|
||||
AND n.name CONTAINS '${patternFirstWord}'
|
||||
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
||||
LIMIT 3
|
||||
`,
|
||||
|
|
@ -143,6 +144,29 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
// When FTS indexes are unavailable (read-only DB, first run before indexes are built),
|
||||
// fall back to a direct name CONTAINS query so enrichment still works.
|
||||
if (symbolMatches.length === 0 && !ftsAvailable) {
|
||||
const fallbackRows = await executeQuery(
|
||||
repoId,
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE n.name CONTAINS '${patternFirstWord}'
|
||||
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
||||
LIMIT 5
|
||||
`,
|
||||
).catch(() => []);
|
||||
for (const sym of fallbackRows) {
|
||||
symbolMatches.push({
|
||||
nodeId: sym.id || sym[0],
|
||||
name: sym.name || sym[1],
|
||||
type: sym.type || sym[2],
|
||||
filePath: sym.filePath || sym[3],
|
||||
score: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (symbolMatches.length === 0) return '';
|
||||
|
||||
// Step 3: Batch-fetch callers/callees/processes/cohesion for top matches
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ vi.mock('../../src/storage/repo-manager.js', () => ({
|
|||
}));
|
||||
|
||||
let augment: (pattern: string, cwd?: string) => Promise<string>;
|
||||
let augmentNoFts: (pattern: string, cwd?: string) => Promise<string>;
|
||||
|
||||
withTestLbugDB(
|
||||
'augment',
|
||||
|
|
@ -106,6 +107,28 @@ withTestLbugDB(
|
|||
const result = await augment('日本語テスト', handle.dbPath);
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
// ─── Negative-safety: fallback must stay gated on !ftsAvailable ───
|
||||
//
|
||||
// When FTS is available but happens to return zero BM25 hits, the
|
||||
// CONTAINS fallback must NOT fire — preserving the original early-return
|
||||
// semantics. If anyone later loosens the gate to `symbolMatches.length
|
||||
// === 0` alone, this test fails.
|
||||
|
||||
it('does NOT fire CONTAINS fallback when FTS is available but BM25 returns empty', async () => {
|
||||
const bm25 = await import('../../src/core/search/bm25-index.js');
|
||||
const spy = vi
|
||||
.spyOn(bm25, 'searchFTSFromLbug')
|
||||
.mockResolvedValue({ results: [], ftsAvailable: true });
|
||||
try {
|
||||
// 'login' WOULD match a graph node via CONTAINS, but FTS is available
|
||||
// and empty → fallback gate must hold → result must be ''.
|
||||
const result = await augment('login', handle.dbPath);
|
||||
expect(result).toBe('');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
|
|
@ -131,3 +154,65 @@ withTestLbugDB(
|
|||
},
|
||||
},
|
||||
);
|
||||
|
||||
// ─── FTS-unavailable suite: exercises the CONTAINS fallback branch ────────────
|
||||
//
|
||||
// No ftsIndexes → searchFTSFromLbug returns ftsAvailable: false → fallback fires.
|
||||
// Same seed data so 'login' still exists as a graph node.
|
||||
|
||||
withTestLbugDB(
|
||||
'augment-no-fts',
|
||||
(handle) => {
|
||||
describe('augment() — FTS indexes unavailable (CONTAINS fallback)', () => {
|
||||
it('falls back to CONTAINS query and returns enrichment when FTS is unavailable', async () => {
|
||||
const result = await augmentNoFts('login', handle.dbPath);
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result).toContain('[GitNexus]');
|
||||
});
|
||||
|
||||
it("returns empty string for whitespace-only pattern (CONTAINS '' guard)", async () => {
|
||||
const result = await augmentNoFts(' ', handle.dbPath);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when no nodes match the CONTAINS query', async () => {
|
||||
const result = await augmentNoFts('nxyz_notfound', handle.dbPath);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when fallback CONTAINS query throws', async () => {
|
||||
const poolAdapter = await import('../../src/core/lbug/pool-adapter.js');
|
||||
const spy = vi
|
||||
.spyOn(poolAdapter, 'executeQuery')
|
||||
.mockRejectedValue(new Error('simulated DB error'));
|
||||
try {
|
||||
const result = await augmentNoFts('login', handle.dbPath);
|
||||
expect(result).toBe('');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
seed: AUGMENT_SEED_DATA,
|
||||
// Intentionally no ftsIndexes — forces searchFTSFromLbug to return ftsAvailable: false
|
||||
poolAdapter: true,
|
||||
afterSetup: async (handle) => {
|
||||
const { listRegisteredRepos } = await import('../../src/storage/repo-manager.js');
|
||||
(listRegisteredRepos as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{
|
||||
name: handle.repoId,
|
||||
path: handle.dbPath,
|
||||
storagePath: handle.tmpHandle.dbPath,
|
||||
indexedAt: new Date().toISOString(),
|
||||
lastCommit: 'abc123',
|
||||
},
|
||||
]);
|
||||
|
||||
const engine = await import('../../src/core/augmentation/engine.js');
|
||||
augmentNoFts = engine.augment;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue