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>
439 lines
13 KiB
TypeScript
439 lines
13 KiB
TypeScript
/**
|
|
* Tests for A2A Retry Queue
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
RetryQueue,
|
|
createRetryQueue,
|
|
type PendingDelivery,
|
|
type DeliveryAttemptResult,
|
|
DEFAULT_RETRY_CONFIG,
|
|
} from '../../../../../src/adapters/a2a/notifications/retry-queue.js';
|
|
|
|
describe('RetryQueue', () => {
|
|
let queue: RetryQueue;
|
|
|
|
const createDelivery = (overrides?: Partial<PendingDelivery>): PendingDelivery => ({
|
|
id: `delivery-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
subscriptionId: 'sub-123',
|
|
taskId: 'task-123',
|
|
url: 'https://example.com/webhook',
|
|
secret: 'test-secret',
|
|
payload: JSON.stringify({ event: 'task.completed' }),
|
|
originalTimestamp: new Date(),
|
|
attempt: 1,
|
|
scheduledAt: new Date(),
|
|
retryConfig: DEFAULT_RETRY_CONFIG,
|
|
...overrides,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
queue = createRetryQueue({
|
|
enableAutoProcessing: false, // Manual processing for tests
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
queue.destroy();
|
|
});
|
|
|
|
describe('enqueue', () => {
|
|
it('should add delivery to queue', () => {
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
expect(queue.size).toBe(1);
|
|
expect(queue.has(delivery.id)).toBe(true);
|
|
});
|
|
|
|
it('should get delivery by ID', () => {
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
const retrieved = queue.get(delivery.id);
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved!.taskId).toBe(delivery.taskId);
|
|
});
|
|
|
|
it('should emit enqueued event', () => {
|
|
const listener = vi.fn();
|
|
queue.on('enqueued', listener);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
expect(listener).toHaveBeenCalledWith({ delivery });
|
|
});
|
|
|
|
it('should evict oldest when at capacity', () => {
|
|
const queue2 = createRetryQueue({
|
|
maxQueueSize: 2,
|
|
enableAutoProcessing: false,
|
|
});
|
|
|
|
const evictedListener = vi.fn();
|
|
queue2.on('evicted', evictedListener);
|
|
|
|
const delivery1 = createDelivery({ id: 'oldest' });
|
|
const delivery2 = createDelivery({ id: 'middle' });
|
|
const delivery3 = createDelivery({ id: 'newest' });
|
|
|
|
queue2.enqueue(delivery1);
|
|
queue2.enqueue(delivery2);
|
|
queue2.enqueue(delivery3);
|
|
|
|
expect(queue2.size).toBe(2);
|
|
expect(queue2.has('oldest')).toBe(false);
|
|
expect(evictedListener).toHaveBeenCalled();
|
|
|
|
queue2.destroy();
|
|
});
|
|
});
|
|
|
|
describe('enqueueNew', () => {
|
|
it('should create and enqueue a new delivery', () => {
|
|
const delivery = queue.enqueueNew({
|
|
subscriptionId: 'sub-123',
|
|
taskId: 'task-456',
|
|
url: 'https://example.com/webhook',
|
|
secret: 'secret',
|
|
payload: '{"test": true}',
|
|
});
|
|
|
|
expect(delivery.id).toBeDefined();
|
|
expect(delivery.taskId).toBe('task-456');
|
|
expect(delivery.attempt).toBe(1);
|
|
expect(queue.has(delivery.id)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('remove', () => {
|
|
it('should remove delivery from queue', () => {
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
expect(queue.remove(delivery.id)).toBe(true);
|
|
expect(queue.size).toBe(0);
|
|
});
|
|
|
|
it('should return false for non-existent delivery', () => {
|
|
expect(queue.remove('non-existent')).toBe(false);
|
|
});
|
|
|
|
it('should emit removed event', () => {
|
|
const listener = vi.fn();
|
|
queue.on('removed', listener);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
queue.remove(delivery.id);
|
|
|
|
expect(listener).toHaveBeenCalledWith({ id: delivery.id });
|
|
});
|
|
});
|
|
|
|
describe('removeBySubscription', () => {
|
|
it('should remove all deliveries for subscription', () => {
|
|
queue.enqueue(createDelivery({ id: 'd1', subscriptionId: 'sub-1' }));
|
|
queue.enqueue(createDelivery({ id: 'd2', subscriptionId: 'sub-1' }));
|
|
queue.enqueue(createDelivery({ id: 'd3', subscriptionId: 'sub-2' }));
|
|
|
|
const removed = queue.removeBySubscription('sub-1');
|
|
|
|
expect(removed).toBe(2);
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('removeByTask', () => {
|
|
it('should remove all deliveries for task', () => {
|
|
queue.enqueue(createDelivery({ id: 'd1', taskId: 'task-1' }));
|
|
queue.enqueue(createDelivery({ id: 'd2', taskId: 'task-1' }));
|
|
queue.enqueue(createDelivery({ id: 'd3', taskId: 'task-2' }));
|
|
|
|
const removed = queue.removeByTask('task-1');
|
|
|
|
expect(removed).toBe(2);
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('processQueue', () => {
|
|
it('should process ready deliveries', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
statusCode: 200,
|
|
shouldRetry: false,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(mockDeliveryFn).toHaveBeenCalledWith(delivery);
|
|
expect(queue.size).toBe(0); // Removed on success
|
|
});
|
|
|
|
it('should not process future scheduled deliveries', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({ success: true });
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const futureDelivery = createDelivery({
|
|
scheduledAt: new Date(Date.now() + 60000),
|
|
});
|
|
queue.enqueue(futureDelivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(mockDeliveryFn).not.toHaveBeenCalled();
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
|
|
it('should reschedule on failure with retry', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
statusCode: 500,
|
|
shouldRetry: true,
|
|
error: 'Server error',
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery({ attempt: 1 });
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
const updated = queue.get(delivery.id);
|
|
expect(updated).toBeDefined();
|
|
expect(updated!.attempt).toBe(2);
|
|
expect(updated!.lastError).toBe('Server error');
|
|
expect(updated!.scheduledAt.getTime()).toBeGreaterThan(Date.now());
|
|
});
|
|
|
|
it('should remove on max attempts reached', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
shouldRetry: true,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery({
|
|
attempt: 5,
|
|
retryConfig: { ...DEFAULT_RETRY_CONFIG, maxAttempts: 5 },
|
|
});
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(queue.size).toBe(0);
|
|
});
|
|
|
|
it('should emit success event', async () => {
|
|
const successListener = vi.fn();
|
|
queue.on('success', successListener);
|
|
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
statusCode: 200,
|
|
shouldRetry: false,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(successListener).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit failed event when giving up', async () => {
|
|
const failedListener = vi.fn();
|
|
queue.on('failed', failedListener);
|
|
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
shouldRetry: false, // Not retryable
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(failedListener).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit retrying event', async () => {
|
|
const retryingListener = vi.fn();
|
|
queue.on('retrying', retryingListener);
|
|
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
shouldRetry: true,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery();
|
|
queue.enqueue(delivery);
|
|
|
|
await queue.processQueue();
|
|
|
|
expect(retryingListener).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getStats', () => {
|
|
it('should return accurate statistics', async () => {
|
|
const mockDeliveryFn = vi.fn()
|
|
.mockResolvedValueOnce({ success: true, shouldRetry: false })
|
|
.mockResolvedValueOnce({ success: false, shouldRetry: false });
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
queue.enqueue(createDelivery({ id: 'd1' }));
|
|
queue.enqueue(createDelivery({ id: 'd2' }));
|
|
queue.enqueue(createDelivery({
|
|
id: 'd3',
|
|
scheduledAt: new Date(Date.now() + 60000),
|
|
}));
|
|
|
|
await queue.processQueue();
|
|
|
|
const stats = queue.getStats();
|
|
expect(stats.totalItems).toBe(1);
|
|
expect(stats.totalProcessed).toBe(2);
|
|
expect(stats.totalSuccess).toBe(1);
|
|
expect(stats.totalFailed).toBe(1);
|
|
expect(stats.scheduledForLater).toBe(1);
|
|
expect(stats.readyToProcess).toBe(0);
|
|
});
|
|
|
|
it('should track by attempt count', () => {
|
|
queue.enqueue(createDelivery({ attempt: 1 }));
|
|
queue.enqueue(createDelivery({ attempt: 1 }));
|
|
queue.enqueue(createDelivery({ attempt: 2 }));
|
|
queue.enqueue(createDelivery({ attempt: 3 }));
|
|
|
|
const stats = queue.getStats();
|
|
expect(stats.byAttempt[1]).toBe(2);
|
|
expect(stats.byAttempt[2]).toBe(1);
|
|
expect(stats.byAttempt[3]).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('clearFailed', () => {
|
|
it('should remove items at max attempts', () => {
|
|
queue.enqueue(createDelivery({ attempt: 5, retryConfig: { ...DEFAULT_RETRY_CONFIG, maxAttempts: 5 } }));
|
|
queue.enqueue(createDelivery({ attempt: 3, retryConfig: { ...DEFAULT_RETRY_CONFIG, maxAttempts: 5 } }));
|
|
queue.enqueue(createDelivery({ attempt: 5, retryConfig: { ...DEFAULT_RETRY_CONFIG, maxAttempts: 5 } }));
|
|
|
|
const cleared = queue.clearFailed();
|
|
|
|
expect(cleared).toBe(2);
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('exponential backoff', () => {
|
|
it('should calculate correct delays', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
shouldRetry: true,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
// First attempt
|
|
const delivery = createDelivery({
|
|
retryConfig: {
|
|
maxAttempts: 5,
|
|
baseDelayMs: 1000,
|
|
maxDelayMs: 60000,
|
|
backoffMultiplier: 2,
|
|
jitterEnabled: false,
|
|
},
|
|
});
|
|
queue.enqueue(delivery);
|
|
|
|
const beforeProcess = Date.now();
|
|
await queue.processQueue();
|
|
|
|
const updated = queue.get(delivery.id);
|
|
const delay = updated!.scheduledAt.getTime() - beforeProcess;
|
|
|
|
// Should be approximately 1000ms (base delay * 2^0)
|
|
// Allow some tolerance for test timing variations
|
|
expect(delay).toBeGreaterThanOrEqual(900);
|
|
expect(delay).toBeLessThanOrEqual(3000);
|
|
});
|
|
|
|
it('should cap delay at maxDelayMs', async () => {
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({
|
|
success: false,
|
|
shouldRetry: true,
|
|
});
|
|
|
|
queue.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
const delivery = createDelivery({
|
|
attempt: 10, // High attempt number
|
|
retryConfig: {
|
|
maxAttempts: 20,
|
|
baseDelayMs: 1000,
|
|
maxDelayMs: 5000, // Low max
|
|
backoffMultiplier: 2,
|
|
jitterEnabled: false,
|
|
},
|
|
});
|
|
queue.enqueue(delivery);
|
|
|
|
const beforeProcess = Date.now();
|
|
await queue.processQueue();
|
|
|
|
const updated = queue.get(delivery.id);
|
|
const delay = updated!.scheduledAt.getTime() - beforeProcess;
|
|
|
|
// Should be capped at 5000ms
|
|
expect(delay).toBeLessThanOrEqual(5500);
|
|
});
|
|
});
|
|
|
|
describe('auto processing', () => {
|
|
it('should start and stop processing timer', async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const queue2 = createRetryQueue({
|
|
enableAutoProcessing: true,
|
|
processingIntervalMs: 100,
|
|
});
|
|
|
|
const mockDeliveryFn = vi.fn().mockResolvedValue({ success: true });
|
|
queue2.setDeliveryFunction(mockDeliveryFn);
|
|
|
|
queue2.enqueue(createDelivery());
|
|
|
|
// Advance past the processing interval deterministically
|
|
await vi.advanceTimersByTimeAsync(150);
|
|
|
|
expect(mockDeliveryFn).toHaveBeenCalled();
|
|
|
|
queue2.stopProcessing();
|
|
queue2.destroy();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|
|
});
|