Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

346 lines
9.2 KiB
TypeScript

/**
* Agentic QE v3 - Base Worker Unit Tests
* ADR-014: Background Workers for QE Monitoring
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { BaseWorker } from '../../../src/workers/base-worker';
import {
WorkerConfig,
WorkerContext,
WorkerResult,
WorkerMetrics,
WorkerFinding,
WorkerRecommendation,
} from '../../../src/workers/interfaces';
// Test implementation of BaseWorker
class TestWorker extends BaseWorker {
public executionCount = 0;
public shouldFail = false;
public executionDelay = 0;
constructor(config?: Partial<WorkerConfig>) {
super({
id: 'test-worker',
name: 'Test Worker',
description: 'A test worker for unit testing',
intervalMs: 60000,
priority: 'normal',
targetDomains: ['test-execution'],
enabled: true,
timeoutMs: 5000,
retryCount: 2,
retryDelayMs: 100,
...config,
});
}
protected async doExecute(context: WorkerContext): Promise<WorkerResult> {
this.executionCount++;
if (this.executionDelay > 0) {
await new Promise(resolve => setTimeout(resolve, this.executionDelay));
}
if (this.shouldFail) {
throw new Error('Test execution failure');
}
const metrics: WorkerMetrics = {
itemsAnalyzed: 10,
issuesFound: 2,
healthScore: 85,
trend: 'stable',
domainMetrics: { testMetric: 'value' },
};
const findings: WorkerFinding[] = [
{
type: 'test-finding',
severity: 'medium',
domain: 'test-execution',
title: 'Test Finding',
description: 'A test finding for unit testing',
},
];
const recommendations: WorkerRecommendation[] = [
{
priority: 'p2',
domain: 'test-execution',
action: 'Test Action',
description: 'A test recommendation',
estimatedImpact: 'medium',
effort: 'low',
autoFixable: true,
},
];
return this.createResult(100, metrics, findings, recommendations);
}
}
function createMockContext(): WorkerContext {
return {
eventBus: {
publish: vi.fn().mockResolvedValue(undefined),
},
memory: {
get: vi.fn().mockResolvedValue(undefined),
set: vi.fn().mockResolvedValue(undefined),
search: vi.fn().mockResolvedValue([]),
},
logger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
domains: {
getDomainAPI: vi.fn().mockReturnValue(undefined),
getDomainHealth: vi.fn().mockReturnValue({ status: 'healthy', errors: [] }),
},
signal: new AbortController().signal,
};
}
describe('BaseWorker', () => {
let worker: TestWorker;
let context: WorkerContext;
beforeEach(() => {
worker = new TestWorker();
context = createMockContext();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('initialization', () => {
it('should have correct initial status', () => {
expect(worker.status).toBe('idle');
expect(worker.lastResult).toBeUndefined();
expect(worker.lastRunAt).toBeUndefined();
});
it('should initialize successfully', async () => {
await worker.initialize();
expect(worker.status).toBe('idle');
expect(worker.nextRunAt).toBeDefined();
});
it('should have correct config values', () => {
expect(worker.config.id).toBe('test-worker');
expect(worker.config.priority).toBe('normal');
expect(worker.config.enabled).toBe(true);
});
});
describe('execute', () => {
it('should execute successfully', async () => {
const result = await worker.execute(context);
expect(result.success).toBe(true);
expect(result.workerId).toBe('test-worker');
expect(result.metrics.itemsAnalyzed).toBe(10);
expect(result.findings).toHaveLength(1);
expect(result.recommendations).toHaveLength(1);
});
it('should track execution count', async () => {
await worker.execute(context);
expect(worker.executionCount).toBe(1);
await worker.execute(context);
expect(worker.executionCount).toBe(2);
});
it('should update lastResult after execution', async () => {
await worker.execute(context);
expect(worker.lastResult).toBeDefined();
expect(worker.lastResult?.success).toBe(true);
});
it('should update lastRunAt after execution', async () => {
const before = Date.now();
await worker.execute(context);
const after = Date.now();
expect(worker.lastRunAt).toBeDefined();
expect(worker.lastRunAt!.getTime()).toBeGreaterThanOrEqual(before);
expect(worker.lastRunAt!.getTime()).toBeLessThanOrEqual(after);
});
it('should schedule next run after execution', async () => {
await worker.execute(context);
expect(worker.nextRunAt).toBeDefined();
expect(worker.nextRunAt!.getTime()).toBeGreaterThan(Date.now());
});
it('should publish event after execution', async () => {
await worker.execute(context);
expect(context.eventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
type: 'worker.executed',
workerId: 'test-worker',
})
);
});
});
describe('failure handling', () => {
it('should handle execution failure', async () => {
worker.shouldFail = true;
const result = await worker.execute(context);
expect(result.success).toBe(false);
expect(result.error).toBe('Test execution failure');
});
it('should retry on failure', async () => {
let attemptCount = 0;
worker.shouldFail = true;
// Override to track attempts
const originalDoExecute = worker['doExecute'].bind(worker);
worker['doExecute'] = async (ctx: WorkerContext) => {
attemptCount++;
return originalDoExecute(ctx);
};
await worker.execute(context);
// Initial attempt + 2 retries = 3 attempts
expect(attemptCount).toBe(3);
});
it('should log warnings on retry', async () => {
worker.shouldFail = true;
await worker.execute(context);
expect(context.logger.warn).toHaveBeenCalled();
});
it('should set status to error on failure', async () => {
worker.shouldFail = true;
await worker.execute(context);
expect(worker.status).toBe('error');
});
});
describe('timeout handling', () => {
it('should timeout long-running executions', async () => {
worker = new TestWorker({ timeoutMs: 50 });
worker.executionDelay = 200;
const result = await worker.execute(context);
expect(result.success).toBe(false);
expect(result.error).toContain('timed out');
});
});
describe('abort handling', () => {
it('should handle abort signal', async () => {
const abortController = new AbortController();
context = {
...context,
signal: abortController.signal,
};
worker.executionDelay = 100;
const executePromise = worker.execute(context);
setTimeout(() => abortController.abort(), 20);
const result = await executePromise;
expect(result.success).toBe(false);
expect(result.error).toContain('aborted');
});
});
describe('pause/resume', () => {
it('should pause worker', () => {
worker.pause();
expect(worker.status).toBe('paused');
});
it('should resume worker', () => {
worker.pause();
worker.resume();
expect(worker.status).toBe('idle');
});
it('should not change status if stopped', async () => {
await worker.stop();
worker.pause();
expect(worker.status).toBe('stopped');
});
});
describe('stop', () => {
it('should stop worker', async () => {
await worker.stop();
expect(worker.status).toBe('stopped');
});
});
describe('health tracking', () => {
it('should track health correctly after success', async () => {
await worker.execute(context);
const health = worker.getHealth();
expect(health.status).toBe('idle');
expect(health.totalExecutions).toBe(1);
expect(health.successfulExecutions).toBe(1);
expect(health.failedExecutions).toBe(0);
expect(health.healthScore).toBe(100);
});
it('should track health correctly after failure', async () => {
worker.shouldFail = true;
await worker.execute(context);
const health = worker.getHealth();
expect(health.status).toBe('error');
expect(health.totalExecutions).toBe(1);
expect(health.successfulExecutions).toBe(0);
expect(health.failedExecutions).toBe(1);
expect(health.healthScore).toBe(0);
});
it('should track recent results', async () => {
await worker.execute(context);
await worker.execute(context);
const health = worker.getHealth();
expect(health.recentResults).toHaveLength(2);
expect(health.recentResults[0].success).toBe(true);
});
it('should calculate average duration', async () => {
await worker.execute(context);
await worker.execute(context);
const health = worker.getHealth();
// Duration is tracked even if very fast (0ms is valid)
expect(health.avgDurationMs).toBeGreaterThanOrEqual(0);
expect(health.recentResults.length).toBe(2);
});
});
});