1
0

field-name-retrieval.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. /**
  2. * Multi-word FIELD-NAME query retrieval (#1196).
  3. *
  4. * A query bag of object-literal keys / API field names (`profileInfo
  5. * isTrialEligible quotaInfo billingMethod`) has no nodes of its own — the
  6. * definers are methods whose names contain each token at a camel-hump
  7. * boundary (`profileInfo` → `getProfileInfoV2`). Three compounding defects
  8. * made those definers unreachable:
  9. * 1. the CamelCase-boundary LIKE step title-cased interior humps
  10. * (`profileInfo` → `Profileinfo`) and then compared case-SENSITIVELY,
  11. * dropping every row SQLite's case-insensitive LIKE had just found;
  12. * 2. that step's kind whitelist held only type-like kinds, so on
  13. * method-centric codebases it contributed nothing at all;
  14. * 3. explore's named-symbol seeding was exact-name only, so a field token
  15. * seeded no files and the output budget went to unrelated neighbors.
  16. */
  17. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  18. import * as fs from 'fs';
  19. import * as path from 'path';
  20. import * as os from 'os';
  21. import CodeGraph from '../src/index';
  22. import { ToolHandler } from '../src/mcp/tools';
  23. describe('field-name query retrieval (#1196)', () => {
  24. let testDir: string;
  25. let cg: CodeGraph;
  26. let handler: ToolHandler;
  27. beforeEach(async () => {
  28. testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1196-'));
  29. fs.mkdirSync(path.join(testDir, 'controller'), { recursive: true });
  30. fs.mkdirSync(path.join(testDir, 'service'), { recursive: true });
  31. fs.writeFileSync(
  32. path.join(testDir, 'controller', 'profileController.js'),
  33. `const billing = require('../service/billing');
  34. class ProfileController {
  35. getProfileInfo(userId) {
  36. return { profileInfo: { id: userId }, isTrialEligible: this.checkTrialEligibility(userId) };
  37. }
  38. getProfileInfoV2(userId) {
  39. const quotaInfo = this.loadQuotaInfo(userId);
  40. return { profileInfo: { id: userId }, quotaInfo, billingMethod: billing.getBillingMethod(userId) };
  41. }
  42. checkTrialEligibility(userId) { return userId > 100; }
  43. loadQuotaInfo(userId) { return { used: 1, max: 10, userId }; }
  44. }
  45. module.exports = new ProfileController();
  46. `
  47. );
  48. fs.writeFileSync(
  49. path.join(testDir, 'service', 'billing.js'),
  50. `function _getCustomerBillingMethods(userId) {
  51. return [{ type: 'card', userId }];
  52. }
  53. function getBillingMethod(userId) {
  54. return _getCustomerBillingMethods(userId)[0];
  55. }
  56. module.exports = { getBillingMethod };
  57. `
  58. );
  59. // Noise files so the definers aren't the only content.
  60. for (let i = 1; i <= 5; i++) {
  61. fs.writeFileSync(
  62. path.join(testDir, 'service', `noise${i}.js`),
  63. `function unrelatedHelper${i}() { return ${i}; }\nmodule.exports = { unrelatedHelper${i} };\n`
  64. );
  65. }
  66. cg = CodeGraph.initSync(testDir);
  67. await cg.indexAll();
  68. handler = new ToolHandler(cg);
  69. });
  70. afterEach(() => {
  71. if (cg) cg.destroy();
  72. if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
  73. });
  74. it('a bag of field-name tokens surfaces the files that DEFINE those fields', async () => {
  75. const res = await handler.execute('codegraph_explore', {
  76. query: 'profileInfo isTrialEligible quotaInfo billingMethod',
  77. });
  78. const text = res.content[0]!.text as string;
  79. // The two definer files the reporter saw entirely absent.
  80. expect(text).toContain('profileController.js');
  81. expect(text).toContain('billing.js');
  82. // The camel-infix definers themselves are shown.
  83. expect(text).toMatch(/getProfileInfo(V2)?/);
  84. expect(text).toContain('BillingMethod');
  85. });
  86. it('exact-name seeding still wins when the token IS a real symbol', async () => {
  87. // `getBillingMethod` names a real function — the fallback must not
  88. // dilute or replace exact seeding.
  89. const res = await handler.execute('codegraph_explore', { query: 'getBillingMethod' });
  90. const text = res.content[0]!.text as string;
  91. expect(text).toContain('billing.js');
  92. expect(text).toContain('getBillingMethod');
  93. });
  94. });