mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
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>
346 lines
9.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|