Files
codegraph/__tests__/field-name-retrieval.test.ts
T
1de7e8f8b5 fix(retrieval): multi-hump field-name queries reach their definers (#1319)
Three compounding defects (#1196) made a query bag of object-literal
keys (`profileInfo isTrialEligible quotaInfo billingMethod`) return
unrelated results while the defining files never surfaced:

1. Step 5b title-cased interior humps (profileInfo -> Profileinfo) and
   then compared case-SENSITIVELY, dropping every row SQLite's
   case-insensitive LIKE had just recovered. The hump lookup is now
   case-insensitive with an explicit uppercase-at-match requirement.
2. Step 5b/5c's kind whitelist held only type-like kinds — dead code on
   method-centric codebases. Callable kinds (function/method/component)
   are fetched as a SEPARATE LIKE batch so hot single-word terms can't
   crowd classes out of the length-ordered 200-row batch.
3. explore's named-symbol seeding was exact-name only; a field token
   seeded nothing. A camelCase token with ZERO exact defs now seeds its
   camel-infix definers (callables, hump-boundary or prefix, shortest
   first, capped at 3) — bare lowercase words keep the #1252 stopword
   guard untouched.

The reporter's acceptance query is a pinned e2e test (definer files
present, exact-name seeding unaffected). excalidraw probe: the
canonical flow query (mutateElement renderStaticScene) is byte-
identical; NL queries shift toward more-central callables
(useUIAppState/getDefaultAppState over observer periphery).

Fixes #1196

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:11:24 -05:00

104 lines
3.9 KiB
TypeScript

/**
* Multi-word FIELD-NAME query retrieval (#1196).
*
* A query bag of object-literal keys / API field names (`profileInfo
* isTrialEligible quotaInfo billingMethod`) has no nodes of its own — the
* definers are methods whose names contain each token at a camel-hump
* boundary (`profileInfo` → `getProfileInfoV2`). Three compounding defects
* made those definers unreachable:
* 1. the CamelCase-boundary LIKE step title-cased interior humps
* (`profileInfo` → `Profileinfo`) and then compared case-SENSITIVELY,
* dropping every row SQLite's case-insensitive LIKE had just found;
* 2. that step's kind whitelist held only type-like kinds, so on
* method-centric codebases it contributed nothing at all;
* 3. explore's named-symbol seeding was exact-name only, so a field token
* seeded no files and the output budget went to unrelated neighbors.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
import { ToolHandler } from '../src/mcp/tools';
describe('field-name query retrieval (#1196)', () => {
let testDir: string;
let cg: CodeGraph;
let handler: ToolHandler;
beforeEach(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1196-'));
fs.mkdirSync(path.join(testDir, 'controller'), { recursive: true });
fs.mkdirSync(path.join(testDir, 'service'), { recursive: true });
fs.writeFileSync(
path.join(testDir, 'controller', 'profileController.js'),
`const billing = require('../service/billing');
class ProfileController {
getProfileInfo(userId) {
return { profileInfo: { id: userId }, isTrialEligible: this.checkTrialEligibility(userId) };
}
getProfileInfoV2(userId) {
const quotaInfo = this.loadQuotaInfo(userId);
return { profileInfo: { id: userId }, quotaInfo, billingMethod: billing.getBillingMethod(userId) };
}
checkTrialEligibility(userId) { return userId > 100; }
loadQuotaInfo(userId) { return { used: 1, max: 10, userId }; }
}
module.exports = new ProfileController();
`
);
fs.writeFileSync(
path.join(testDir, 'service', 'billing.js'),
`function _getCustomerBillingMethods(userId) {
return [{ type: 'card', userId }];
}
function getBillingMethod(userId) {
return _getCustomerBillingMethods(userId)[0];
}
module.exports = { getBillingMethod };
`
);
// Noise files so the definers aren't the only content.
for (let i = 1; i <= 5; i++) {
fs.writeFileSync(
path.join(testDir, 'service', `noise${i}.js`),
`function unrelatedHelper${i}() { return ${i}; }\nmodule.exports = { unrelatedHelper${i} };\n`
);
}
cg = CodeGraph.initSync(testDir);
await cg.indexAll();
handler = new ToolHandler(cg);
});
afterEach(() => {
if (cg) cg.destroy();
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
});
it('a bag of field-name tokens surfaces the files that DEFINE those fields', async () => {
const res = await handler.execute('codegraph_explore', {
query: 'profileInfo isTrialEligible quotaInfo billingMethod',
});
const text = res.content[0]!.text as string;
// The two definer files the reporter saw entirely absent.
expect(text).toContain('profileController.js');
expect(text).toContain('billing.js');
// The camel-infix definers themselves are shown.
expect(text).toMatch(/getProfileInfo(V2)?/);
expect(text).toContain('BillingMethod');
});
it('exact-name seeding still wins when the token IS a real symbol', async () => {
// `getBillingMethod` names a real function — the fallback must not
// dilute or replace exact seeding.
const res = await handler.execute('codegraph_explore', { query: 'getBillingMethod' });
const text = res.content[0]!.text as string;
expect(text).toContain('billing.js');
expect(text).toContain('getBillingMethod');
});
});