pr19-improvements.test.ts 24 KB

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