kernel-scaffold.test.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * Native-kernel scaffold tests (R1, docs/design/rust-kernel-migration-plan.md).
  3. *
  4. * Covers the wire contract, decoder, routing policy, kill switch, and
  5. * per-file fallback. These are SCAFFOLD tests — behavioral parity with the
  6. * wasm extractors is R3's equivalence gate, not asserted here.
  7. *
  8. * The kernel binary is optional: without a staged .node
  9. * (scripts/build-kernel.sh) the suite skips. CI that builds the kernel sets
  10. * CODEGRAPH_KERNEL_EXPECT=1, which turns "missing binary" into a FAILURE so
  11. * the gate can't silently pass by not building the kernel.
  12. */
  13. import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
  14. import * as fs from 'fs';
  15. import * as path from 'path';
  16. import { NODE_KINDS, EDGE_KINDS } from '../src/types';
  17. import { generateNodeId } from '../src/extraction/tree-sitter-helpers';
  18. import { getKernel, tryKernelExtract, kernelRoutes, resetKernelForTests } from '../src/extraction/kernel';
  19. import { extractFromSource } from '../src/extraction';
  20. import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
  21. const KERNEL_PATH = path.join(
  22. __dirname,
  23. '..',
  24. 'codegraph-kernel',
  25. 'prebuilds',
  26. `${process.platform}-${process.arch}`,
  27. 'codegraph-kernel.node'
  28. );
  29. const kernelBuilt = fs.existsSync(KERNEL_PATH);
  30. const expectKernel = process.env.CODEGRAPH_KERNEL_EXPECT === '1';
  31. const FIXTURE = [
  32. 'export class MathHelper {',
  33. ' calculateTotal(a: number): number { return helper(a); }',
  34. '}',
  35. 'function helper(x: number): number { return x * 2; }',
  36. 'helper(3);',
  37. '',
  38. ].join('\n');
  39. const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS', 'CODEGRAPH_KERNEL_PATH'] as const;
  40. let savedEnv: Record<string, string | undefined>;
  41. beforeEach(() => {
  42. savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
  43. for (const k of ENV_KEYS) delete process.env[k];
  44. resetKernelForTests();
  45. });
  46. afterEach(() => {
  47. for (const k of ENV_KEYS) {
  48. if (savedEnv[k] === undefined) delete process.env[k];
  49. else process.env[k] = savedEnv[k];
  50. }
  51. resetKernelForTests();
  52. });
  53. it.runIf(expectKernel)('kernel binary must exist when CODEGRAPH_KERNEL_EXPECT=1', () => {
  54. expect(kernelBuilt, `expected kernel at ${KERNEL_PATH} — run scripts/build-kernel.sh`).toBe(true);
  55. });
  56. describe.skipIf(!kernelBuilt)('kernel scaffold', () => {
  57. it('loads and its kind tables match src/types.ts exactly', () => {
  58. const kernel = getKernel();
  59. expect(kernel).not.toBeNull();
  60. const info = kernel!.contractInfo();
  61. expect(info.nodeKinds).toEqual([...NODE_KINDS]);
  62. expect(info.edgeKinds).toEqual([...EDGE_KINDS]);
  63. expect(info.languages).toContain('typescript');
  64. expect(info.languages).toContain('javascript');
  65. });
  66. it('no language routes to the kernel by default (R1: wasm path unchanged)', () => {
  67. expect(kernelRoutes('typescript')).toBe(false);
  68. expect(tryKernelExtract('src/a.ts', 'function f() {}', 'typescript')).toBeNull();
  69. });
  70. describe('with typescript routed (CODEGRAPH_KERNEL_LANGS)', () => {
  71. beforeEach(() => {
  72. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript';
  73. });
  74. it('decodes nodes, contains edges, and calls refs from the buffers', () => {
  75. const result = tryKernelExtract('src/utils.ts', FIXTURE, 'typescript');
  76. expect(result).not.toBeNull();
  77. const { nodes, edges, unresolvedReferences, errors } = result!;
  78. expect(errors).toEqual([]);
  79. const byKind = (kind: string) => nodes.filter((n) => n.kind === kind);
  80. expect(byKind('file')).toHaveLength(1);
  81. expect(byKind('class').map((n) => n.name)).toEqual(['MathHelper']);
  82. expect(byKind('method').map((n) => n.qualifiedName)).toEqual(['MathHelper::calculateTotal']);
  83. expect(byKind('function').map((n) => n.name)).toEqual(['helper']);
  84. const file = byKind('file')[0]!;
  85. expect(file.id).toBe('file:src/utils.ts');
  86. expect(file.qualifiedName).toBe('src/utils.ts');
  87. expect(file.endLine).toBe(FIXTURE.split('\n').length);
  88. expect(file.isExported).toBe(false);
  89. // Every node carries the decode-call constants.
  90. for (const n of nodes) {
  91. expect(n.filePath).toBe('src/utils.ts');
  92. expect(n.language).toBe('typescript');
  93. expect(n.updatedAt).toBeGreaterThan(0);
  94. }
  95. // contains: file→class, class→method, file→function.
  96. const contains = edges.filter((e) => e.kind === 'contains');
  97. const cls = byKind('class')[0]!;
  98. const method = byKind('method')[0]!;
  99. const fn = byKind('function')[0]!;
  100. expect(contains).toContainEqual({ source: file.id, target: cls.id, kind: 'contains' });
  101. expect(contains).toContainEqual({ source: cls.id, target: method.id, kind: 'contains' });
  102. expect(contains).toContainEqual({ source: file.id, target: fn.id, kind: 'contains' });
  103. // calls refs attach to the innermost enclosing symbol (method for the
  104. // in-body call, file node for the top-level call).
  105. const calls = unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  106. expect(calls.map((r) => [r.fromNodeId, r.referenceName])).toEqual([
  107. [method.id, 'helper'],
  108. [file.id, 'helper'],
  109. ]);
  110. for (const r of calls) {
  111. // No denormalized filePath/language at the extraction seam — the wasm
  112. // extractors leave them unset (the store fills them, `?? filePath`),
  113. // and the kernel matches that exactly (see decode.ts).
  114. expect(r.filePath).toBeUndefined();
  115. expect(r.language).toBeUndefined();
  116. expect(r.line).toBeGreaterThan(0);
  117. }
  118. });
  119. it('kernel node ids are byte-identical to generateNodeId', () => {
  120. const result = tryKernelExtract('src/utils.ts', FIXTURE, 'typescript')!;
  121. for (const n of result.nodes) {
  122. if (n.kind === 'file') continue;
  123. expect(n.id).toBe(generateNodeId('src/utils.ts', n.kind, n.name, n.startLine));
  124. }
  125. });
  126. it('CODEGRAPH_KERNEL=0 kill switch disables routing', () => {
  127. process.env.CODEGRAPH_KERNEL = '0';
  128. expect(kernelRoutes('typescript')).toBe(false);
  129. expect(tryKernelExtract('src/a.ts', FIXTURE, 'typescript')).toBeNull();
  130. });
  131. it('languages outside the route stay on the wasm path', () => {
  132. expect(kernelRoutes('javascript')).toBe(false);
  133. expect(tryKernelExtract('src/a.js', 'function f() {}', 'javascript')).toBeNull();
  134. });
  135. it('tsx routes with its own entry and returns a graph', () => {
  136. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript,tsx';
  137. const result = tryKernelExtract(
  138. 'src/App.tsx',
  139. 'export function App() { return render(); }\n',
  140. 'tsx'
  141. );
  142. expect(result).not.toBeNull();
  143. expect(result!.nodes.some((n) => n.kind === 'function' && n.name === 'App')).toBe(true);
  144. });
  145. });
  146. describe('extractFromSource seam', () => {
  147. beforeAll(async () => {
  148. await initGrammars();
  149. await loadGrammarsForLanguages(['typescript']);
  150. });
  151. it('unrouted language flows through the wasm extractor unchanged', () => {
  152. // `const f = () => 1` yields a function node on the wasm path; the seed
  153. // kernel query deliberately doesn't extract it — so its presence proves
  154. // which path ran.
  155. const result = extractFromSource('src/a.ts', 'export const f = () => 1;\n', 'typescript');
  156. expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'f')).toBe(true);
  157. });
  158. it('routed language takes the kernel and falls back per file on kernel absence', () => {
  159. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript';
  160. const viaKernel = extractFromSource('src/utils.ts', FIXTURE, 'typescript');
  161. expect(viaKernel.nodes.map((n) => n.kind)).toContain('method');
  162. // Point the loader at a nonexistent binary: routing is requested but the
  163. // kernel can't load, so the SAME call must fall back to wasm, not fail.
  164. process.env.CODEGRAPH_KERNEL_PATH = path.join(__dirname, 'nope', 'missing.node');
  165. process.env.CODEGRAPH_KERNEL = '0'; // and belt-and-braces the kill switch
  166. resetKernelForTests();
  167. const viaWasm = extractFromSource('src/utils.ts', FIXTURE, 'typescript');
  168. expect(viaWasm.nodes.some((n) => n.kind === 'class' && n.name === 'MathHelper')).toBe(true);
  169. });
  170. });
  171. });