Files
codegraph/__tests__/pr19-improvements.test.ts
T
ac52fd76c0 Self-contained distribution: bundle Node + node:sqlite, drop better-sqlite3/wasm (closes #238) (#282)
* fix(db): eliminate concurrent-read "database is locked"; add node:sqlite backend (#238)

WAL + busy_timeout were already enabled, so the issue's suggested fix was a
no-op. The real causes, addressed here:

- busy_timeout is now set first (before journal_mode) and lowered 120s -> 5s,
  so open-time pragmas wait out a lock instead of hanging for two minutes.
- getCodeGraph no longer opens a second connection to the default project when
  a tool passes its own projectPath (the in-process lock amplifier).
- The wasm fallback (no WAL) gets a bounded read-retry on SQLITE_BUSY.
- New: node:sqlite backend, preferred over wasm, so installs whose native
  better-sqlite3 build fails land on a real-WAL backend instead of no-WAL wasm.
- codegraph status / codegraph_status now report the effective journal mode, so
  a lock report is triageable (wal vs delete).
- CLI hard-blocks Node < 20 to actually enforce the engines floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(db)!: node:sqlite is the sole backend; drop better-sqlite3 + wasm

Now that distribution will bundle a Node 24 runtime, node:sqlite (real SQLite
with WAL + FTS5) is always available. Collapse the three-backend adapter to
node:sqlite only and remove the machinery the other two needed:

- Remove better-sqlite3 (optionalDependency) and node-sqlite3-wasm (dependency).
- Remove WasmDatabaseAdapter, the named->positional param translation, the
  SQLITE_BUSY read-retry, the wasm fallback banner, the backend env override,
  and the native/node-sqlite/wasm selection chain.
- createDatabase now opens node:sqlite directly, with a clear error pointing at
  the bundled release / Node 22.5+ when the module is absent.
- NodeSqliteAdapter.close() is idempotent and pragma() supports { simple }, to
  match the better-sqlite3 behavior callers relied on.
- status (CLI + MCP) reports the single node:sqlite backend; journal-mode
  diagnostics and the getCodeGraph single-connection fix are retained.
- Tests repointed off better-sqlite3 onto node:sqlite.

Net -1044 lines. Running from source now requires Node 22.5+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): self-contained bundle prototype (vendored Node + install channels)

Phase 3 of the node:sqlite migration: ship a vendored Node runtime so CodeGraph
runs with no system Node and no native build (node:sqlite is built in).

- scripts/build-bundle.sh: build a per-platform archive (official Node + dist +
  prod deps + launcher). Same recipe per platform; pins Node v24.16.0.
- install.sh: curl|sh installer (no Node required) — detects os/arch, pulls the
  archive from Releases, symlinks onto PATH; re-run to upgrade, --uninstall to
  remove. The VPS/SSH path.
- scripts/npm-shim.js: thin launcher for the npm channel — resolves the
  per-platform optionalDependency bundle and execs it, so `npm i -g` keeps
  working and the real work runs on the bundled Node regardless of the user's.
- BUNDLING.md: distribution design + release-pipeline TODO (CI matrix, platform
  packages, code signing, brew, retiring the Node-version gate).

Validated end-to-end: darwin-arm64 and linux-x64 bundles both run init + index +
status (Backend: node:sqlite, Journal: wal) + FTS query with NO system Node —
linux-x64 verified in a clean ubuntu:24.04 amd64 container. Release archives are
gitignored; CI will produce and upload them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): add Windows PowerShell installer (install.ps1)

The `irm … | iex` one-liner for Windows, mirroring install.sh: detect arch,
pull the matching bundle from Releases, extract to %LOCALAPPDATA%\codegraph,
add it to user PATH. Re-run to upgrade. (Windows bundle production in
build-bundle.sh is still TODO.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): release workflow + npm packaging; README/CHANGELOG for bundled distro

