f6772dac7c
Remove .codegraph/config.json and the entire config surface. CodeGraph now indexes every file whose extension maps to a supported language and respects .gitignore everywhere — git repos via git itself, non-git projects via the `ignore` library (root + nested .gitignore files, the same way git does). - Remove CodeGraphConfig/DEFAULT_CONFIG, src/config.ts, and the public config API (the `config` option on init, getConfig/updateConfig/getConfigPath). - Derive the source-file allowlist from EXTENSION_MAP (isSourceFile); maxFileSize is now a constant. Drop the .codegraphignore marker. - Behavior change: committed, non-gitignored dirs (vendor/, a committed dist/) are now indexed — .gitignore is the single source of truth. Earlier inert fields (languages, frameworks, extractDocstrings, trackCallSites, customPatterns) and their dead helpers are removed as part of this. Resolves #283. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
307 lines
9.0 KiB
TypeScript
307 lines
9.0 KiB
TypeScript
/**
|
|
* Sync Module Tests
|
|
*
|
|
* Tests for sync functionality (incremental updates).
|
|
* Note: Git hooks functionality has been removed in favor of codegraph's
|
|
* Claude Code hooks integration.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { execFileSync } from 'child_process';
|
|
import CodeGraph from '../src/index';
|
|
|
|
describe('Sync Module', () => {
|
|
describe('Sync Functionality', () => {
|
|
let testDir: string;
|
|
let cg: CodeGraph;
|
|
|
|
beforeEach(async () => {
|
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-func-'));
|
|
|
|
// Create initial source files
|
|
const srcDir = path.join(testDir, 'src');
|
|
fs.mkdirSync(srcDir);
|
|
fs.writeFileSync(
|
|
path.join(srcDir, 'index.ts'),
|
|
`export function hello() { return 'world'; }`
|
|
);
|
|
|
|
// Initialize and index
|
|
cg = CodeGraph.initSync(testDir, {
|
|
config: {
|
|
include: ['**/*.ts'],
|
|
exclude: [],
|
|
},
|
|
});
|
|
await cg.indexAll();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (cg) {
|
|
cg.destroy();
|
|
}
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('getChangedFiles()', () => {
|
|
it('should detect added files', () => {
|
|
// Add a new file
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'new.ts'),
|
|
`export function newFunc() { return 42; }`
|
|
);
|
|
|
|
const changes = cg.getChangedFiles();
|
|
|
|
expect(changes.added).toContain('src/new.ts');
|
|
expect(changes.modified).toHaveLength(0);
|
|
expect(changes.removed).toHaveLength(0);
|
|
});
|
|
|
|
it('should detect modified files', () => {
|
|
// Modify existing file
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'index.ts'),
|
|
`export function hello() { return 'modified'; }`
|
|
);
|
|
|
|
const changes = cg.getChangedFiles();
|
|
|
|
expect(changes.added).toHaveLength(0);
|
|
expect(changes.modified).toContain('src/index.ts');
|
|
expect(changes.removed).toHaveLength(0);
|
|
});
|
|
|
|
it('should detect removed files', () => {
|
|
// Remove file
|
|
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
|
|
|
|
const changes = cg.getChangedFiles();
|
|
|
|
expect(changes.added).toHaveLength(0);
|
|
expect(changes.modified).toHaveLength(0);
|
|
expect(changes.removed).toContain('src/index.ts');
|
|
});
|
|
});
|
|
|
|
describe('sync()', () => {
|
|
it('should reindex added files', async () => {
|
|
// Add a new file
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'new.ts'),
|
|
`export function newFunc() { return 42; }`
|
|
);
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesAdded).toBe(1);
|
|
expect(result.filesModified).toBe(0);
|
|
expect(result.filesRemoved).toBe(0);
|
|
|
|
// Verify new function is in the graph
|
|
const nodes = cg.searchNodes('newFunc');
|
|
expect(nodes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should reindex modified files', async () => {
|
|
// Modify existing file
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'index.ts'),
|
|
`export function goodbye() { return 'farewell'; }`
|
|
);
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesModified).toBe(1);
|
|
|
|
// Verify new function is in the graph
|
|
const nodes = cg.searchNodes('goodbye');
|
|
expect(nodes.length).toBeGreaterThan(0);
|
|
|
|
// Verify old function is gone
|
|
const oldNodes = cg.searchNodes('hello');
|
|
expect(oldNodes.length).toBe(0);
|
|
});
|
|
|
|
it('should remove nodes from deleted files', async () => {
|
|
// Remove file
|
|
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesRemoved).toBe(1);
|
|
|
|
// Verify function is gone
|
|
const nodes = cg.searchNodes('hello');
|
|
expect(nodes.length).toBe(0);
|
|
});
|
|
|
|
it('should report no changes when nothing changed', async () => {
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesAdded).toBe(0);
|
|
expect(result.filesModified).toBe(0);
|
|
expect(result.filesRemoved).toBe(0);
|
|
expect(result.filesChecked).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Git-based sync', () => {
|
|
let testDir: string;
|
|
let cg: CodeGraph;
|
|
|
|
function git(...args: string[]) {
|
|
execFileSync('git', args, { cwd: testDir, stdio: 'pipe' });
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-git-sync-'));
|
|
|
|
// Initialize a git repo with an initial commit
|
|
git('init');
|
|
git('config', 'user.email', 'test@test.com');
|
|
git('config', 'user.name', 'Test');
|
|
|
|
const srcDir = path.join(testDir, 'src');
|
|
fs.mkdirSync(srcDir);
|
|
fs.writeFileSync(
|
|
path.join(srcDir, 'index.ts'),
|
|
`export function hello() { return 'world'; }`
|
|
);
|
|
|
|
git('add', '-A');
|
|
git('commit', '-m', 'initial');
|
|
|
|
// Initialize CodeGraph and index
|
|
cg = CodeGraph.initSync(testDir, {
|
|
config: {
|
|
include: ['**/*.ts'],
|
|
exclude: [],
|
|
},
|
|
});
|
|
await cg.indexAll();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (cg) {
|
|
cg.destroy();
|
|
}
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('should detect modified files via git', async () => {
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'index.ts'),
|
|
`export function hello() { return 'modified'; }`
|
|
);
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesModified).toBe(1);
|
|
expect(result.changedFilePaths).toContain('src/index.ts');
|
|
});
|
|
|
|
it('should detect new untracked files via git', async () => {
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'new.ts'),
|
|
`export function newFunc() { return 42; }`
|
|
);
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesAdded).toBe(1);
|
|
expect(result.changedFilePaths).toContain('src/new.ts');
|
|
|
|
// Verify the function was indexed
|
|
const nodes = cg.searchNodes('newFunc');
|
|
expect(nodes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should stop reporting untracked files once they are indexed (issue #206)', async () => {
|
|
// Untracked files stay `??` in git status even after codegraph indexes
|
|
// them. Change detection must compare them against the DB by hash, not
|
|
// report every untracked file as "added" on every sync/status.
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'new.ts'),
|
|
`export function newFunc() { return 42; }`
|
|
);
|
|
|
|
// First sync indexes the untracked file.
|
|
const first = await cg.sync();
|
|
expect(first.filesAdded).toBe(1);
|
|
|
|
// The file is still untracked in git, but now lives in the DB.
|
|
expect(cg.searchNodes('newFunc').length).toBeGreaterThan(0);
|
|
|
|
// status must not keep flagging it as a pending addition...
|
|
const changes = cg.getChangedFiles();
|
|
expect(changes.added).not.toContain('src/new.ts');
|
|
expect(changes.modified).not.toContain('src/new.ts');
|
|
|
|
// ...and a second sync must be a no-op for it.
|
|
const second = await cg.sync();
|
|
expect(second.filesAdded).toBe(0);
|
|
expect(second.filesModified).toBe(0);
|
|
});
|
|
|
|
it('should re-index an untracked file when its contents change', async () => {
|
|
const filePath = path.join(testDir, 'src', 'new.ts');
|
|
fs.writeFileSync(filePath, `export function newFunc() { return 42; }`);
|
|
await cg.sync();
|
|
|
|
// Modify the still-untracked file.
|
|
fs.writeFileSync(filePath, `export function renamedFunc() { return 7; }`);
|
|
|
|
const changes = cg.getChangedFiles();
|
|
expect(changes.modified).toContain('src/new.ts');
|
|
|
|
const result = await cg.sync();
|
|
expect(result.filesModified).toBe(1);
|
|
expect(cg.searchNodes('renamedFunc').length).toBeGreaterThan(0);
|
|
expect(cg.searchNodes('newFunc').length).toBe(0);
|
|
});
|
|
|
|
it('should detect deleted files via git', async () => {
|
|
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesRemoved).toBe(1);
|
|
|
|
// Verify function is gone
|
|
const nodes = cg.searchNodes('hello');
|
|
expect(nodes.length).toBe(0);
|
|
});
|
|
|
|
it('should skip files with unsupported extensions', async () => {
|
|
// A .txt file has no supported grammar, so sync must not index it.
|
|
fs.writeFileSync(
|
|
path.join(testDir, 'src', 'notes.txt'),
|
|
`just some notes`
|
|
);
|
|
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesAdded).toBe(0);
|
|
expect(result.filesModified).toBe(0);
|
|
});
|
|
|
|
it('should report no changes on clean working tree', async () => {
|
|
const result = await cg.sync();
|
|
|
|
expect(result.filesAdded).toBe(0);
|
|
expect(result.filesModified).toBe(0);
|
|
expect(result.filesRemoved).toBe(0);
|
|
expect(result.changedFilePaths).toBeUndefined();
|
|
});
|
|
});
|
|
});
|