Files
Dragan Spiridonov e82a7240ec feat: implement CC-internals improvements IMP-00 through IMP-10 (Waves 0-4)
11 improvements across 4 priority tiers, all integrated into production
code paths with 416 new tests across 33 test files. Each wave passed
a brutal honesty audit with all findings resolved.

Wave 0-2 (P0/P1): Middleware chain, microcompact, tool concurrency,
retry engine, session durability, prompt cache latch, startup fast
paths, hook security hardening.

Wave 3 (P2): 4-tier context compaction pipeline, plugin architecture
with local/GitHub/npm sources.

Wave 4 (P3): QE Quality Daemon — priority queue, git watcher (with
Linux polling fallback), coverage delta analyzer, CI monitor, test
suggester, nightly consolidation, notification service with SSRF guard,
persistent SQLite-backed memory, CLI commands, MCP startup integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:32:50 +00:00

180 lines
5.6 KiB
TypeScript

/**
* Tests for the QE Quality Daemon Git Watcher (IMP-10).
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { PriorityQueue } from '../../../src/workers/quality-daemon/priority-queue';
import { GitWatcher } from '../../../src/workers/quality-daemon/git-watcher';
import type { DaemonTaskPayload, GitCommitPayload } from '../../../src/workers/quality-daemon/priority-queue';
const mockExecFile = vi.fn();
// Mock child_process.execFile
vi.mock('child_process', () => ({
execFile: (...args: unknown[]) => mockExecFile(...args),
}));
// Mock fs.watch
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
watch: vi.fn(() => ({
close: vi.fn(),
on: vi.fn(),
})),
existsSync: vi.fn(() => true),
readFileSync: vi.fn(() => 'abc123\n'),
};
});
function setupGitMock(branches: Array<{ name: string; hash: string }>): void {
const output = branches.map(b => `${b.name} ${b.hash}`).join('\n');
mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, cb?: Function) => {
if (cb) {
if (args.includes('for-each-ref')) {
cb(null, { stdout: output, stderr: '' });
} else if (args.includes('diff-tree')) {
cb(null, { stdout: 'src/foo.ts\nsrc/bar.ts\n', stderr: '' });
} else {
cb(null, { stdout: '', stderr: '' });
}
}
});
}
describe('GitWatcher', () => {
let queue: PriorityQueue;
let watcher: GitWatcher;
beforeEach(() => {
queue = new PriorityQueue();
watcher = new GitWatcher(queue, {
repoRoot: '/tmp/fake-repo',
debounceMs: 10,
});
vi.clearAllMocks();
});
afterEach(() => {
watcher.stop();
vi.restoreAllMocks();
});
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
it('starts and sets running to true', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
expect(watcher.running).toBe(false);
await watcher.start();
expect(watcher.running).toBe(true);
});
it('stop sets running to false and clears timers', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
await watcher.start();
watcher.stop();
expect(watcher.running).toBe(false);
});
it('does not error on double start', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
await watcher.start();
await watcher.start();
expect(watcher.running).toBe(true);
});
// -------------------------------------------------------------------------
// poll() — commit detection (core feature)
// -------------------------------------------------------------------------
it('poll detects branch hash change and enqueues item', async () => {
// First poll: establish baseline with hash aaa111
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
let changed = await watcher.poll();
expect(changed).toEqual([]);
expect(queue.isEmpty).toBe(true);
// Second poll: hash changed to bbb222
setupGitMock([{ name: 'main', hash: 'bbb222' }]);
changed = await watcher.poll();
expect(changed).toEqual(['main']);
expect(queue.size).toBe(1);
const item = queue.dequeue()!;
expect(item.priority).toBe('next');
expect(item.source).toBe('git-watcher');
const payload = item.payload as GitCommitPayload;
expect(payload.type).toBe('git_commit');
expect(payload.branch).toBe('main');
expect(payload.commitHash).toBe('bbb222');
expect(payload.changedFiles).toEqual(['src/foo.ts', 'src/bar.ts']);
});
it('poll detects multiple branch changes', async () => {
setupGitMock([
{ name: 'main', hash: 'aaa111' },
{ name: 'feature', hash: 'ccc333' },
]);
await watcher.poll(); // baseline
setupGitMock([
{ name: 'main', hash: 'bbb222' },
{ name: 'feature', hash: 'ddd444' },
]);
const changed = await watcher.poll();
expect(changed).toEqual(['main', 'feature']);
expect(queue.size).toBe(2);
});
it('poll does not enqueue when hash is unchanged', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
await watcher.poll();
// Same hash on second poll
const changed = await watcher.poll();
expect(changed).toEqual([]);
expect(queue.isEmpty).toBe(true);
});
it('poll returns empty array when git is unavailable', async () => {
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, cb?: Function) => {
if (cb) cb(new Error('git: command not found'), { stdout: '', stderr: '' });
});
const result = await watcher.poll();
expect(result).toEqual([]);
});
it('poll guards against concurrent execution', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
// Start two polls simultaneously — second should be skipped
const [result1, result2] = await Promise.all([
watcher.poll(),
watcher.poll(),
]);
// One should have run, the other should be empty (guard)
expect([...result1, ...result2]).toEqual([]);
});
// -------------------------------------------------------------------------
// TTL on enqueued items
// -------------------------------------------------------------------------
it('enqueued items have 5-minute TTL', async () => {
setupGitMock([{ name: 'main', hash: 'aaa111' }]);
await watcher.poll();
setupGitMock([{ name: 'main', hash: 'bbb222' }]);
await watcher.poll();
const item = queue.dequeue()!;
expect(item.ttlMs).toBe(5 * 60 * 1000);
});
});