1
0

explore-synth-constant-endpoints.test.ts 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. /**
  2. * Regression: codegraph_explore must SURFACE a synthesized edge whose endpoints are
  3. * `constant` nodes (RTK thunk→thunk), on a SMALL repo.
  4. *
  5. * `buildFlowFromNamedSymbols` historically filtered its "named" set to CALLABLE kinds
  6. * (method/function/component/constructor), excluding `constant`. RTK thunks are
  7. * `export const X = createAsyncThunk(...)`, so a thunk→thunk hop is constant→constant —
  8. * it never entered the flow scan and surfaced nowhere on the Flow path. The kind-agnostic
  9. * "### Relationships" section would have caught it, but that is disabled below 500 files.
  10. * Net: on a small RTK app the synthesized edge existed in the graph yet was invisible to
  11. * the agent. The fix feeds a `dynNamed` set (named non-callable endpoints that participate
  12. * in a heuristic edge) to the tier-independent "## Dynamic-dispatch links" scan. This test
  13. * pins it on a deliberately tiny (<150-file) fixture so the Relationships gate is OFF and
  14. * the dynamic-dispatch-links path is the ONLY thing that can surface the hop.
  15. */
  16. import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
  17. import * as fs from 'node:fs';
  18. import * as path from 'node:path';
  19. import * as os from 'node:os';
  20. import CodeGraph from '../src/index';
  21. import { ToolHandler } from '../src/mcp/tools';
  22. // Assertions read RAW codegraph_explore output; managed offload would replace it. Disable
  23. // it for this file so the suite is hermetic regardless of dev-machine config, then restore.
  24. let _prevOffloadDisable: string | undefined;
  25. beforeAll(() => { _prevOffloadDisable = process.env.CODEGRAPH_OFFLOAD_DISABLE; process.env.CODEGRAPH_OFFLOAD_DISABLE = '1'; });
  26. afterAll(() => {
  27. if (_prevOffloadDisable === undefined) delete process.env.CODEGRAPH_OFFLOAD_DISABLE;
  28. else process.env.CODEGRAPH_OFFLOAD_DISABLE = _prevOffloadDisable;
  29. });
  30. describe('codegraph_explore — synthesized constant→constant edges surface on small repos', () => {
  31. let dir: string;
  32. let cg: CodeGraph;
  33. let handler: ToolHandler;
  34. afterEach(() => {
  35. if (cg) cg.destroy();
  36. if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  37. });
  38. it('surfaces an RTK thunk→thunk hop (both `constant`) in the Dynamic-dispatch links section', async () => {
  39. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'explore-thunk-surface-'));
  40. fs.writeFileSync(
  41. path.join(dir, 'package.json'),
  42. JSON.stringify({ name: 'app', dependencies: { '@reduxjs/toolkit': '^2' } })
  43. );
  44. fs.writeFileSync(
  45. path.join(dir, 'thunks.ts'),
  46. `import { createAsyncThunk } from '@reduxjs/toolkit';
  47. export const deepThunk = createAsyncThunk('app/deep', async (n: number) => n * 2);
  48. export const innerThunk = createAsyncThunk('app/inner', async (n: number, { dispatch }) => {
  49. return dispatch(deepThunk(n));
  50. });
  51. export const outerThunk = createAsyncThunk('app/outer', async (n: number, { dispatch }) => {
  52. await dispatch(innerThunk(n));
  53. });
  54. `
  55. );
  56. cg = CodeGraph.initSync(dir, { config: { include: ['**/*.ts'], exclude: [] } });
  57. await cg.indexAll();
  58. // Precondition: the endpoints really are `constant` nodes — the exact kind the old
  59. // CALLABLE-only flow scan dropped (if extraction ever classed them as functions the
  60. // test would pass vacuously, so assert the case we actually fixed).
  61. const db = (cg as any).db.db;
  62. const outerKind = db.prepare(`SELECT kind FROM nodes WHERE name = 'outerThunk' LIMIT 1`).get()?.kind;
  63. expect(outerKind).toBe('constant');
  64. handler = new ToolHandler(cg);
  65. const res = await handler.execute('codegraph_explore', { query: 'outerThunk innerThunk' });
  66. const text = res.content[0].text as string;
  67. // The synthesized hop now surfaces (was invisible: both endpoints `constant` AND the
  68. // small-repo Relationships section is off).
  69. expect(text).toContain('## Dynamic-dispatch links among your symbols');
  70. expect(text).toMatch(/outerThunk\s+→\s+innerThunk/);
  71. // It reads as a dynamic-dispatch bridge with its wiring site, not a bare `calls`.
  72. expect(text).toMatch(/dynamic: redux thunk @/);
  73. expect(text).not.toMatch(/outerThunk\s+→\s+innerThunk\s+\[calls\]/);
  74. });
  75. });