Files
Dragan Spiridonov 87ff3e96da feat: YAML pipelines, validation pipeline, cross-phase signals, heartbeat scheduler, context sources
- Add YAML deterministic pipeline loader and registry (Imp-9) for token-free workflow execution
- Add validation pipeline skill and MCP handler (BMAD-003) with 13-step requirements validation
- Add cross-phase signal handlers for QCSD feedback loops (store, query, agent_complete, phase events)
- Add heartbeat scheduler worker (Imp-10) for token-free background maintenance every 30 minutes
- Add defect intelligence and requirements traceability context sources
- Register all new MCP tools in protocol-server and server
- Update skills manifests, tests, and documentation

chore(release): bump version to v3.7.19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:17:04 +00:00

293 lines
9.1 KiB
TypeScript

/**
* Agentic QE v3 - Daemon Unit Tests
* ADR-014: Background Workers for QE Monitoring
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
QEDaemon,
createDaemon,
getDaemon,
resetDaemon,
} from '../../../src/workers/daemon';
describe('QEDaemon', () => {
let daemon: QEDaemon;
beforeEach(() => {
vi.useFakeTimers();
resetDaemon();
});
afterEach(async () => {
if (daemon?.running) {
await daemon.stop();
}
vi.useRealTimers();
resetDaemon();
});
describe('createDaemon', () => {
it('should create a daemon with default config', () => {
daemon = createDaemon();
expect(daemon).toBeDefined();
expect(daemon.running).toBe(false);
});
it('should create a daemon with custom config', () => {
daemon = createDaemon({
autoStart: false,
logLevel: 'debug',
});
expect(daemon).toBeDefined();
});
});
describe('getDaemon', () => {
it('should return singleton instance', () => {
const daemon1 = getDaemon();
const daemon2 = getDaemon();
expect(daemon1).toBe(daemon2);
});
it('should create new instance after reset', () => {
const daemon1 = getDaemon();
resetDaemon();
const daemon2 = getDaemon();
expect(daemon1).not.toBe(daemon2);
});
});
describe('start', () => {
it('should start the daemon', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
expect(daemon.running).toBe(true);
});
it('should register all workers by default', async () => {
// Use autoStart: false to just check registration without running workers
// Workers now throw errors when domain APIs are unavailable, so we can't
// actually auto-start them in unit tests without proper domain setup
daemon = createDaemon({ autoStart: false });
await daemon.start();
const manager = daemon.getWorkerManager();
const workers = manager.list();
// All workers should have been registered
expect(workers.length).toBe(11); // All 11 QE workers
});
it('should not auto-start workers when disabled', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
const manager = daemon.getWorkerManager();
const workers = manager.list();
// Workers registered but not started
workers.forEach(worker => {
expect(worker.lastRunAt).toBeUndefined();
});
});
it('should handle multiple start calls gracefully', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
await daemon.start();
expect(daemon.running).toBe(true);
});
});
describe('stop', () => {
it('should stop the daemon', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
await daemon.stop();
expect(daemon.running).toBe(false);
});
it('should handle stop when not running', async () => {
daemon = createDaemon();
await daemon.stop();
expect(daemon.running).toBe(false);
});
});
describe('restart', () => {
it('should restart the daemon', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
await daemon.restart();
expect(daemon.running).toBe(true);
});
});
describe('uptime', () => {
it('should return 0 when not started', () => {
daemon = createDaemon();
expect(daemon.uptime).toBe(0);
});
it('should track uptime', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
vi.advanceTimersByTime(5000);
expect(daemon.uptime).toBe(5);
vi.advanceTimersByTime(10000);
expect(daemon.uptime).toBe(15);
});
it('should reset uptime after restart', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
vi.advanceTimersByTime(5000);
expect(daemon.uptime).toBe(5);
await daemon.restart();
expect(daemon.uptime).toBe(0);
});
});
describe('getStatus', () => {
it('should return status when not running', () => {
daemon = createDaemon();
const status = daemon.getStatus();
expect(status.running).toBe(false);
expect(status.pid).toBe(process.pid);
expect(status.uptime).toBe(0);
expect(status.startedAt).toBeUndefined();
});
it('should return status when running', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
const status = daemon.getStatus();
expect(status.running).toBe(true);
expect(status.startedAt).toBeDefined();
expect(status.workerManager).toBeDefined();
});
it('should include worker manager health', async () => {
daemon = createDaemon({ autoStart: false });
await daemon.start();
const status = daemon.getStatus();
expect(status.workerManager.totalWorkers).toBe(11);
});
});
describe('getWorkerManager', () => {
it('should return the worker manager', () => {
daemon = createDaemon();
const manager = daemon.getWorkerManager();
expect(manager).toBeDefined();
expect(manager.list).toBeDefined();
});
});
describe('runWorker', () => {
it('should attempt to run a specific worker and fail without domain setup', async () => {
// Use real timers for this test since the worker uses async retries
vi.useRealTimers();
daemon = createDaemon({ autoStart: false });
await daemon.start();
// Workers now properly throw errors when domain APIs are unavailable
// instead of silently returning empty metrics
const consoleSpy = vi.spyOn(console, 'warn');
// Run the worker - it will fail after retries
await daemon.runWorker('test-health');
// After all retries fail, the worker should log warnings about the failure
// The warning message contains both the worker name and the error
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringMatching(/\[test-health\].*domain not available/)
);
consoleSpy.mockRestore();
// Restore fake timers for other tests
vi.useFakeTimers();
}, 30000); // Allow 30 seconds for retries
});
describe('enabledWorkers config', () => {
it('should only register enabled workers', () => {
daemon = createDaemon({
enabledWorkers: ['test-health', 'coverage-tracker'],
});
const manager = daemon.getWorkerManager();
const workers = manager.list();
expect(workers).toHaveLength(2);
expect(workers.map(w => w.config.id)).toContain('test-health');
expect(workers.map(w => w.config.id)).toContain('coverage-tracker');
});
});
describe('health checks', () => {
it('should perform periodic health checks', async () => {
const consoleSpy = vi.spyOn(console, 'warn');
daemon = createDaemon({
autoStart: false,
healthCheckIntervalMs: 1000,
});
await daemon.start();
// Advance past health check interval
vi.advanceTimersByTime(2000);
// Health check should have run (no warnings if healthy)
// We just verify it doesn't throw
expect(daemon.running).toBe(true);
consoleSpy.mockRestore();
});
});
});
describe('WORKER_REGISTRY', () => {
it('should export all 11 workers in registry', async () => {
const { WORKER_REGISTRY } = await import('../../../src/workers');
expect(Object.keys(WORKER_REGISTRY)).toHaveLength(11);
expect(WORKER_REGISTRY['test-health']).toBeDefined();
expect(WORKER_REGISTRY['coverage-tracker']).toBeDefined();
expect(WORKER_REGISTRY['flaky-detector']).toBeDefined();
expect(WORKER_REGISTRY['security-scan']).toBeDefined();
expect(WORKER_REGISTRY['quality-gate']).toBeDefined();
expect(WORKER_REGISTRY['learning-consolidation']).toBeDefined();
expect(WORKER_REGISTRY['defect-predictor']).toBeDefined();
expect(WORKER_REGISTRY['regression-monitor']).toBeDefined();
expect(WORKER_REGISTRY['performance-baseline']).toBeDefined();
expect(WORKER_REGISTRY['compliance-checker']).toBeDefined();
expect(WORKER_REGISTRY['heartbeat-scheduler']).toBeDefined();
});
it('should have correct intervals for each worker', async () => {
const { WORKER_REGISTRY } = await import('../../../src/workers');
expect(WORKER_REGISTRY['test-health'].intervalMs).toBe(5 * 60 * 1000);
expect(WORKER_REGISTRY['coverage-tracker'].intervalMs).toBe(10 * 60 * 1000);
expect(WORKER_REGISTRY['flaky-detector'].intervalMs).toBe(15 * 60 * 1000);
expect(WORKER_REGISTRY['security-scan'].intervalMs).toBe(30 * 60 * 1000);
expect(WORKER_REGISTRY['quality-gate'].intervalMs).toBe(5 * 60 * 1000);
expect(WORKER_REGISTRY['learning-consolidation'].intervalMs).toBe(30 * 60 * 1000);
expect(WORKER_REGISTRY['defect-predictor'].intervalMs).toBe(15 * 60 * 1000);
expect(WORKER_REGISTRY['regression-monitor'].intervalMs).toBe(10 * 60 * 1000);
expect(WORKER_REGISTRY['performance-baseline'].intervalMs).toBe(60 * 60 * 1000);
expect(WORKER_REGISTRY['compliance-checker'].intervalMs).toBe(30 * 60 * 1000);
});
});