1
0

node-file-view.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. /**
  2. * codegraph_node FILE READ mode: a `file` with no `symbol` reads that file like
  3. * the Read tool — current source with `<n>\t<line>` numbering (byte-for-byte
  4. * Read's shape), narrowable with offset/limit — plus a one-line blast-radius
  5. * header. `symbolsOnly` returns the structural map instead. Config/data files
  6. * are summarized by key, never dumped (#383).
  7. */
  8. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  9. import * as fs from 'fs';
  10. import * as path from 'path';
  11. import * as os from 'os';
  12. import CodeGraph from '../src/index';
  13. import { ToolHandler } from '../src/mcp/tools';
  14. describe('codegraph_node file-view (Read replacement)', () => {
  15. let dir: string;
  16. let cg: CodeGraph;
  17. let h: ToolHandler;
  18. beforeEach(async () => {
  19. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fileview-'));
  20. fs.mkdirSync(path.join(dir, 'src'));
  21. fs.writeFileSync(
  22. path.join(dir, 'src', 'a.ts'),
  23. 'export function helper(x: number) {\n return x + 1;\n}\nexport class Widget {\n build() { return helper(1); }\n}\n',
  24. );
  25. fs.writeFileSync(
  26. path.join(dir, 'src', 'b.ts'),
  27. "import { helper } from './a';\n\n// a comment between symbols\nconst SETTING = 7;\nexport function useHelper() { return helper(2) + SETTING; }\n",
  28. );
  29. // A config/data file (#383): its values may be secrets and must never be
  30. // dumped verbatim by the file-view.
  31. fs.writeFileSync(
  32. path.join(dir, 'src', 'application.properties'),
  33. 'spring.datasource.password=SUPERSECRET123\nserver.port=8080\n',
  34. );
  35. // A large file: exceeds the file-view line budget, so it must be windowed
  36. // honestly (not silently truncated).
  37. fs.writeFileSync(
  38. path.join(dir, 'src', 'big.ts'),
  39. 'export function big() {\n' +
  40. Array.from({ length: 2000 }, (_, i) => ` const v${i} = ${i};`).join('\n') +
  41. '\n return 0;\n}\n',
  42. );
  43. cg = CodeGraph.initSync(dir, { config: { include: ['**/*.ts', '**/*.properties'], exclude: [] } });
  44. await cg.indexAll();
  45. h = new ToolHandler(cg);
  46. });
  47. afterEach(() => {
  48. if (cg) cg.close();
  49. fs.rmSync(dir, { recursive: true, force: true });
  50. });
  51. const text = async (args: Record<string, unknown>): Promise<string> =>
  52. (await h.execute('codegraph_node', args)).content.map((c) => c.text).join('\n');
  53. it('reads a whole file like Read by default — `<n>\\t<line>` lines (no pad), imports + gaps included', async () => {
  54. const out = await text({ file: 'b.ts' }); // no includeCode needed — content is the default
  55. // Byte-for-byte Read shape: line 1 is "1<TAB>import …", NOT space-padded.
  56. expect(out).toMatch(/^1\timport \{ helper \} from '\.\/a';$/m);
  57. expect(out).toContain('// a comment between symbols'); // inter-symbol gap (Read has it; old reconstruction dropped it)
  58. expect(out).toContain('const SETTING = 7'); // top-level statement
  59. expect(out).toContain('useHelper'); // the symbol body too
  60. expect(out).not.toContain('```'); // Read has no code fence; neither do we
  61. });
  62. it('leads with a one-line blast-radius header (the value-add over Read)', async () => {
  63. const out = await text({ file: 'a.ts' });
  64. expect(out).toMatch(/used by 1 file: src\/b\.ts/); // a.ts is imported by b.ts
  65. expect(out).toContain('return x + 1'); // still returns the source
  66. });
  67. it('offset/limit narrow the window exactly like Read', async () => {
  68. const out = await text({ file: 'big.ts', offset: 1000, limit: 3 });
  69. // Window starts at the requested line, numbered exactly: "1000<TAB> const v998 = 998;"
  70. expect(out).toMatch(/^1000\t {2}const v998 = 998;$/m);
  71. expect(out).not.toMatch(/^1\t/m); // line 1 is NOT shown
  72. expect(out).toMatch(/lines 1000[–-]1002 of \d+/); // honest pagination note
  73. });
  74. it('an offset past EOF is reported, not a crash', async () => {
  75. const out = await text({ file: 'a.ts', offset: 9999 });
  76. expect(out).toMatch(/past the end/i);
  77. });
  78. it('paginates a large file honestly by default — "lines 1–N of TOTAL", never a silent truncate', async () => {
  79. const out = await text({ file: 'big.ts' });
  80. expect(out).toMatch(/lines 1[–-]\d+ of \d+/); // explicit window note
  81. expect(out).not.toContain('(output truncated)'); // not the generic 15k chop
  82. expect(out).toMatch(/^1\texport function big/m); // the head of the window is real source
  83. });
  84. it('does NOT dump a config/data file (yaml/properties) — #383 secret safety', async () => {
  85. const out = await text({ file: 'application.properties' });
  86. expect(out).not.toContain('SUPERSECRET123'); // the value never reaches the agent
  87. expect(out.toLowerCase()).toMatch(/config|values withheld/);
  88. });
  89. it('symbolsOnly returns the structural map, not the source', async () => {
  90. const out = await text({ file: 'a.ts', symbolsOnly: true });
  91. expect(out).toContain('### Symbols');
  92. expect(out).toContain('helper');
  93. expect(out).toContain('Widget');
  94. expect(out).not.toContain('return x + 1'); // bodies are NOT included in the map
  95. });
  96. it('still works as a normal symbol lookup (no regression)', async () => {
  97. const out = await text({ symbol: 'helper', includeCode: true });
  98. expect(out).toContain('helper');
  99. expect(out).toContain('return x + 1');
  100. });
  101. it('a miss returns a helpful message, not a crash', async () => {
  102. const out = await text({ file: 'does-not-exist.ts' });
  103. expect(out).toMatch(/no indexed file matches/i);
  104. });
  105. });