- .github/workflows/release.yml: manually-triggered (workflow_dispatch) release
  matrix. Builds a self-contained bundle per platform on its own runner
  (darwin-arm64/x64, linux-x64/arm64), publishes a GitHub Release with all
  archives, and publishes the npm thin-installer (shim + per-platform packages).
  Windows targets are TODO (build-bundle.sh is unix-only).
- scripts/pack-npm.sh: assemble the npm packages from built bundles — per-platform
  packages tagged os/cpu + the main shim package with them as optionalDependencies
  (esbuild pattern). Proven locally: npm-install the tarballs, run via the shim,
  resolves the bundle and runs on the bundled Node 24 (node:sqlite / WAL).
- README: install section now leads with the no-Node one-liners (curl|sh, irm|iex)
  then npm/npx; "bundled · none required" badge.
- CHANGELOG: standout headline for the self-contained release, plus Added/Changed/
  Removed for the install channels, node:sqlite backend, and dropped deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): Windows bundles + single-trigger release workflow

- build-bundle.sh: add win32-x64 / win32-arm64 targets — download Node's Windows
  zip, bundle node.exe + a .cmd launcher, output a .zip. Verified structurally
  (PE32+ node.exe, CRLF .cmd, portable node_modules). Since there are no native
  addons, any target builds on any OS, so the whole matrix builds on one runner.
- pack-npm.sh: handle .zip bundles and win32 packages (os: win32, node.exe).
- release.yml: simplified to your spec — manual trigger reads the version from
  package.json, builds all platform bundles, creates the GitHub Release with notes
  pulled from CHANGELOG.md, and publishes the npm shim + platform packages.
- BUNDLING.md: Windows + build-anywhere notes; release pipeline documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:48:04 -05:00

720 lines
23 KiB
TypeScript

