pr19-improvements.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. /**
  2. * PR #19 Improvement Tests
  3. *
  4. * Tests for changes ported from PR #15 and #16:
  5. * - Lazy grammar loading
  6. * - Arrow function extraction (body traversal)
  7. * - Graph traversal 'both' direction fix
  8. * - Best-candidate resolution picking
  9. * - Schema v2 migration (filePath/language on unresolved_refs)
  10. * - Batch insert for unresolved refs
  11. * - SQLite performance pragmas
  12. * - MCP symbol disambiguation and output truncation
  13. * - CLI uninit command
  14. */
  15. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  16. import * as fs from 'fs';
  17. import * as path from 'path';
  18. import * as os from 'os';
  19. import { extractFromSource } from '../src/extraction';
  20. import {
  21. getParser,
  22. isLanguageSupported,
  23. getSupportedLanguages,
  24. clearParserCache,
  25. getUnavailableGrammarErrors,
  26. } from '../src/extraction/grammars';
  27. // Create a temporary directory for each test
  28. function createTempDir(): string {
  29. return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-pr19-test-'));
  30. }
  31. // Clean up temporary directory
  32. function cleanupTempDir(dir: string): void {
  33. if (fs.existsSync(dir)) {
  34. fs.rmSync(dir, { recursive: true, force: true });
  35. }
  36. }
  37. // Check if better-sqlite3 native bindings are available
  38. function hasSqliteBindings(): boolean {
  39. try {
  40. const Database = require('better-sqlite3');
  41. const db = new Database(':memory:');
  42. db.close();
  43. return true;
  44. } catch {
  45. return false;
  46. }
  47. }
  48. const HAS_SQLITE = hasSqliteBindings();
  49. // =============================================================================
  50. // Lazy Grammar Loading
  51. // =============================================================================
  52. describe('Lazy Grammar Loading', () => {
  53. afterEach(() => {
  54. clearParserCache();
  55. });
  56. it('should load grammars lazily on first use', () => {
  57. // Clear cache to force fresh load
  58. clearParserCache();
  59. // TypeScript should be loadable
  60. const parser = getParser('typescript');
  61. expect(parser).not.toBeNull();
  62. });
  63. it('should cache loaded grammars', () => {
  64. clearParserCache();
  65. const parser1 = getParser('typescript');
  66. const parser2 = getParser('typescript');
  67. // Same reference from cache
  68. expect(parser1).toBe(parser2);
  69. });
  70. it('should return null for unknown language', () => {
  71. const parser = getParser('unknown');
  72. expect(parser).toBeNull();
  73. });
  74. it('should handle unavailable grammars gracefully', () => {
  75. // 'unknown' is not a valid grammar, should not crash
  76. expect(isLanguageSupported('unknown')).toBe(false);
  77. });
  78. it('should report liquid as supported (custom extractor)', () => {
  79. expect(isLanguageSupported('liquid')).toBe(true);
  80. });
  81. it('should include liquid in supported languages', () => {
  82. const supported = getSupportedLanguages();
  83. expect(supported).toContain('liquid');
  84. });
  85. it('should return unavailable grammar errors as a record', () => {
  86. clearParserCache();
  87. const errors = getUnavailableGrammarErrors();
  88. // Should be a plain object (may or may not have entries depending on platform)
  89. expect(typeof errors).toBe('object');
  90. });
  91. it('should support multiple languages independently', () => {
  92. clearParserCache();
  93. // Load two different languages - one failing shouldn't affect the other
  94. const tsParser = getParser('typescript');
  95. const pyParser = getParser('python');
  96. expect(tsParser).not.toBeNull();
  97. expect(pyParser).not.toBeNull();
  98. expect(tsParser).not.toBe(pyParser);
  99. });
  100. it('should clear all caches on clearParserCache', () => {
  101. // Load a grammar
  102. getParser('typescript');
  103. // Clear
  104. clearParserCache();
  105. // Errors should be cleared too
  106. const errors = getUnavailableGrammarErrors();
  107. expect(Object.keys(errors)).toHaveLength(0);
  108. });
  109. });
  110. // =============================================================================
  111. // Arrow Function Extraction - Body Traversal
  112. // =============================================================================
  113. describe('Arrow Function Body Traversal', () => {
  114. it('should extract unresolved references from arrow function bodies', () => {
  115. const code = `
  116. export const useAuth = () => {
  117. const user = getUser();
  118. const token = generateToken(user);
  119. return { user, token };
  120. };
  121. `;
  122. const result = extractFromSource('hooks.ts', code);
  123. // The arrow function should be extracted
  124. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth');
  125. expect(funcNode).toBeDefined();
  126. // Calls inside the body should be captured as unresolved references
  127. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  128. const callNames = calls.map((c) => c.referenceName);
  129. expect(callNames).toContain('getUser');
  130. expect(callNames).toContain('generateToken');
  131. });
  132. it('should extract unresolved references from function expression bodies', () => {
  133. const code = `
  134. export const processData = function(input: string): string {
  135. const cleaned = sanitize(input);
  136. return transform(cleaned);
  137. };
  138. `;
  139. const result = extractFromSource('utils.ts', code);
  140. const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData');
  141. expect(funcNode).toBeDefined();
  142. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  143. const callNames = calls.map((c) => c.referenceName);
  144. expect(callNames).toContain('sanitize');
  145. expect(callNames).toContain('transform');
  146. });
  147. it('should not create duplicate nodes for arrow functions', () => {
  148. const code = `
  149. export const handler = () => {
  150. doSomething();
  151. };
  152. `;
  153. const result = extractFromSource('handler.ts', code);
  154. // Should be exactly 1 function node, 0 variable nodes for 'handler'
  155. const funcNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'function');
  156. const varNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'variable');
  157. expect(funcNodes).toHaveLength(1);
  158. expect(varNodes).toHaveLength(0);
  159. });
  160. it('should extract nested calls in arrow functions in JavaScript', () => {
  161. const code = `
  162. export const fetchData = async () => {
  163. const response = await fetchAPI('/data');
  164. return parseResponse(response);
  165. };
  166. `;
  167. const result = extractFromSource('api.js', code);
  168. const funcNode = result.nodes.find((n) => n.name === 'fetchData');
  169. expect(funcNode).toBeDefined();
  170. expect(funcNode?.kind).toBe('function');
  171. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  172. const callNames = calls.map((c) => c.referenceName);
  173. expect(callNames).toContain('fetchAPI');
  174. expect(callNames).toContain('parseResponse');
  175. });
  176. });
  177. // =============================================================================
  178. // Graph Traversal 'both' Direction Fix
  179. // (requires better-sqlite3 - will use CodeGraph integration)
  180. // =============================================================================
  181. describe('Graph Traversal Both Direction', () => {
  182. let testDir: string;
  183. beforeEach(() => {
  184. testDir = createTempDir();
  185. });
  186. afterEach(() => {
  187. cleanupTempDir(testDir);
  188. });
  189. it.skipIf(!HAS_SQLITE)('should traverse both directions from a node', async () => {
  190. const CodeGraph = (await import('../src/index')).default;
  191. const srcDir = path.join(testDir, 'src');
  192. fs.mkdirSync(srcDir, { recursive: true });
  193. // A -> B -> C (A calls B, B calls C)
  194. fs.writeFileSync(path.join(srcDir, 'a.ts'), `
  195. import { funcB } from './b';
  196. export function funcA(): void { funcB(); }
  197. `);
  198. fs.writeFileSync(path.join(srcDir, 'b.ts'), `
  199. import { funcC } from './c';
  200. export function funcB(): void { funcC(); }
  201. `);
  202. fs.writeFileSync(path.join(srcDir, 'c.ts'), `
  203. export function funcC(): void { console.log('c'); }
  204. `);
  205. const cg = CodeGraph.initSync(testDir, {
  206. config: { include: ['src/**/*.ts'], exclude: [] },
  207. });
  208. await cg.indexAll();
  209. cg.resolveReferences();
  210. const functions = cg.getNodesByKind('function');
  211. const funcB = functions.find((n) => n.name === 'funcB');
  212. if (!funcB) {
  213. cg.destroy();
  214. return;
  215. }
  216. // Traverse 'both' from B - should find A (incoming caller) and C (outgoing callee)
  217. const subgraph = cg.traverse(funcB.id, {
  218. maxDepth: 1,
  219. direction: 'both',
  220. });
  221. // B itself + at least one neighbor in each direction
  222. expect(subgraph.nodes.size).toBeGreaterThanOrEqual(2);
  223. expect(subgraph.nodes.has(funcB.id)).toBe(true);
  224. cg.destroy();
  225. });
  226. });
  227. // =============================================================================
  228. // Best-Candidate Resolution
  229. // =============================================================================
  230. describe('Best-Candidate Resolution', () => {
  231. it.skipIf(!HAS_SQLITE)('should be testable via the resolution module types', async () => {
  232. const { ReferenceResolver } = await import('../src/resolution');
  233. expect(typeof ReferenceResolver.prototype.resolveOne).toBe('function');
  234. });
  235. });
  236. // =============================================================================
  237. // Schema v2 Migration
  238. // =============================================================================
  239. describe('Schema v2 Migration', () => {
  240. it.skipIf(!HAS_SQLITE)('should have correct current schema version', async () => {
  241. const { CURRENT_SCHEMA_VERSION } = await import('../src/db/migrations');
  242. expect(CURRENT_SCHEMA_VERSION).toBe(2);
  243. });
  244. it.skipIf(!HAS_SQLITE)('should have migration for version 2', async () => {
  245. const { getPendingMigrations } = await import('../src/db/migrations');
  246. expect(typeof getPendingMigrations).toBe('function');
  247. });
  248. });
  249. // =============================================================================
  250. // Database Layer: Batch Insert, getAllNodes, Pragmas
  251. // =============================================================================
  252. describe('Database Layer Improvements', () => {
  253. let testDir: string;
  254. beforeEach(() => {
  255. testDir = createTempDir();
  256. });
  257. afterEach(() => {
  258. cleanupTempDir(testDir);
  259. });
  260. it.skipIf(!HAS_SQLITE)('should support batch insert of unresolved refs', async () => {
  261. const { DatabaseConnection } = await import('../src/db');
  262. const { QueryBuilder } = await import('../src/db/queries');
  263. const db = DatabaseConnection.initialize(testDir);
  264. const queries = new QueryBuilder(db.getDatabase());
  265. // Insert a node first (needed as foreign key)
  266. queries.insertNode({
  267. id: 'func:test:1',
  268. kind: 'function',
  269. name: 'testFunc',
  270. qualifiedName: 'test::testFunc',
  271. filePath: 'test.ts',
  272. language: 'typescript',
  273. startLine: 1,
  274. endLine: 5,
  275. startColumn: 0,
  276. endColumn: 1,
  277. updatedAt: Date.now(),
  278. });
  279. // Batch insert unresolved refs with filePath and language
  280. queries.insertUnresolvedRefsBatch([
  281. {
  282. fromNodeId: 'func:test:1',
  283. referenceName: 'helperA',
  284. referenceKind: 'calls',
  285. line: 2,
  286. column: 4,
  287. filePath: 'test.ts',
  288. language: 'typescript',
  289. },
  290. {
  291. fromNodeId: 'func:test:1',
  292. referenceName: 'helperB',
  293. referenceKind: 'calls',
  294. line: 3,
  295. column: 4,
  296. filePath: 'test.ts',
  297. language: 'typescript',
  298. },
  299. ]);
  300. const refs = queries.getUnresolvedReferences();
  301. expect(refs).toHaveLength(2);
  302. expect(refs.map((r) => r.referenceName).sort()).toEqual(['helperA', 'helperB']);
  303. // Verify filePath and language are persisted
  304. expect(refs[0]?.filePath).toBe('test.ts');
  305. expect(refs[0]?.language).toBe('typescript');
  306. db.close();
  307. });
  308. it.skipIf(!HAS_SQLITE)('should support getAllNodes', async () => {
  309. const { DatabaseConnection } = await import('../src/db');
  310. const { QueryBuilder } = await import('../src/db/queries');
  311. const db = DatabaseConnection.initialize(testDir);
  312. const queries = new QueryBuilder(db.getDatabase());
  313. // Insert some nodes
  314. for (let i = 0; i < 3; i++) {
  315. queries.insertNode({
  316. id: `func:test:${i}`,
  317. kind: 'function',
  318. name: `func${i}`,
  319. qualifiedName: `test::func${i}`,
  320. filePath: 'test.ts',
  321. language: 'typescript',
  322. startLine: i * 10 + 1,
  323. endLine: i * 10 + 5,
  324. startColumn: 0,
  325. endColumn: 1,
  326. updatedAt: Date.now(),
  327. });
  328. }
  329. const allNodes = queries.getAllNodes();
  330. expect(allNodes).toHaveLength(3);
  331. expect(allNodes.map((n) => n.name).sort()).toEqual(['func0', 'func1', 'func2']);
  332. db.close();
  333. });
  334. it.skipIf(!HAS_SQLITE)('should set performance pragmas on initialization', async () => {
  335. const { DatabaseConnection } = await import('../src/db');
  336. const db = DatabaseConnection.initialize(testDir);
  337. const rawDb = db.getDatabase();
  338. // Check pragmas were set
  339. const synchronous = rawDb.pragma('synchronous', { simple: true });
  340. expect(synchronous).toBe(1); // NORMAL = 1
  341. const cacheSize = rawDb.pragma('cache_size', { simple: true }) as number;
  342. expect(cacheSize).toBe(-64000);
  343. const tempStore = rawDb.pragma('temp_store', { simple: true });
  344. expect(tempStore).toBe(2); // MEMORY = 2
  345. const mmapSize = rawDb.pragma('mmap_size', { simple: true }) as number;
  346. expect(mmapSize).toBe(268435456); // 256 MB
  347. db.close();
  348. });
  349. it.skipIf(!HAS_SQLITE)('should handle empty batch insert gracefully', async () => {
  350. const { DatabaseConnection } = await import('../src/db');
  351. const { QueryBuilder } = await import('../src/db/queries');
  352. const db = DatabaseConnection.initialize(testDir);
  353. const queries = new QueryBuilder(db.getDatabase());
  354. // Should not throw on empty array
  355. expect(() => queries.insertUnresolvedRefsBatch([])).not.toThrow();
  356. db.close();
  357. });
  358. });
  359. // =============================================================================
  360. // Resolution Warm Caches
  361. // =============================================================================
  362. describe('Resolution Warm Caches', () => {
  363. let testDir: string;
  364. beforeEach(() => {
  365. testDir = createTempDir();
  366. });
  367. afterEach(() => {
  368. cleanupTempDir(testDir);
  369. });
  370. it.skipIf(!HAS_SQLITE)('should warm caches and use them for lookups', async () => {
  371. const CodeGraph = (await import('../src/index')).default;
  372. const srcDir = path.join(testDir, 'src');
  373. fs.mkdirSync(srcDir, { recursive: true });
  374. fs.writeFileSync(path.join(srcDir, 'a.ts'), `
  375. export function myFunc(): void {}
  376. export function otherFunc(): void { myFunc(); }
  377. `);
  378. const cg = CodeGraph.initSync(testDir, {
  379. config: { include: ['src/**/*.ts'], exclude: [] },
  380. });
  381. await cg.indexAll();
  382. // resolveReferences internally calls warmCaches
  383. const result = cg.resolveReferences();
  384. // Should complete without error
  385. expect(result.stats.total).toBeGreaterThanOrEqual(0);
  386. cg.destroy();
  387. });
  388. });
  389. // =============================================================================
  390. // MCP Tool Improvements
  391. // =============================================================================
  392. describe('MCP Tool Improvements', () => {
  393. it.skipIf(!HAS_SQLITE)('should export ToolHandler class', async () => {
  394. const { ToolHandler } = await import('../src/mcp/tools');
  395. expect(typeof ToolHandler).toBe('function');
  396. });
  397. it.skipIf(!HAS_SQLITE)('should have findSymbol and truncateOutput as private methods', async () => {
  398. const { ToolHandler } = await import('../src/mcp/tools');
  399. const proto = ToolHandler.prototype;
  400. expect(typeof (proto as any).findSymbol).toBe('function');
  401. expect(typeof (proto as any).truncateOutput).toBe('function');
  402. });
  403. it.skipIf(!HAS_SQLITE)('should truncate output exceeding MAX_OUTPUT_LENGTH', async () => {
  404. const { ToolHandler } = await import('../src/mcp/tools');
  405. // Access private method for testing
  406. const handler = Object.create(ToolHandler.prototype);
  407. const truncate = (handler as any).truncateOutput.bind(handler);
  408. // Short text should not be truncated
  409. const short = 'Hello world';
  410. expect(truncate(short)).toBe(short);
  411. // Long text should be truncated
  412. const long = 'x'.repeat(20000);
  413. const result = truncate(long);
  414. expect(result.length).toBeLessThan(long.length);
  415. expect(result).toContain('... (output truncated)');
  416. });
  417. it.skipIf(!HAS_SQLITE)('should truncate at a clean line boundary', async () => {
  418. const { ToolHandler } = await import('../src/mcp/tools');
  419. const handler = Object.create(ToolHandler.prototype);
  420. const truncate = (handler as any).truncateOutput.bind(handler);
  421. // Build text with newlines exceeding the limit
  422. const lines: string[] = [];
  423. for (let i = 0; i < 500; i++) {
  424. lines.push(`Line ${i}: ${'a'.repeat(50)}`);
  425. }
  426. const text = lines.join('\n');
  427. const result = truncate(text);
  428. // Should end with truncation notice after a newline boundary
  429. expect(result).toContain('... (output truncated)');
  430. // Should not cut mid-line (the char before truncation notice should be \n)
  431. const beforeTruncation = result.split('\n\n... (output truncated)')[0]!;
  432. expect(beforeTruncation.endsWith('\n') || !beforeTruncation.includes('\0')).toBe(true);
  433. });
  434. describe('findSymbol disambiguation', () => {
  435. it.skipIf(!HAS_SQLITE)('should prefer exact name matches', async () => {
  436. const { ToolHandler } = await import('../src/mcp/tools');
  437. const CodeGraph = (await import('../src/index')).default;
  438. const tmpDir = createTempDir();
  439. const srcDir = path.join(tmpDir, 'src');
  440. fs.mkdirSync(srcDir, { recursive: true });
  441. fs.writeFileSync(path.join(srcDir, 'a.ts'), `
  442. export function getValue(): number { return 1; }
  443. export function getValueFromCache(): number { return 2; }
  444. `);
  445. const cg = CodeGraph.initSync(tmpDir, {
  446. config: { include: ['src/**/*.ts'], exclude: [] },
  447. });
  448. await cg.indexAll();
  449. const handler = new ToolHandler(cg);
  450. const findSymbol = (handler as any).findSymbol.bind(handler);
  451. const match = findSymbol(cg, 'getValue');
  452. expect(match).not.toBeNull();
  453. expect(match.node.name).toBe('getValue');
  454. // Should not have a disambiguation note for single exact match
  455. expect(match.note).toBe('');
  456. handler.closeAll();
  457. cg.destroy();
  458. cleanupTempDir(tmpDir);
  459. });
  460. it.skipIf(!HAS_SQLITE)('should note when multiple symbols share the same name', async () => {
  461. const { ToolHandler } = await import('../src/mcp/tools');
  462. const CodeGraph = (await import('../src/index')).default;
  463. const tmpDir = createTempDir();
  464. const srcDir = path.join(tmpDir, 'src');
  465. fs.mkdirSync(srcDir, { recursive: true });
  466. // Two files with the same function name
  467. fs.writeFileSync(path.join(srcDir, 'a.ts'), `
  468. export function handle(): void {}
  469. `);
  470. fs.writeFileSync(path.join(srcDir, 'b.ts'), `
  471. export function handle(): void {}
  472. `);
  473. const cg = CodeGraph.initSync(tmpDir, {
  474. config: { include: ['src/**/*.ts'], exclude: [] },
  475. });
  476. await cg.indexAll();
  477. const handler = new ToolHandler(cg);
  478. const findSymbol = (handler as any).findSymbol.bind(handler);
  479. const match = findSymbol(cg, 'handle');
  480. expect(match).not.toBeNull();
  481. expect(match.node.name).toBe('handle');
  482. // Should have a disambiguation note
  483. expect(match.note).toContain('2 symbols named "handle"');
  484. handler.closeAll();
  485. cg.destroy();
  486. cleanupTempDir(tmpDir);
  487. });
  488. it.skipIf(!HAS_SQLITE)('should return null when symbol is not found', async () => {
  489. const { ToolHandler } = await import('../src/mcp/tools');
  490. const CodeGraph = (await import('../src/index')).default;
  491. const tmpDir = createTempDir();
  492. const srcDir = path.join(tmpDir, 'src');
  493. fs.mkdirSync(srcDir, { recursive: true });
  494. fs.writeFileSync(path.join(srcDir, 'a.ts'), `export function foo(): void {}`);
  495. const cg = CodeGraph.initSync(tmpDir, {
  496. config: { include: ['src/**/*.ts'], exclude: [] },
  497. });
  498. await cg.indexAll();
  499. const handler = new ToolHandler(cg);
  500. const findSymbol = (handler as any).findSymbol.bind(handler);
  501. const match = findSymbol(cg, 'nonExistentSymbol');
  502. expect(match).toBeNull();
  503. handler.closeAll();
  504. cg.destroy();
  505. cleanupTempDir(tmpDir);
  506. });
  507. });
  508. });
  509. // =============================================================================
  510. // CLI uninit Command
  511. // =============================================================================
  512. describe('CLI uninit', () => {
  513. let testDir: string;
  514. beforeEach(() => {
  515. testDir = createTempDir();
  516. });
  517. afterEach(() => {
  518. cleanupTempDir(testDir);
  519. });
  520. it.skipIf(!HAS_SQLITE)('should uninitialize a project via CodeGraph.uninitialize()', async () => {
  521. const CodeGraph = (await import('../src/index')).default;
  522. // Initialize
  523. const cg = CodeGraph.initSync(testDir);
  524. expect(CodeGraph.isInitialized(testDir)).toBe(true);
  525. // Uninitialize
  526. cg.uninitialize();
  527. // .codegraph directory should be removed
  528. expect(CodeGraph.isInitialized(testDir)).toBe(false);
  529. });
  530. });
  531. // =============================================================================
  532. // Tree-sitter Version Pinning
  533. // =============================================================================
  534. describe('Tree-sitter Version Pinning', () => {
  535. it('should have exact versions (no caret) in package.json', () => {
  536. const pkgPath = path.join(__dirname, '..', 'package.json');
  537. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
  538. const treeSitterDeps = Object.entries(pkg.dependencies as Record<string, string>)
  539. .filter(([name]) => name.startsWith('tree-sitter') || name.includes('tree-sitter'));
  540. for (const [name, version] of treeSitterDeps) {
  541. // Skip github: references
  542. if (version.startsWith('github:')) continue;
  543. expect(version, `${name} should not use caret range`).not.toMatch(/^\^/);
  544. }
  545. });
  546. it('should have tree-sitter override pinned', () => {
  547. const pkgPath = path.join(__dirname, '..', 'package.json');
  548. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
  549. expect(pkg.overrides).toBeDefined();
  550. expect(pkg.overrides['tree-sitter']).toBeDefined();
  551. expect(pkg.overrides['tree-sitter']).not.toMatch(/^\^/);
  552. });
  553. });
  554. // =============================================================================
  555. // Embedder Float32Array Fix
  556. // =============================================================================
  557. describe('Float32Array Fix', () => {
  558. it('should correctly convert typed arrays (regression check)', () => {
  559. // Simulates the fix: Float32Array.from(Array.from(arr)) vs new Float32Array(arr.length)
  560. const source = new Float64Array([1.5, 2.5, 3.5, 4.5]);
  561. // The OLD buggy approach:
  562. const buggy = new Float32Array(source.length);
  563. // buggy is all zeros!
  564. expect(buggy[0]).toBe(0);
  565. expect(buggy[1]).toBe(0);
  566. // The NEW fixed approach:
  567. const fixed = Float32Array.from(Array.from(source));
  568. expect(fixed[0]).toBeCloseTo(1.5);
  569. expect(fixed[1]).toBeCloseTo(2.5);
  570. expect(fixed[2]).toBeCloseTo(3.5);
  571. expect(fixed[3]).toBeCloseTo(4.5);
  572. });
  573. });