|
|
@@ -438,8 +438,11 @@ describe('FileWatcher', () => {
|
|
|
watcher.start();
|
|
|
await watcher.waitUntilReady();
|
|
|
|
|
|
- // A non-source-file event — FileWatcher's `isSourceFile` gate must drop
|
|
|
- // it before scheduling sync.
|
|
|
+ // An EXISTING non-source file changing — FileWatcher's `isSourceFile`
|
|
|
+ // gate must drop it before scheduling sync. (It must exist on disk:
|
|
|
+ // a VANISHED non-source path is the deleted-directory shape, which
|
|
|
+ // deliberately schedules a sync — #1285.)
|
|
|
+ fs.writeFileSync(path.join(testDir, 'src', 'readme.md'), '# docs\n');
|
|
|
__emitWatchEventForTests(testDir, 'src/readme.md');
|
|
|
|
|
|
// Wait a bit longer than debounce — sync should NOT trigger.
|
|
|
@@ -449,6 +452,64 @@ describe('FileWatcher', () => {
|
|
|
watcher.stop();
|
|
|
});
|
|
|
|
|
|
+ it('a deleted directory schedules a sync so child records get removed (#1285)', async () => {
|
|
|
+ const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
|
|
+ const watcher = newWatcher(syncFn, { debounceMs: 100 });
|
|
|
+ watcher.start();
|
|
|
+ await watcher.waitUntilReady();
|
|
|
+
|
|
|
+ // A directory deletion arrives as ONE event on the directory path —
|
|
|
+ // no extension, nothing on disk anymore. Must schedule a sync (the
|
|
|
+ // sync's scan-diff removes the children), not be dropped as
|
|
|
+ // "non-source".
|
|
|
+ const sub = path.join(testDir, 'docs');
|
|
|
+ fs.mkdirSync(path.join(sub, 'nested'), { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(sub, 'nested', 'mod.ts'), 'export const q = 1;');
|
|
|
+ fs.rmSync(sub, { recursive: true, force: true });
|
|
|
+ __emitWatchEventForTests(testDir, 'docs');
|
|
|
+
|
|
|
+ await waitFor(() => syncFn.mock.calls.length > 0);
|
|
|
+ expect(syncFn).toHaveBeenCalled();
|
|
|
+
|
|
|
+ watcher.stop();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('end-to-end: deleting a subdirectory removes its files from the index via watch sync (#1285)', async () => {
|
|
|
+ // Real CodeGraph as the sync target; the watcher is inert and driven
|
|
|
+ // by the synthetic event seam for determinism.
|
|
|
+ fs.writeFileSync(path.join(testDir, 'root.ts'), 'export const r = 1;');
|
|
|
+ const deep = path.join(testDir, 'docs', 'a', 'b');
|
|
|
+ fs.mkdirSync(deep, { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(deep, 'inner.ts'), 'export const i = 2;');
|
|
|
+
|
|
|
+ const cg = CodeGraph.initSync(testDir);
|
|
|
+ await cg.indexAll();
|
|
|
+ const before = cg.getFiles().map((f) => f.path);
|
|
|
+ expect(before).toContain('docs/a/b/inner.ts');
|
|
|
+
|
|
|
+ const syncFn = vi.fn(async () => {
|
|
|
+ const r = await cg.sync();
|
|
|
+ return { filesChanged: r.filesAdded + r.filesModified + r.filesRemoved, durationMs: r.durationMs };
|
|
|
+ });
|
|
|
+ const watcher = newWatcher(syncFn, { debounceMs: 100 });
|
|
|
+ watcher.start();
|
|
|
+ await watcher.waitUntilReady();
|
|
|
+
|
|
|
+ fs.rmSync(path.join(testDir, 'docs'), { recursive: true, force: true });
|
|
|
+ __emitWatchEventForTests(testDir, 'docs');
|
|
|
+
|
|
|
+ await waitFor(() => syncFn.mock.calls.length > 0, 5000);
|
|
|
+ // The sync body is async — poll the DB until the removal commits.
|
|
|
+ await waitFor(() => !cg.getFiles().some((f) => f.path.startsWith('docs/')), 5000);
|
|
|
+
|
|
|
+ const after = cg.getFiles().map((f) => f.path);
|
|
|
+ expect(after).toContain('root.ts');
|
|
|
+ expect(after.some((p) => p.startsWith('docs/'))).toBe(false);
|
|
|
+
|
|
|
+ watcher.stop();
|
|
|
+ cg.close();
|
|
|
+ });
|
|
|
+
|
|
|
it('should ignore .codegraph directory changes', async () => {
|
|
|
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
|
|
const watcher = newWatcher(syncFn, { debounceMs: 200 });
|