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>
880 lines
24 KiB
TypeScript
880 lines
24 KiB
TypeScript
/**
|
|
* AG-UI Backpressure Handler Unit Tests
|
|
*
|
|
* Comprehensive test suite covering backpressure detection, strategies,
|
|
* metrics tracking, and event prioritization.
|
|
*
|
|
* Target: 25+ tests with stress testing
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
BackpressureHandler,
|
|
createBackpressureHandler,
|
|
createBackpressureConsumer,
|
|
isCriticalEventCategory,
|
|
getRecommendedStrategy,
|
|
type BackpressureHandlerConfig,
|
|
type BackpressureStrategy,
|
|
type AGUIEvent,
|
|
AGUIEventType,
|
|
getEventCategory,
|
|
} from '../../../../src/adapters/ag-ui/index.js';
|
|
|
|
// ============================================================================
|
|
// Test Fixtures
|
|
// ============================================================================
|
|
|
|
const createMockEvent = (
|
|
type: AGUIEventType = AGUIEventType.TEXT_MESSAGE_CONTENT,
|
|
overrides: Partial<AGUIEvent> = {}
|
|
): AGUIEvent => ({
|
|
type,
|
|
eventId: `evt_${Math.random().toString(36).slice(2)}`,
|
|
timestamp: new Date().toISOString(),
|
|
messageId: 'msg-123',
|
|
delta: 'test content',
|
|
...overrides,
|
|
} as AGUIEvent);
|
|
|
|
const createLifecycleEvent = (): AGUIEvent =>
|
|
createMockEvent(AGUIEventType.RUN_STARTED, {
|
|
threadId: 'thread-1',
|
|
runId: 'run-1',
|
|
});
|
|
|
|
const createToolEvent = (): AGUIEvent =>
|
|
createMockEvent(AGUIEventType.TOOL_CALL_START, {
|
|
toolCallId: 'tc-1',
|
|
toolCallName: 'test_tool',
|
|
});
|
|
|
|
const createContentEvent = (): AGUIEvent =>
|
|
createMockEvent(AGUIEventType.TEXT_MESSAGE_CONTENT, {
|
|
delta: 'streaming content',
|
|
});
|
|
|
|
// ============================================================================
|
|
// Factory Function Tests
|
|
// ============================================================================
|
|
|
|
describe('createBackpressureHandler', () => {
|
|
it('should create handler with default config', () => {
|
|
const handler = createBackpressureHandler();
|
|
|
|
expect(handler).toBeInstanceOf(BackpressureHandler);
|
|
expect(handler.getBufferSize()).toBe(0);
|
|
expect(handler.isUnderPressure()).toBe(false);
|
|
});
|
|
|
|
it('should create handler with custom maxBufferSize', () => {
|
|
const handler = createBackpressureHandler({ maxBufferSize: 50 });
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.maxBufferSize).toBe(50);
|
|
});
|
|
|
|
it('should create handler with buffer strategy', () => {
|
|
const handler = createBackpressureHandler({ strategy: 'buffer' });
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.strategy).toBe('buffer');
|
|
});
|
|
|
|
it('should create handler with drop-non-critical strategy', () => {
|
|
const handler = createBackpressureHandler({ strategy: 'drop-non-critical' });
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.strategy).toBe('drop-non-critical');
|
|
});
|
|
|
|
it('should create handler with pause-producer strategy', () => {
|
|
const handler = createBackpressureHandler({ strategy: 'pause-producer' });
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.strategy).toBe('pause-producer');
|
|
});
|
|
|
|
it('should create handler with callbacks', () => {
|
|
const onPressure = vi.fn();
|
|
const onRelieve = vi.fn();
|
|
const handler = createBackpressureHandler({ onPressure, onRelieve });
|
|
|
|
expect(handler).toBeInstanceOf(BackpressureHandler);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Buffer Strategy Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler - Buffer Strategy', () => {
|
|
let handler: BackpressureHandler;
|
|
|
|
beforeEach(() => {
|
|
handler = createBackpressureHandler({
|
|
maxBufferSize: 5,
|
|
strategy: 'buffer',
|
|
highWatermark: 80,
|
|
lowWatermark: 50,
|
|
});
|
|
});
|
|
|
|
it('should accept events under buffer limit', () => {
|
|
const result = handler.push(createContentEvent());
|
|
|
|
expect(result).toBe(true);
|
|
expect(handler.getBufferSize()).toBe(1);
|
|
});
|
|
|
|
it('should buffer multiple events', () => {
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(handler.getBufferSize()).toBe(4);
|
|
});
|
|
|
|
it('should reject non-critical events when buffer is full', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Try to push one more non-critical event
|
|
const result = handler.push(createContentEvent());
|
|
|
|
expect(result).toBe(false);
|
|
expect(handler.getBufferSize()).toBe(5);
|
|
});
|
|
|
|
it('should accept critical events when buffer is full', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Push critical event
|
|
const result = handler.push(createLifecycleEvent());
|
|
|
|
expect(result).toBe(true);
|
|
expect(handler.getBufferSize()).toBe(6);
|
|
});
|
|
|
|
it('should track dropped events', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Try to push more
|
|
handler.push(createContentEvent());
|
|
handler.push(createContentEvent());
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.droppedCount).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Drop Non-Critical Strategy Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler - Drop Non-Critical Strategy', () => {
|
|
let handler: BackpressureHandler;
|
|
|
|
beforeEach(() => {
|
|
handler = createBackpressureHandler({
|
|
maxBufferSize: 3,
|
|
strategy: 'drop-non-critical',
|
|
});
|
|
});
|
|
|
|
it('should keep lifecycle events under pressure', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Push lifecycle event
|
|
const result = handler.push(createLifecycleEvent());
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should drop TEXT_MESSAGE_CONTENT under pressure', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createLifecycleEvent());
|
|
}
|
|
|
|
// Push content event
|
|
const result = handler.push(createContentEvent());
|
|
|
|
expect(result).toBe(false);
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.droppedCount).toBe(1);
|
|
});
|
|
|
|
it('should drop TOOL_CALL_ARGS under pressure', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createLifecycleEvent());
|
|
}
|
|
|
|
// Push args event
|
|
const argsEvent = createMockEvent(AGUIEventType.TOOL_CALL_ARGS, {
|
|
toolCallId: 'tc-1',
|
|
delta: '{"key":',
|
|
});
|
|
const result = handler.push(argsEvent);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should keep TOOL_CALL_START under pressure', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Push tool start event
|
|
const result = handler.push(createToolEvent());
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should keep STATE_SNAPSHOT under pressure', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Push snapshot event
|
|
const snapshotEvent = createMockEvent(AGUIEventType.STATE_SNAPSHOT, {
|
|
state: { key: 'value' },
|
|
});
|
|
const result = handler.push(snapshotEvent);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should track dropped events by type', () => {
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createLifecycleEvent());
|
|
}
|
|
|
|
// Drop various event types
|
|
handler.push(createContentEvent());
|
|
handler.push(createContentEvent());
|
|
handler.push(createMockEvent(AGUIEventType.TOOL_CALL_ARGS));
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.droppedByType[AGUIEventType.TEXT_MESSAGE_CONTENT]).toBe(2);
|
|
expect(metrics.droppedByType[AGUIEventType.TOOL_CALL_ARGS]).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Pause Producer Strategy Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler - Pause Producer Strategy', () => {
|
|
let handler: BackpressureHandler;
|
|
|
|
beforeEach(() => {
|
|
handler = createBackpressureHandler({
|
|
maxBufferSize: 3,
|
|
strategy: 'pause-producer',
|
|
});
|
|
});
|
|
|
|
it('should signal pause when buffer is full', () => {
|
|
const pauseSpy = vi.fn();
|
|
handler.on('pause', pauseSpy);
|
|
|
|
// Fill buffer
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Push one more to trigger pause
|
|
handler.push(createContentEvent());
|
|
|
|
expect(handler.isProducerPaused()).toBe(true);
|
|
expect(pauseSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should track pause count', () => {
|
|
// Fill and trigger pause
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.pauseCount).toBe(1);
|
|
});
|
|
|
|
it('should resume producer when drained', () => {
|
|
const resumeSpy = vi.fn();
|
|
handler.on('resume', resumeSpy);
|
|
|
|
// Fill and trigger pause
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Drain buffer
|
|
handler.pull();
|
|
handler.resumeProducer();
|
|
|
|
expect(handler.isProducerPaused()).toBe(false);
|
|
expect(resumeSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should track total pause time', async () => {
|
|
// Fill and trigger pause
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
// Resume
|
|
handler.resumeProducer();
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.totalPauseTimeMs).toBeGreaterThanOrEqual(45);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Watermark Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Watermarks', () => {
|
|
it('should enter pressure state at high watermark', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 10,
|
|
highWatermark: 80,
|
|
});
|
|
|
|
// Push to 80% capacity (8 events)
|
|
for (let i = 0; i < 8; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(handler.isUnderPressure()).toBe(true);
|
|
});
|
|
|
|
it('should exit pressure state at low watermark', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 10,
|
|
highWatermark: 80,
|
|
lowWatermark: 50,
|
|
});
|
|
|
|
// Fill to high watermark (80% of 10 = 8 events)
|
|
for (let i = 0; i < 8; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
expect(handler.isUnderPressure()).toBe(true);
|
|
|
|
// Pull 2 to leave 6 events (60%, still above 50% low watermark)
|
|
handler.pull(2);
|
|
expect(handler.isUnderPressure()).toBe(true); // 60% > 50%
|
|
|
|
// Pull 2 more to leave 4 events (40%, below 50% low watermark)
|
|
handler.pull(2);
|
|
expect(handler.isUnderPressure()).toBe(false); // 40% <= 50%
|
|
});
|
|
|
|
it('should emit pressure event', () => {
|
|
const onPressure = vi.fn();
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 5,
|
|
highWatermark: 80,
|
|
onPressure,
|
|
});
|
|
|
|
// Push to 80% capacity - with 5 max, 80% = 4 events needed
|
|
// After pushing 4 events: 4/5 = 80%, should trigger pressure
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(onPressure).toHaveBeenCalled();
|
|
expect(onPressure.mock.calls[0][0].underPressure).toBe(true);
|
|
});
|
|
|
|
it('should emit relieve event', () => {
|
|
const onRelieve = vi.fn();
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 10,
|
|
highWatermark: 80,
|
|
lowWatermark: 50,
|
|
onRelieve,
|
|
});
|
|
|
|
// Enter pressure state - need 80% = 8 events to trigger
|
|
for (let i = 0; i < 8; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
expect(handler.isUnderPressure()).toBe(true);
|
|
|
|
// Exit pressure state - need to get to <= 50% = 5 events
|
|
// Pull 4 to leave 4 events (40%)
|
|
handler.pull(4);
|
|
|
|
expect(onRelieve).toHaveBeenCalled();
|
|
expect(onRelieve.mock.calls[0][0].underPressure).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Memory Limit Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Memory Limits', () => {
|
|
it('should respect memory limit', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 1000,
|
|
maxMemoryBytes: 100, // Very small limit for testing
|
|
strategy: 'buffer',
|
|
});
|
|
|
|
// Push events until memory limit is hit
|
|
let accepted = 0;
|
|
for (let i = 0; i < 10; i++) {
|
|
if (handler.push(createContentEvent())) {
|
|
accepted++;
|
|
}
|
|
}
|
|
|
|
expect(accepted).toBeLessThan(10);
|
|
expect(handler.getMemoryUsage()).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
it('should track memory usage', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 100,
|
|
maxMemoryBytes: 10 * 1024 * 1024, // 10MB
|
|
});
|
|
|
|
handler.push(createContentEvent());
|
|
|
|
expect(handler.getMemoryUsage()).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Pull API Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Pull API', () => {
|
|
let handler: BackpressureHandler;
|
|
|
|
beforeEach(() => {
|
|
handler = createBackpressureHandler({ maxBufferSize: 10 });
|
|
});
|
|
|
|
it('should pull all events by default', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
const events = handler.pull();
|
|
|
|
expect(events).toHaveLength(5);
|
|
expect(handler.getBufferSize()).toBe(0);
|
|
});
|
|
|
|
it('should pull specified count', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
const events = handler.pull(3);
|
|
|
|
expect(events).toHaveLength(3);
|
|
expect(handler.getBufferSize()).toBe(2);
|
|
});
|
|
|
|
it('should pull one event with pullOne', () => {
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
const event = handler.pullOne();
|
|
|
|
expect(event).toBeDefined();
|
|
expect(handler.getBufferSize()).toBe(2);
|
|
});
|
|
|
|
it('should return undefined when buffer empty', () => {
|
|
const event = handler.pullOne();
|
|
|
|
expect(event).toBeUndefined();
|
|
});
|
|
|
|
it('should reduce memory usage on pull', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
const initialMemory = handler.getMemoryUsage();
|
|
|
|
handler.pull(3);
|
|
|
|
expect(handler.getMemoryUsage()).toBeLessThan(initialMemory);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Clear and State Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler State Management', () => {
|
|
it('should clear buffer', () => {
|
|
const handler = createBackpressureHandler({ maxBufferSize: 10 });
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
handler.clear();
|
|
|
|
expect(handler.getBufferSize()).toBe(0);
|
|
expect(handler.getMemoryUsage()).toBe(0);
|
|
});
|
|
|
|
it('should exit pressure state on clear', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 5,
|
|
highWatermark: 80,
|
|
});
|
|
|
|
// Enter pressure state - 80% of 5 = 4 events
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
// Verify we're at 80% utilization
|
|
expect(handler.getMetrics().utilizationPercent).toBe(80);
|
|
expect(handler.isUnderPressure()).toBe(true);
|
|
|
|
handler.clear();
|
|
|
|
expect(handler.isUnderPressure()).toBe(false);
|
|
});
|
|
|
|
it('should check hasEvents correctly', () => {
|
|
const handler = createBackpressureHandler();
|
|
|
|
expect(handler.hasEvents()).toBe(false);
|
|
|
|
handler.push(createContentEvent());
|
|
|
|
expect(handler.hasEvents()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Metrics Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Metrics', () => {
|
|
it('should track processed count', () => {
|
|
const handler = createBackpressureHandler();
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(handler.getMetrics().processedCount).toBe(10);
|
|
});
|
|
|
|
it('should calculate utilization percent', () => {
|
|
const handler = createBackpressureHandler({ maxBufferSize: 10 });
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(handler.getMetrics().utilizationPercent).toBe(50);
|
|
});
|
|
|
|
it('should include current state in metrics', () => {
|
|
const handler = createBackpressureHandler({ maxBufferSize: 5 });
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.bufferSize).toBe(3);
|
|
expect(metrics.memoryUsageBytes).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Event Priority Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Event Priority', () => {
|
|
it('should use custom critical events', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 2,
|
|
strategy: 'drop-non-critical',
|
|
eventPriority: {
|
|
critical: [AGUIEventType.CUSTOM],
|
|
},
|
|
});
|
|
|
|
// Fill buffer
|
|
for (let i = 0; i < 2; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Custom event should be accepted
|
|
const customEvent = createMockEvent(AGUIEventType.CUSTOM, {
|
|
name: 'test',
|
|
value: {},
|
|
});
|
|
const result = handler.push(customEvent);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should use custom droppable events', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 2,
|
|
strategy: 'drop-non-critical',
|
|
eventPriority: {
|
|
droppable: [AGUIEventType.RAW],
|
|
},
|
|
});
|
|
|
|
// Fill buffer
|
|
for (let i = 0; i < 2; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// RAW event should be dropped
|
|
const rawEvent = createMockEvent(AGUIEventType.RAW, {
|
|
event: {},
|
|
});
|
|
const result = handler.push(rawEvent);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Event Emission Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Events', () => {
|
|
it('should emit drop event', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 2,
|
|
strategy: 'drop-non-critical',
|
|
});
|
|
const dropSpy = vi.fn();
|
|
handler.on('drop', dropSpy);
|
|
|
|
// Fill and trigger drop
|
|
for (let i = 0; i < 3; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(dropSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit pressure event', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 5,
|
|
highWatermark: 80,
|
|
});
|
|
const pressureSpy = vi.fn();
|
|
handler.on('pressure', pressureSpy);
|
|
|
|
// Trigger pressure - 80% of 5 = 4 events needed
|
|
for (let i = 0; i < 4; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
expect(pressureSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit relieve event', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 10,
|
|
highWatermark: 80,
|
|
lowWatermark: 50,
|
|
});
|
|
const relieveSpy = vi.fn();
|
|
handler.on('relieve', relieveSpy);
|
|
|
|
// Enter pressure - need 80% = 8 events
|
|
for (let i = 0; i < 8; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
expect(handler.isUnderPressure()).toBe(true);
|
|
|
|
// Exit pressure - pull 4 to leave 4 (40% < 50%)
|
|
handler.pull(4);
|
|
|
|
expect(relieveSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Utility Function Tests
|
|
// ============================================================================
|
|
|
|
describe('isCriticalEventCategory', () => {
|
|
it('should return true for lifecycle category', () => {
|
|
expect(isCriticalEventCategory('lifecycle')).toBe(true);
|
|
});
|
|
|
|
it('should return true for tool category', () => {
|
|
expect(isCriticalEventCategory('tool')).toBe(true);
|
|
});
|
|
|
|
it('should return false for text category', () => {
|
|
expect(isCriticalEventCategory('text')).toBe(false);
|
|
});
|
|
|
|
it('should return false for state category', () => {
|
|
expect(isCriticalEventCategory('state')).toBe(false);
|
|
});
|
|
|
|
it('should return false for special category', () => {
|
|
expect(isCriticalEventCategory('special')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getRecommendedStrategy', () => {
|
|
it('should return drop-non-critical for realtime', () => {
|
|
expect(getRecommendedStrategy('realtime')).toBe('drop-non-critical');
|
|
});
|
|
|
|
it('should return pause-producer for reliable', () => {
|
|
expect(getRecommendedStrategy('reliable')).toBe('pause-producer');
|
|
});
|
|
|
|
it('should return buffer for balanced', () => {
|
|
expect(getRecommendedStrategy('balanced')).toBe('buffer');
|
|
});
|
|
});
|
|
|
|
describe('createBackpressureConsumer', () => {
|
|
it('should create consumer that processes events', async () => {
|
|
const handler = createBackpressureHandler();
|
|
const processedEvents: AGUIEvent[] = [];
|
|
|
|
const consumer = createBackpressureConsumer(
|
|
handler,
|
|
(event) => {
|
|
processedEvents.push(event);
|
|
},
|
|
{ pullInterval: 10, batchSize: 5 }
|
|
);
|
|
|
|
// Push events
|
|
for (let i = 0; i < 10; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
|
|
// Start consumer
|
|
consumer.start();
|
|
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
consumer.stop();
|
|
|
|
expect(processedEvents.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should track running state', () => {
|
|
const handler = createBackpressureHandler();
|
|
const consumer = createBackpressureConsumer(handler, () => {});
|
|
|
|
expect(consumer.isRunning()).toBe(false);
|
|
|
|
consumer.start();
|
|
expect(consumer.isRunning()).toBe(true);
|
|
|
|
consumer.stop();
|
|
expect(consumer.isRunning()).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Stress Tests
|
|
// ============================================================================
|
|
|
|
describe('BackpressureHandler Stress Tests', () => {
|
|
it('should handle high throughput (10000 events)', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 1000,
|
|
strategy: 'buffer',
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < 10000; i++) {
|
|
handler.push(createContentEvent());
|
|
if (handler.getBufferSize() > 500) {
|
|
handler.pull(100);
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(duration).toBeLessThan(1000); // Should complete in under 1 second
|
|
expect(handler.getMetrics().processedCount).toBe(10000);
|
|
});
|
|
|
|
it('should handle rapid push/pull cycles', () => {
|
|
const handler = createBackpressureHandler({ maxBufferSize: 100 });
|
|
|
|
for (let cycle = 0; cycle < 100; cycle++) {
|
|
// Push batch
|
|
for (let i = 0; i < 50; i++) {
|
|
handler.push(createContentEvent());
|
|
}
|
|
// Pull batch
|
|
handler.pull(40);
|
|
}
|
|
|
|
expect(handler.getBufferSize()).toBeGreaterThan(0);
|
|
expect(handler.getMetrics().processedCount).toBe(5000);
|
|
});
|
|
|
|
it('should maintain consistency under pressure', () => {
|
|
const handler = createBackpressureHandler({
|
|
maxBufferSize: 10,
|
|
strategy: 'drop-non-critical',
|
|
highWatermark: 80,
|
|
lowWatermark: 50,
|
|
});
|
|
|
|
let dropped = 0;
|
|
handler.on('drop', () => dropped++);
|
|
|
|
// Rapid push with occasional pulls
|
|
for (let i = 0; i < 1000; i++) {
|
|
handler.push(createContentEvent());
|
|
if (i % 10 === 0) {
|
|
handler.pull(3);
|
|
}
|
|
}
|
|
|
|
const metrics = handler.getMetrics();
|
|
expect(metrics.processedCount).toBe(1000);
|
|
expect(metrics.droppedCount).toBe(dropped);
|
|
expect(metrics.droppedCount + handler.getBufferSize()).toBeLessThanOrEqual(1000);
|
|
});
|
|
});
|