Files
colbymchenry--codegraph/__tests__/sync.test.ts
Colby Mchenry a47355780b fix(sync): stop reporting git-untracked files as pending after sync (#206) (#218)
Both git fast-paths in ExtractionOrchestrator (sync and getChangedFiles)
classified every untracked (`??`) file as "added" without checking the
index. Indexing a file doesn't make git track it, so the file stayed `??`
and was re-reported as pending and re-indexed on every run: `codegraph
status` listed it under Pending Changes forever and each `sync` re-added
it, even though its symbols were already queryable.

Merge the modified + added handling into a single hash-compared loop so
untracked files get the same treatment as tracked ones: "added" only if
missing from the index, "modified" if contents changed, skipped otherwise.
The non-git fallback path already did this and is unchanged.

Closes #206. Reported by @15290391025.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:15:54 -05:00

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 not matching config', async () => {
// Create a .js file which doesn't match **/*.ts
fs.writeFileSync(
path.join(testDir, 'src', 'ignored.js'),
`function ignored() {}`
);
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();
});
});
});