kernel-scaffold.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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('TS/JS family + Java + Python + Go route to the kernel by default; others stay wasm', () => {
  67. for (const lang of ['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', 'ruby'] as const) {
  68. expect(kernelRoutes(lang), lang).toBe(true);
  69. }
  70. expect(kernelRoutes('php')).toBe(false);
  71. expect(tryKernelExtract('src/a.php', '<?php function f() {}\n', 'php')).toBeNull();
  72. // CODEGRAPH_KERNEL_LANGS REPLACES the default set when present.
  73. process.env.CODEGRAPH_KERNEL_LANGS = 'tsx';
  74. expect(kernelRoutes('typescript')).toBe(false);
  75. expect(kernelRoutes('tsx')).toBe(true);
  76. });
  77. describe('with typescript routed (CODEGRAPH_KERNEL_LANGS)', () => {
  78. beforeEach(() => {
  79. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript';
  80. });
  81. it('decodes nodes, contains edges, and calls refs from the buffers', () => {
  82. const result = tryKernelExtract('src/utils.ts', FIXTURE, 'typescript');
  83. expect(result).not.toBeNull();
  84. const { nodes, edges, unresolvedReferences, errors } = result!;
  85. expect(errors).toEqual([]);
  86. const byKind = (kind: string) => nodes.filter((n) => n.kind === kind);
  87. expect(byKind('file')).toHaveLength(1);
  88. expect(byKind('class').map((n) => n.name)).toEqual(['MathHelper']);
  89. expect(byKind('method').map((n) => n.qualifiedName)).toEqual(['MathHelper::calculateTotal']);
  90. expect(byKind('function').map((n) => n.name)).toEqual(['helper']);
  91. const file = byKind('file')[0]!;
  92. expect(file.id).toBe('file:src/utils.ts');
  93. expect(file.qualifiedName).toBe('src/utils.ts');
  94. expect(file.endLine).toBe(FIXTURE.split('\n').length);
  95. expect(file.isExported).toBe(false);
  96. // Every node carries the decode-call constants.
  97. for (const n of nodes) {
  98. expect(n.filePath).toBe('src/utils.ts');
  99. expect(n.language).toBe('typescript');
  100. expect(n.updatedAt).toBeGreaterThan(0);
  101. }
  102. // contains: file→class, class→method, file→function.
  103. const contains = edges.filter((e) => e.kind === 'contains');
  104. const cls = byKind('class')[0]!;
  105. const method = byKind('method')[0]!;
  106. const fn = byKind('function')[0]!;
  107. expect(contains).toContainEqual({ source: file.id, target: cls.id, kind: 'contains' });
  108. expect(contains).toContainEqual({ source: cls.id, target: method.id, kind: 'contains' });
  109. expect(contains).toContainEqual({ source: file.id, target: fn.id, kind: 'contains' });
  110. // calls refs attach to the innermost enclosing symbol (method for the
  111. // in-body call, file node for the top-level call).
  112. const calls = unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  113. expect(calls.map((r) => [r.fromNodeId, r.referenceName])).toEqual([
  114. [method.id, 'helper'],
  115. [file.id, 'helper'],
  116. ]);
  117. for (const r of calls) {
  118. // No denormalized filePath/language at the extraction seam — the wasm
  119. // extractors leave them unset (the store fills them, `?? filePath`),
  120. // and the kernel matches that exactly (see decode.ts).
  121. expect(r.filePath).toBeUndefined();
  122. expect(r.language).toBeUndefined();
  123. expect(r.line).toBeGreaterThan(0);
  124. }
  125. });
  126. it('kernel node ids are byte-identical to generateNodeId', () => {
  127. const result = tryKernelExtract('src/utils.ts', FIXTURE, 'typescript')!;
  128. for (const n of result.nodes) {
  129. if (n.kind === 'file') continue;
  130. expect(n.id).toBe(generateNodeId('src/utils.ts', n.kind, n.name, n.startLine));
  131. }
  132. });
  133. it('CODEGRAPH_KERNEL=0 kill switch disables routing', () => {
  134. process.env.CODEGRAPH_KERNEL = '0';
  135. expect(kernelRoutes('typescript')).toBe(false);
  136. expect(tryKernelExtract('src/a.ts', FIXTURE, 'typescript')).toBeNull();
  137. });
  138. it('languages outside the route stay on the wasm path', () => {
  139. expect(kernelRoutes('javascript')).toBe(false);
  140. expect(tryKernelExtract('src/a.js', 'function f() {}', 'javascript')).toBeNull();
  141. });
  142. it('tsx routes with its own entry and returns a graph', () => {
  143. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript,tsx';
  144. const result = tryKernelExtract(
  145. 'src/App.tsx',
  146. 'export function App() { return render(); }\n',
  147. 'tsx'
  148. );
  149. expect(result).not.toBeNull();
  150. expect(result!.nodes.some((n) => n.kind === 'function' && n.name === 'App')).toBe(true);
  151. });
  152. });
  153. describe('extractFromSource seam', () => {
  154. beforeAll(async () => {
  155. await initGrammars();
  156. await loadGrammarsForLanguages(['typescript']);
  157. });
  158. it('kill switch routes through the wasm extractor unchanged', () => {
  159. process.env.CODEGRAPH_KERNEL = '0';
  160. const result = extractFromSource('src/a.ts', 'export const f = () => 1;\n', 'typescript');
  161. expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'f')).toBe(true);
  162. delete process.env.CODEGRAPH_KERNEL;
  163. // Default-routed path produces the same node (R2 parity).
  164. const viaKernel = extractFromSource('src/a.ts', 'export const f = () => 1;\n', 'typescript');
  165. expect(viaKernel.nodes.some((n) => n.kind === 'function' && n.name === 'f')).toBe(true);
  166. });
  167. it('routed language takes the kernel and falls back per file on kernel absence', () => {
  168. process.env.CODEGRAPH_KERNEL_LANGS = 'typescript';
  169. const viaKernel = extractFromSource('src/utils.ts', FIXTURE, 'typescript');
  170. expect(viaKernel.nodes.map((n) => n.kind)).toContain('method');
  171. // Point the loader at a nonexistent binary: routing is requested but the
  172. // kernel can't load, so the SAME call must fall back to wasm, not fail.
  173. process.env.CODEGRAPH_KERNEL_PATH = path.join(__dirname, 'nope', 'missing.node');
  174. process.env.CODEGRAPH_KERNEL = '0'; // and belt-and-braces the kill switch
  175. resetKernelForTests();
  176. const viaWasm = extractFromSource('src/utils.ts', FIXTURE, 'typescript');
  177. expect(viaWasm.nodes.some((n) => n.kind === 'class' && n.name === 'MathHelper')).toBe(true);
  178. });
  179. });
  180. });