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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5b71a89574
commit
f6772dac7c
@@ -9,10 +9,9 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
|
||||
import { extractFromSource, scanDirectory } from '../src/extraction';
|
||||
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
||||
import { normalizePath } from '../src/utils';
|
||||
import { DEFAULT_CONFIG } from '../src/types';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
@@ -3003,39 +3002,57 @@ describe('Directory Exclusion', () => {
|
||||
cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
it('should exclude node_modules directories', () => {
|
||||
// Create structure: src/index.ts + node_modules/pkg/index.js
|
||||
it('should exclude directories listed in .gitignore', () => {
|
||||
// Create structure: src/index.ts + node_modules/pkg/index.js, gitignore node_modules
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const nmDir = path.join(tempDir, 'node_modules', 'pkg');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(nmDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
|
||||
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude nested node_modules directories', () => {
|
||||
// Create structure: packages/app/node_modules/pkg/index.js
|
||||
it('should exclude nested node_modules via a root .gitignore', () => {
|
||||
// A trailing-slash pattern with no leading slash matches at any depth.
|
||||
const srcDir = path.join(tempDir, 'packages', 'app', 'src');
|
||||
const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(nmDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
|
||||
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
expect(files).toContain('packages/app/src/index.ts');
|
||||
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude .git directories', () => {
|
||||
it('should apply a nested .gitignore only to its own subtree', () => {
|
||||
const appSrc = path.join(tempDir, 'app', 'src');
|
||||
fs.mkdirSync(appSrc, { recursive: true });
|
||||
fs.writeFileSync(path.join(appSrc, 'keep.ts'), 'export const a = 1;');
|
||||
fs.writeFileSync(path.join(appSrc, 'skip.ts'), 'export const b = 2;');
|
||||
fs.writeFileSync(path.join(tempDir, 'app', '.gitignore'), 'src/skip.ts\n');
|
||||
// A sibling with the same name outside app/ must NOT be ignored.
|
||||
const otherDir = path.join(tempDir, 'other', 'src');
|
||||
fs.mkdirSync(otherDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(otherDir, 'skip.ts'), 'export const c = 3;');
|
||||
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
expect(files).toContain('app/src/keep.ts');
|
||||
expect(files).not.toContain('app/src/skip.ts');
|
||||
expect(files).toContain('other/src/skip.ts');
|
||||
});
|
||||
|
||||
it('should always skip .git directories', () => {
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const gitDir = path.join(tempDir, '.git', 'objects');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
@@ -3043,8 +3060,7 @@ describe('Directory Exclusion', () => {
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('.git'))).toBe(true);
|
||||
@@ -3055,29 +3071,12 @@ describe('Directory Exclusion', () => {
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0]).toBe('src/components/Button.tsx');
|
||||
expect(files[0]).not.toContain('\\');
|
||||
});
|
||||
|
||||
it('should respect .codegraphignore marker', () => {
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const vendorDir = path.join(tempDir, 'vendor');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(vendorDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(vendorDir, 'lib.ts'), 'export const y = 2;');
|
||||
fs.writeFileSync(path.join(vendorDir, '.codegraphignore'), '');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Git Submodules', () => {
|
||||
@@ -3124,8 +3123,7 @@ describe('Git Submodules', () => {
|
||||
);
|
||||
git(mainDir, 'commit', '-q', '-m', 'add submodule');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: mainDir };
|
||||
const files = scanDirectory(mainDir, config);
|
||||
const files = scanDirectory(mainDir);
|
||||
|
||||
expect(files).toContain('app.ts');
|
||||
expect(files).toContain('libs/lib/lib.ts');
|
||||
@@ -3173,8 +3171,7 @@ describe('Nested non-submodule git repos', () => {
|
||||
git(path.join(root, 'sub_repo2'), 'init', '-q');
|
||||
fs.writeFileSync(path.join(sub2, 'two.ts'), 'export const two = 2;');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: root };
|
||||
const files = scanDirectory(root, config);
|
||||
const files = scanDirectory(root);
|
||||
|
||||
// Both committed and untracked source from the nested repos must be found.
|
||||
expect(files).toContain('sub_repo1/src/one.ts');
|
||||
@@ -3197,8 +3194,7 @@ describe('Nested non-submodule git repos', () => {
|
||||
fs.writeFileSync(path.join(sub, 'real.ts'), 'export const real = 1;');
|
||||
fs.writeFileSync(path.join(sub, 'generated.ts'), 'export const generated = 1;');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: root };
|
||||
const files = scanDirectory(root, config);
|
||||
const files = scanDirectory(root);
|
||||
|
||||
expect(files).toContain('sub_repo/src/real.ts');
|
||||
expect(files).not.toContain('sub_repo/src/generated.ts');
|
||||
|
||||
@@ -9,8 +9,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { DEFAULT_CONFIG, Node, Edge } from '../src/types';
|
||||
import { loadConfig, saveConfig } from '../src/config';
|
||||
import { Node, Edge } from '../src/types';
|
||||
import { isInitialized, getCodeGraphDir, validateDirectory } from '../src/directory';
|
||||
import { DatabaseConnection, getDatabasePath } from '../src/db';
|
||||
|
||||
@@ -60,41 +59,12 @@ describe('CodeGraph Foundation', () => {
|
||||
cg.close();
|
||||
});
|
||||
|
||||
it('should create config.json with defaults', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
|
||||
const configPath = path.join(getCodeGraphDir(tempDir), 'config.json');
|
||||
expect(fs.existsSync(configPath)).toBe(true);
|
||||
|
||||
const config = cg.getConfig();
|
||||
expect(config.version).toBe(DEFAULT_CONFIG.version);
|
||||
expect(config.include).toEqual(DEFAULT_CONFIG.include);
|
||||
expect(config.exclude).toEqual(DEFAULT_CONFIG.exclude);
|
||||
|
||||
cg.close();
|
||||
});
|
||||
|
||||
it('should throw if already initialized', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
cg.close();
|
||||
|
||||
expect(() => CodeGraph.initSync(tempDir)).toThrow(/already initialized/i);
|
||||
});
|
||||
|
||||
it('should accept custom config options', () => {
|
||||
const cg = CodeGraph.initSync(tempDir, {
|
||||
config: {
|
||||
maxFileSize: 500000,
|
||||
extractDocstrings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const config = cg.getConfig();
|
||||
expect(config.maxFileSize).toBe(500000);
|
||||
expect(config.extractDocstrings).toBe(false);
|
||||
|
||||
cg.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Opening Projects', () => {
|
||||
@@ -112,17 +82,6 @@ describe('CodeGraph Foundation', () => {
|
||||
it('should throw if not initialized', () => {
|
||||
expect(() => CodeGraph.openSync(tempDir)).toThrow(/not initialized/i);
|
||||
});
|
||||
|
||||
it('should preserve configuration across open/close', () => {
|
||||
const cg1 = CodeGraph.initSync(tempDir, {
|
||||
config: { maxFileSize: 123456 },
|
||||
});
|
||||
cg1.close();
|
||||
|
||||
const cg2 = CodeGraph.openSync(tempDir);
|
||||
expect(cg2.getConfig().maxFileSize).toBe(123456);
|
||||
cg2.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static Methods', () => {
|
||||
@@ -182,31 +141,6 @@ describe('CodeGraph Foundation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('should load and merge config with defaults', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
cg.close();
|
||||
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config.version).toBe(DEFAULT_CONFIG.version);
|
||||
expect(config.rootDir).toBe(path.resolve(tempDir));
|
||||
});
|
||||
|
||||
it('should update configuration', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
|
||||
cg.updateConfig({ maxFileSize: 999999 });
|
||||
|
||||
expect(cg.getConfig().maxFileSize).toBe(999999);
|
||||
|
||||
cg.close();
|
||||
|
||||
// Verify persistence
|
||||
const config = loadConfig(tempDir);
|
||||
expect(config.maxFileSize).toBe(999999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Directory Management', () => {
|
||||
it('should validate directory structure', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
|
||||
+18
-72
@@ -15,9 +15,7 @@ import * as os from 'os';
|
||||
import { FileLock } from '../src/utils';
|
||||
import CodeGraph from '../src/index';
|
||||
import { ToolHandler, tools } from '../src/mcp/tools';
|
||||
import { shouldIncludeFile, scanDirectory } from '../src/extraction';
|
||||
import { shouldIncludeFile as configShouldInclude } from '../src/config';
|
||||
import { CodeGraphConfig, DEFAULT_CONFIG } from '../src/types';
|
||||
import { scanDirectory, isSourceFile } from '../src/extraction';
|
||||
import { DatabaseConnection, getDatabasePath } from '../src/db';
|
||||
import { QueryBuilder } from '../src/db/queries';
|
||||
|
||||
@@ -298,58 +296,24 @@ describe('Atomic Writes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Glob Matching (picomatch)', () => {
|
||||
const makeConfig = (include: string[], exclude: string[]): CodeGraphConfig => ({
|
||||
...DEFAULT_CONFIG,
|
||||
rootDir: '/test',
|
||||
include,
|
||||
exclude,
|
||||
describe('Source file detection (isSourceFile)', () => {
|
||||
it('selects files by supported extension', () => {
|
||||
expect(isSourceFile('src/index.ts')).toBe(true);
|
||||
expect(isSourceFile('src/deep/nested/file.ts')).toBe(true);
|
||||
expect(isSourceFile('src/component.tsx')).toBe(true);
|
||||
expect(isSourceFile('lib/util.js')).toBe(true);
|
||||
expect(isSourceFile('src/main.py')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match standard glob patterns in extraction', () => {
|
||||
const config = makeConfig(['**/*.ts'], ['node_modules/**']);
|
||||
|
||||
expect(shouldIncludeFile('src/index.ts', config)).toBe(true);
|
||||
expect(shouldIncludeFile('src/deep/nested/file.ts', config)).toBe(true);
|
||||
expect(shouldIncludeFile('src/index.js', config)).toBe(false);
|
||||
expect(shouldIncludeFile('node_modules/lib/index.ts', config)).toBe(false);
|
||||
it('rejects unsupported extensions and extensionless files', () => {
|
||||
expect(isSourceFile('src/component.css')).toBe(false);
|
||||
expect(isSourceFile('README.md')).toBe(false);
|
||||
expect(isSourceFile('Makefile')).toBe(false);
|
||||
expect(isSourceFile('.gitignore')).toBe(false);
|
||||
});
|
||||
|
||||
it('should match standard glob patterns in config', () => {
|
||||
const config = makeConfig(['**/*.py'], ['__pycache__/**']);
|
||||
|
||||
expect(configShouldInclude('src/main.py', config)).toBe(true);
|
||||
expect(configShouldInclude('src/main.ts', config)).toBe(false);
|
||||
expect(configShouldInclude('__pycache__/module.py', config)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex glob patterns correctly', () => {
|
||||
const config = makeConfig(['src/**/*.{ts,tsx}', 'lib/**/*.js'], []);
|
||||
|
||||
expect(shouldIncludeFile('src/component.ts', config)).toBe(true);
|
||||
expect(shouldIncludeFile('src/component.tsx', config)).toBe(true);
|
||||
expect(shouldIncludeFile('lib/util.js', config)).toBe(true);
|
||||
expect(shouldIncludeFile('src/component.css', config)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle patterns that previously caused ReDoS', () => {
|
||||
// This pattern would cause catastrophic backtracking with hand-rolled regex
|
||||
const evilPattern = '**/**/**/**/**/**/**/**/**/**/**/**/**/**/a';
|
||||
const config = makeConfig([evilPattern], []);
|
||||
|
||||
const start = Date.now();
|
||||
// This should return quickly, not hang
|
||||
shouldIncludeFile('x/x/x/x/x/x/x/x/x/x/x/x/x/x/b', config);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
// Should complete in under 100ms, not seconds
|
||||
expect(elapsed).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('should handle dot files correctly', () => {
|
||||
const config = makeConfig(['**/*.ts'], []);
|
||||
|
||||
expect(shouldIncludeFile('.hidden/index.ts', config)).toBe(true);
|
||||
it('matches regardless of leading dot directories', () => {
|
||||
expect(isSourceFile('.hidden/index.ts')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,15 +428,9 @@ describe('Symlink Cycle Detection', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const config: CodeGraphConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
rootDir: tempDir,
|
||||
include: ['**/*.ts'],
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
// This should complete without hanging
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
// Should find the real file but not loop infinitely
|
||||
expect(files).toContain('src/index.ts');
|
||||
@@ -496,14 +454,8 @@ describe('Symlink Cycle Detection', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const config: CodeGraphConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
rootDir: tempDir,
|
||||
include: ['**/*.ts'],
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
|
||||
// Should find files from both the real dir and via the symlink
|
||||
// But deduplicate since they resolve to the same real path
|
||||
@@ -521,15 +473,9 @@ describe('Symlink Cycle Detection', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const config: CodeGraphConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
rootDir: tempDir,
|
||||
include: ['**/*.ts'],
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
const files = scanDirectory(tempDir, config);
|
||||
const files = scanDirectory(tempDir);
|
||||
expect(files).toContain('src/valid.ts');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,11 +281,11 @@ describe('Sync Module', () => {
|
||||
expect(nodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should skip files not matching config', async () => {
|
||||
// Create a .js file which doesn't match **/*.ts
|
||||
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', 'ignored.js'),
|
||||
`function ignored() {}`
|
||||
path.join(testDir, 'src', 'notes.txt'),
|
||||
`just some notes`
|
||||
);
|
||||
|
||||
const result = await cg.sync();
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { watchDisabledReason } from '../src/sync/watch-policy';
|
||||
import { FileWatcher } from '../src/sync/watcher';
|
||||
import type { CodeGraphConfig } from '../src/types';
|
||||
|
||||
describe('watchDisabledReason', () => {
|
||||
it('returns a reason when CODEGRAPH_NO_WATCH=1', () => {
|
||||
@@ -63,18 +62,6 @@ describe('watchDisabledReason', () => {
|
||||
describe('FileWatcher honors the watch policy', () => {
|
||||
let testDir: string;
|
||||
|
||||
const baseConfig: CodeGraphConfig = {
|
||||
version: 1,
|
||||
rootDir: '.',
|
||||
include: ['**/*.ts'],
|
||||
exclude: ['**/node_modules/**'],
|
||||
languages: [],
|
||||
frameworks: [],
|
||||
maxFileSize: 1024 * 1024,
|
||||
extractDocstrings: true,
|
||||
trackCallSites: true,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.CODEGRAPH_NO_WATCH;
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
@@ -87,7 +74,7 @@ describe('FileWatcher honors the watch policy', () => {
|
||||
process.env.CODEGRAPH_NO_WATCH = '1';
|
||||
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn);
|
||||
const watcher = new FileWatcher(testDir, syncFn);
|
||||
|
||||
expect(watcher.start()).toBe(false);
|
||||
expect(watcher.isActive()).toBe(false);
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { FileWatcher } from '../src/sync/watcher';
|
||||
import type { CodeGraphConfig } from '../src/types';
|
||||
import CodeGraph from '../src/index';
|
||||
|
||||
/**
|
||||
@@ -34,18 +33,6 @@ function waitFor(
|
||||
describe('FileWatcher', () => {
|
||||
let testDir: string;
|
||||
|
||||
const baseConfig: CodeGraphConfig = {
|
||||
version: 1,
|
||||
rootDir: '.',
|
||||
include: ['**/*.ts', '**/*.js'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
languages: [],
|
||||
frameworks: [],
|
||||
maxFileSize: 1024 * 1024,
|
||||
extractDocstrings: true,
|
||||
trackCallSites: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-watcher-'));
|
||||
// Create a source file so the directory isn't empty
|
||||
@@ -63,7 +50,7 @@ describe('FileWatcher', () => {
|
||||
describe('start/stop lifecycle', () => {
|
||||
it('should start and stop without errors', () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn);
|
||||
const watcher = new FileWatcher(testDir, syncFn);
|
||||
|
||||
const started = watcher.start();
|
||||
expect(started).toBe(true);
|
||||
@@ -75,7 +62,7 @@ describe('FileWatcher', () => {
|
||||
|
||||
it('should be idempotent on double start', () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn);
|
||||
const watcher = new FileWatcher(testDir, syncFn);
|
||||
|
||||
expect(watcher.start()).toBe(true);
|
||||
expect(watcher.start()).toBe(true); // Should not throw
|
||||
@@ -86,7 +73,7 @@ describe('FileWatcher', () => {
|
||||
|
||||
it('should be idempotent on double stop', () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn);
|
||||
const watcher = new FileWatcher(testDir, syncFn);
|
||||
|
||||
watcher.start();
|
||||
watcher.stop();
|
||||
@@ -98,7 +85,7 @@ describe('FileWatcher', () => {
|
||||
describe('debounced sync', () => {
|
||||
it('should trigger sync after file change', async () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 1, durationMs: 10 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, { debounceMs: 200 });
|
||||
const watcher = new FileWatcher(testDir, syncFn, { debounceMs: 200 });
|
||||
|
||||
watcher.start();
|
||||
|
||||
@@ -114,7 +101,7 @@ describe('FileWatcher', () => {
|
||||
|
||||
it('should debounce rapid changes into a single sync', async () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 1, durationMs: 10 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, { debounceMs: 500 });
|
||||
const watcher = new FileWatcher(testDir, syncFn, { debounceMs: 500 });
|
||||
|
||||
watcher.start();
|
||||
|
||||
@@ -140,7 +127,7 @@ describe('FileWatcher', () => {
|
||||
describe('filtering', () => {
|
||||
it('should ignore files not matching include patterns', async () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, { debounceMs: 200 });
|
||||
const watcher = new FileWatcher(testDir, syncFn, { debounceMs: 200 });
|
||||
|
||||
watcher.start();
|
||||
|
||||
@@ -160,7 +147,7 @@ describe('FileWatcher', () => {
|
||||
|
||||
it('should ignore .codegraph directory changes', async () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, { debounceMs: 200 });
|
||||
const watcher = new FileWatcher(testDir, syncFn, { debounceMs: 200 });
|
||||
|
||||
watcher.start();
|
||||
|
||||
@@ -185,7 +172,7 @@ describe('FileWatcher', () => {
|
||||
it('should call onSyncComplete after successful sync', async () => {
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 2, durationMs: 50 });
|
||||
const onSyncComplete = vi.fn();
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, {
|
||||
const watcher = new FileWatcher(testDir, syncFn, {
|
||||
debounceMs: 200,
|
||||
onSyncComplete,
|
||||
});
|
||||
@@ -203,7 +190,7 @@ describe('FileWatcher', () => {
|
||||
it('should call onSyncError when sync throws', async () => {
|
||||
const syncFn = vi.fn().mockRejectedValue(new Error('sync failed'));
|
||||
const onSyncError = vi.fn();
|
||||
const watcher = new FileWatcher(testDir, baseConfig, syncFn, {
|
||||
const watcher = new FileWatcher(testDir, syncFn, {
|
||||
debounceMs: 200,
|
||||
onSyncError,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user