node-file-view.test.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * codegraph_node FILE-VIEW mode: a bare `file` (no `symbol`) returns that file's
  3. * symbol map + graph role (dependents), and verbatim bodies with includeCode —
  4. * a Read replacement for a source file that also surfaces the blast radius.
  5. */
  6. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import CodeGraph from '../src/index';
  11. import { ToolHandler } from '../src/mcp/tools';
  12. describe('codegraph_node file-view (Read replacement)', () => {
  13. let dir: string;
  14. let cg: CodeGraph;
  15. let h: ToolHandler;
  16. beforeEach(async () => {
  17. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fileview-'));
  18. fs.mkdirSync(path.join(dir, 'src'));
  19. fs.writeFileSync(
  20. path.join(dir, 'src', 'a.ts'),
  21. 'export function helper(x: number) {\n return x + 1;\n}\nexport class Widget {\n build() { return helper(1); }\n}\n',
  22. );
  23. fs.writeFileSync(
  24. path.join(dir, 'src', 'b.ts'),
  25. "import { helper } from './a';\nexport function useHelper() { return helper(2); }\n",
  26. );
  27. cg = CodeGraph.initSync(dir, { config: { include: ['**/*.ts'], exclude: [] } });
  28. await cg.indexAll();
  29. h = new ToolHandler(cg);
  30. });
  31. afterEach(() => {
  32. if (cg) cg.close();
  33. fs.rmSync(dir, { recursive: true, force: true });
  34. });
  35. const text = async (args: Record<string, unknown>): Promise<string> =>
  36. (await h.execute('codegraph_node', args)).content.map((c) => c.text).join('\n');
  37. it("a bare file (no symbol) returns the file's symbols + dependents", async () => {
  38. const out = await text({ file: 'a.ts' });
  39. expect(out).toContain('src/a.ts');
  40. expect(out).toContain('helper');
  41. expect(out).toContain('Widget');
  42. expect(out).toMatch(/depended on by 1 file/i);
  43. expect(out).toContain('src/b.ts'); // the dependent file (blast radius)
  44. });
  45. it('resolves by basename and returns verbatim bodies with includeCode', async () => {
  46. const out = await text({ file: 'a.ts', includeCode: true });
  47. expect(out).toContain('return x + 1'); // helper body
  48. expect(out).toContain('class Widget'); // class body, verbatim
  49. // It must NOT steer the agent back to Read — it is the Read replacement.
  50. expect(out.toLowerCase()).not.toContain('read `src/a.ts`');
  51. });
  52. it('still works as a normal symbol lookup (no regression)', async () => {
  53. const out = await text({ symbol: 'helper', includeCode: true });
  54. expect(out).toContain('helper');
  55. expect(out).toContain('return x + 1');
  56. });
  57. it('a miss returns a helpful message, not a crash', async () => {
  58. const out = await text({ file: 'does-not-exist.ts' });
  59. expect(out).toMatch(/no indexed file matches/i);
  60. });
  61. });