/**
* PR #19 Improvement Tests
*
* Tests for changes ported from PR #15 and #16:
* - Lazy grammar loading
* - Arrow function extraction (body traversal)
* - Graph traversal 'both' direction fix
* - Best-candidate resolution picking
* - Schema v2 migration (filePath/language on unresolved_refs)
* - Batch insert for unresolved refs
* - SQLite performance pragmas
* - MCP symbol disambiguation and output truncation
* - CLI uninit command
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { extractFromSource } from '../src/extraction';
import {
getParser,
isLanguageSupported,
getSupportedLanguages,
clearParserCache,
getUnavailableGrammarErrors,
initGrammars,
loadAllGrammars,
} from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});
// Create a temporary directory for each test
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-pr19-test-'));
}
// Clean up temporary directory
function cleanupTempDir(dir: string): void {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
// Check if the node:sqlite backend is available (Node >= 22.5)
function hasSqliteBindings(): boolean {
try {
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
db.close();
return true;
} catch {
return false;
}
}
const HAS_SQLITE = hasSqliteBindings();
// =============================================================================
// Lazy Grammar Loading
// =============================================================================
describe('Lazy Grammar Loading', () => {
afterEach(() => {
clearParserCache();
});
it('should load grammars lazily on first use', () => {
// Clear cache to force fresh load
clearParserCache();
// TypeScript should be loadable
const parser = getParser('typescript');
expect(parser).not.toBeNull();
});
it('should cache loaded grammars', () => {
clearParserCache();
const parser1 = getParser('typescript');
const parser2 = getParser('typescript');
// Same reference from cache
expect(parser1).toBe(parser2);
});
it('should return null for unknown language', () => {
const parser = getParser('unknown');
expect(parser).toBeNull();
});
it('should handle unavailable grammars gracefully', () => {
// 'unknown' is not a valid grammar, should not crash
expect(isLanguageSupported('unknown')).toBe(false);
});
it('should report liquid as supported (custom extractor)', () => {
expect(isLanguageSupported('liquid')).toBe(true);
});
it('should include liquid in supported languages', () => {
const supported = getSupportedLanguages();
expect(supported).toContain('liquid');
});
it('should return unavailable grammar errors as a record', () => {
clearParserCache();
const errors = getUnavailableGrammarErrors();
// Should be a plain object (may or may not have entries depending on platform)
expect(typeof errors).toBe('object');
});
it('should support multiple languages independently', () => {
clearParserCache();
// Load two different languages - one failing shouldn't affect the other
const tsParser = getParser('typescript');
const pyParser = getParser('python');
expect(tsParser).not.toBeNull();
expect(pyParser).not.toBeNull();
expect(tsParser).not.toBe(pyParser);
});
it('should clear all caches on clearParserCache', () => {
// Load a grammar
getParser('typescript');
// Clear
clearParserCache();
// Errors should be cleared too
const errors = getUnavailableGrammarErrors();
expect(Object.keys(errors)).toHaveLength(0);
});
});
// =============================================================================
// Arrow Function Extraction - Body Traversal
// =============================================================================
describe('Arrow Function Body Traversal', () => {
it('should extract unresolved references from arrow function bodies', () => {
const code = `
export const useAuth = () => {
const user = getUser();
const token = generateToken(user);
return { user, token };
};
`;
const result = extractFromSource('hooks.ts', code);
// The arrow function should be extracted
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth');
expect(funcNode).toBeDefined();
// Calls inside the body should be captured as unresolved references
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
const callNames = calls.map((c) => c.referenceName);
expect(callNames).toContain('getUser');
expect(callNames).toContain('generateToken');
});
it('should extract unresolved references from function expression bodies', () => {
const code = `
export const processData = function(input: string): string {
const cleaned = sanitize(input);
return transform(cleaned);
};
`;
const result = extractFromSource('utils.ts', code);
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData');
expect(funcNode).toBeDefined();
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
const callNames = calls.map((c) => c.referenceName);
expect(callNames).toContain('sanitize');
expect(callNames).toContain('transform');
});
it('should not create duplicate nodes for arrow functions', () => {
const code = `
export const handler = () => {
doSomething();
};
`;
const result = extractFromSource('handler.ts', code);
// Should be exactly 1 function node, 0 variable nodes for 'handler'
const funcNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'function');
const varNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'variable');
expect(funcNodes).toHaveLength(1);
expect(varNodes).toHaveLength(0);
});
it('should extract nested calls in arrow functions in JavaScript', () => {
const code = `
export const fetchData = async () => {
const response = await fetchAPI('/data');
return parseResponse(response);
};
`;
const result = extractFromSource('api.js', code);
const funcNode = result.nodes.find((n) => n.name === 'fetchData');
expect(funcNode).toBeDefined();
expect(funcNode?.kind).toBe('function');
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
const callNames = calls.map((c) => c.referenceName);
expect(callNames).toContain('fetchAPI');
expect(callNames).toContain('parseResponse');
});
});
// =============================================================================
// Graph Traversal 'both' Direction Fix
// (requires better-sqlite3 - will use CodeGraph integration)
// =============================================================================
describe('Graph Traversal Both Direction', () => {
let testDir: string;
beforeEach(() => {
testDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(testDir);
});
it.skipIf(!HAS_SQLITE)('should traverse both directions from a node', async () => {
const CodeGraph = (await import('../src/index')).default;
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
// A -> B -> C (A calls B, B calls C)
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
import { funcB } from './b';
export function funcA(): void { funcB(); }
`);
fs.writeFileSync(path.join(srcDir, 'b.ts'), `
import { funcC } from './c';
export function funcB(): void { funcC(); }
`);
fs.writeFileSync(path.join(srcDir, 'c.ts'), `
export function funcC(): void { console.log('c'); }
`);
const cg = CodeGraph.initSync(testDir, {
config: { include: ['src/**/*.ts'], exclude: [] },
});
await cg.indexAll();
cg.resolveReferences();
const functions = cg.getNodesByKind('function');
const funcB = functions.find((n) => n.name === 'funcB');
if (!funcB) {
cg.destroy();
return;
}
// Traverse 'both' from B - should find A (incoming caller) and C (outgoing callee)
const subgraph = cg.traverse(funcB.id, {
maxDepth: 1,
direction: 'both',
});
// B itself + at least one neighbor in each direction
expect(subgraph.nodes.size).toBeGreaterThanOrEqual(2);
expect(subgraph.nodes.has(funcB.id)).toBe(true);
cg.destroy();
});
});
// =============================================================================
// Best-Candidate Resolution
// =============================================================================
describe('Best-Candidate Resolution', () => {
it.skipIf(!HAS_SQLITE)('should be testable via the resolution module types', async () => {
const { ReferenceResolver } = await import('../src/resolution');
expect(typeof ReferenceResolver.prototype.resolveOne).toBe('function');
});
});
// =============================================================================
// Schema v2 Migration
// =============================================================================
describe('Schema v2 Migration', () => {
it.skipIf(!HAS_SQLITE)('should have correct current schema version', async () => {
const { CURRENT_SCHEMA_VERSION } = await import('../src/db/migrations');
expect(CURRENT_SCHEMA_VERSION).toBe(4);
});
it.skipIf(!HAS_SQLITE)('should have migration for version 2', async () => {
const { getPendingMigrations } = await import('../src/db/migrations');
expect(typeof getPendingMigrations).toBe('function');
});
});
// =============================================================================
// Database Layer: Batch Insert, getAllNodes, Pragmas
// =============================================================================
describe('Database Layer Improvements', () => {
let testDir: string;
beforeEach(() => {
testDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(testDir);
});
it.skipIf(!HAS_SQLITE)('should support batch insert of unresolved refs', async () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Insert a node first (needed as foreign key)
queries.insertNode({
id: 'func:test:1',
kind: 'function',
name: 'testFunc',
qualifiedName: 'test::testFunc',
filePath: 'test.ts',
language: 'typescript',
startLine: 1,
endLine: 5,
startColumn: 0,
endColumn: 1,
updatedAt: Date.now(),
});
// Batch insert unresolved refs with filePath and language
queries.insertUnresolvedRefsBatch([
{
fromNodeId: 'func:test:1',
referenceName: 'helperA',
referenceKind: 'calls',
line: 2,
column: 4,
filePath: 'test.ts',
language: 'typescript',
},
{
fromNodeId: 'func:test:1',
referenceName: 'helperB',
referenceKind: 'calls',
line: 3,
column: 4,
filePath: 'test.ts',
language: 'typescript',
},
]);
const refs = queries.getUnresolvedReferences();
expect(refs).toHaveLength(2);
expect(refs.map((r) => r.referenceName).sort()).toEqual(['helperA', 'helperB']);
// Verify filePath and language are persisted
expect(refs[0]?.filePath).toBe('test.ts');
expect(refs[0]?.language).toBe('typescript');
db.close();
});
it.skipIf(!HAS_SQLITE)('should support getAllNodes', async () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Insert some nodes
for (let i = 0; i < 3; i++) {
queries.insertNode({
id: `func:test:${i}`,
kind: 'function',
name: `func${i}`,
qualifiedName: `test::func${i}`,
filePath: 'test.ts',
language: 'typescript',
startLine: i * 10 + 1,
endLine: i * 10 + 5,
startColumn: 0,
endColumn: 1,
updatedAt: Date.now(),
});
}
const allNodes = queries.getAllNodes();
expect(allNodes).toHaveLength(3);
expect(allNodes.map((n) => n.name).sort()).toEqual(['func0', 'func1', 'func2']);
db.close();
});
it.skipIf(!HAS_SQLITE)('should set performance pragmas on initialization', async () => {
const { DatabaseConnection } = await import('../src/db');
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const rawDb = db.getDb();
// Check pragmas were set
const synchronous = rawDb.pragma('synchronous', { simple: true });
expect(synchronous).toBe(1); // NORMAL = 1
const cacheSize = rawDb.pragma('cache_size', { simple: true }) as number;
expect(cacheSize).toBe(-64000);
const tempStore = rawDb.pragma('temp_store', { simple: true });
expect(tempStore).toBe(2); // MEMORY = 2
const mmapSize = rawDb.pragma('mmap_size', { simple: true }) as number;
expect(mmapSize).toBe(268435456); // 256 MB
db.close();
});
it.skipIf(!HAS_SQLITE)('should handle empty batch insert gracefully', async () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Should not throw on empty array
expect(() => queries.insertUnresolvedRefsBatch([])).not.toThrow();
db.close();
});
});
// =============================================================================
// Resolution Warm Caches
// =============================================================================
describe('Resolution Warm Caches', () => {
let testDir: string;
beforeEach(() => {
testDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(testDir);
});
it.skipIf(!HAS_SQLITE)('should warm caches and use them for lookups', async () => {
const CodeGraph = (await import('../src/index')).default;
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
export function myFunc(): void {}
export function otherFunc(): void { myFunc(); }
`);
const cg = CodeGraph.initSync(testDir, {
config: { include: ['src/**/*.ts'], exclude: [] },
});
await cg.indexAll();
// resolveReferences internally calls warmCaches
const result = cg.resolveReferences();
// Should complete without error
expect(result.stats.total).toBeGreaterThanOrEqual(0);
cg.destroy();
});
});
// =============================================================================
// MCP Tool Improvements
// =============================================================================
describe('MCP Tool Improvements', () => {
it.skipIf(!HAS_SQLITE)('should export ToolHandler class', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
expect(typeof ToolHandler).toBe('function');
});
it.skipIf(!HAS_SQLITE)('should have findSymbol and truncateOutput as private methods', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
const proto = ToolHandler.prototype;
expect(typeof (proto as any).findSymbol).toBe('function');
expect(typeof (proto as any).truncateOutput).toBe('function');
});
it.skipIf(!HAS_SQLITE)('should truncate output exceeding MAX_OUTPUT_LENGTH', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
// Access private method for testing
const handler = Object.create(ToolHandler.prototype);
const truncate = (handler as any).truncateOutput.bind(handler);
// Short text should not be truncated
const short = 'Hello world';
expect(truncate(short)).toBe(short);
// Long text should be truncated
const long = 'x'.repeat(20000);
const result = truncate(long);
expect(result.length).toBeLessThan(long.length);
expect(result).toContain('... (output truncated)');
});
it.skipIf(!HAS_SQLITE)('should truncate at a clean line boundary', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
const handler = Object.create(ToolHandler.prototype);
const truncate = (handler as any).truncateOutput.bind(handler);
// Build text with newlines exceeding the limit
const lines: string[] = [];
for (let i = 0; i < 500; i++) {
lines.push(`Line ${i}: ${'a'.repeat(50)}`);
}
const text = lines.join('\n');
const result = truncate(text);
// Should end with truncation notice after a newline boundary
expect(result).toContain('... (output truncated)');
// Should not cut mid-line (the char before truncation notice should be \n)
const beforeTruncation = result.split('\n\n... (output truncated)')[0]!;
expect(beforeTruncation.endsWith('\n') || !beforeTruncation.includes('\0')).toBe(true);
});
describe('findSymbol disambiguation', () => {
it.skipIf(!HAS_SQLITE)('should prefer exact name matches', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
const CodeGraph = (await import('../src/index')).default;
const tmpDir = createTempDir();
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
export function getValue(): number { return 1; }
export function getValueFromCache(): number { return 2; }
`);
const cg = CodeGraph.initSync(tmpDir, {
config: { include: ['src/**/*.ts'], exclude: [] },
});
await cg.indexAll();
const handler = new ToolHandler(cg);
const findSymbol = (handler as any).findSymbol.bind(handler);
const match = findSymbol(cg, 'getValue');
expect(match).not.toBeNull();
expect(match.node.name).toBe('getValue');
// Should not have a disambiguation note for single exact match
expect(match.note).toBe('');
handler.closeAll();
cg.destroy();
cleanupTempDir(tmpDir);
});
it.skipIf(!HAS_SQLITE)('should note when multiple symbols share the same name', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
const CodeGraph = (await import('../src/index')).default;
const tmpDir = createTempDir();
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
// Two files with the same function name
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
export function handle(): void {}
`);
fs.writeFileSync(path.join(srcDir, 'b.ts'), `
export function handle(): void {}
`);
const cg = CodeGraph.initSync(tmpDir, {
config: { include: ['src/**/*.ts'], exclude: [] },
});
await cg.indexAll();
const handler = new ToolHandler(cg);
const findSymbol = (handler as any).findSymbol.bind(handler);
const match = findSymbol(cg, 'handle');
expect(match).not.toBeNull();
expect(match.node.name).toBe('handle');
// Should have a disambiguation note
expect(match.note).toContain('2 symbols named "handle"');
handler.closeAll();
cg.destroy();
cleanupTempDir(tmpDir);
});
it.skipIf(!HAS_SQLITE)('should return null when symbol is not found', async () => {
const { ToolHandler } = await import('../src/mcp/tools');
const CodeGraph = (await import('../src/index')).default;
const tmpDir = createTempDir();
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'a.ts'), `export function foo(): void {}`);
const cg = CodeGraph.initSync(tmpDir, {
config: { include: ['src/**/*.ts'], exclude: [] },
});
await cg.indexAll();
const handler = new ToolHandler(cg);
const findSymbol = (handler as any).findSymbol.bind(handler);
const match = findSymbol(cg, 'nonExistentSymbol');
expect(match).toBeNull();
handler.closeAll();
cg.destroy();
cleanupTempDir(tmpDir);
});
});
});
// =============================================================================
// CLI uninit Command
// =============================================================================
describe('CLI uninit', () => {
let testDir: string;
beforeEach(() => {
testDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(testDir);
});
it.skipIf(!HAS_SQLITE)('should uninitialize a project via CodeGraph.uninitialize()', async () => {
const CodeGraph = (await import('../src/index')).default;
// Initialize
const cg = CodeGraph.initSync(testDir);
expect(CodeGraph.isInitialized(testDir)).toBe(true);
// Uninitialize
cg.uninitialize();
// .codegraph directory should be removed
expect(CodeGraph.isInitialized(testDir)).toBe(false);
});
});
// =============================================================================
// Tree-sitter Version Pinning
// =============================================================================
describe('Tree-sitter WASM Setup', () => {
it('should use web-tree-sitter and tree-sitter-wasms in dependencies', () => {
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
expect(pkg.dependencies['web-tree-sitter']).toBeDefined();
expect(pkg.dependencies['tree-sitter-wasms']).toBeDefined();
});
it('should not have native tree-sitter in dependencies', () => {
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
expect(pkg.dependencies['tree-sitter']).toBeUndefined();
expect(pkg.overrides).toBeUndefined();
});
});
// =============================================================================
// Embedder Float32Array Fix
// =============================================================================
describe('Float32Array Fix', () => {
it('should correctly convert typed arrays (regression check)', () => {
// Simulates the fix: Float32Array.from(Array.from(arr)) vs new Float32Array(arr.length)
const source = new Float64Array([1.5, 2.5, 3.5, 4.5]);
// The OLD buggy approach:
const buggy = new Float32Array(source.length);
// buggy is all zeros!
expect(buggy[0]).toBe(0);
expect(buggy[1]).toBe(0);
// The NEW fixed approach:
const fixed = Float32Array.from(Array.from(source));
expect(fixed[0]).toBeCloseTo(1.5);
expect(fixed[1]).toBeCloseTo(2.5);
expect(fixed[2]).toBeCloseTo(3.5);
expect(fixed[3]).toBeCloseTo(4.5);
});
});