Standalone npm package (@mosaicstack/telemetry-client) for reporting task-completion telemetry and querying predictions from the Mosaic Stack Telemetry server. - TelemetryClient with setInterval-based background flush - EventQueue (bounded FIFO array) - BatchSubmitter with native fetch, exponential backoff, Retry-After - PredictionCache (Map + TTL) - EventBuilder with auto-generated event_id/timestamp - Zero runtime dependencies (Node 18+ native APIs) - 43 tests, 86% branch coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { EventQueue } from '../src/queue.js';
|
|
import {
|
|
TaskType,
|
|
Complexity,
|
|
Harness,
|
|
Provider,
|
|
Outcome,
|
|
TaskCompletionEvent,
|
|
} from '../src/types/events.js';
|
|
|
|
function makeEvent(id: string): TaskCompletionEvent {
|
|
return {
|
|
instance_id: 'test-instance',
|
|
event_id: id,
|
|
schema_version: '1.0',
|
|
timestamp: new Date().toISOString(),
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.IMPLEMENTATION,
|
|
complexity: Complexity.MEDIUM,
|
|
harness: Harness.CLAUDE_CODE,
|
|
model: 'claude-3-opus',
|
|
provider: Provider.ANTHROPIC,
|
|
estimated_input_tokens: 1000,
|
|
estimated_output_tokens: 500,
|
|
actual_input_tokens: 1100,
|
|
actual_output_tokens: 550,
|
|
estimated_cost_usd_micros: 50000,
|
|
actual_cost_usd_micros: 55000,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [],
|
|
quality_gates_failed: [],
|
|
context_compactions: 0,
|
|
context_rotations: 0,
|
|
context_utilization_final: 0.5,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
};
|
|
}
|
|
|
|
describe('EventQueue', () => {
|
|
it('should enqueue and drain events', () => {
|
|
const queue = new EventQueue(10);
|
|
const event = makeEvent('e1');
|
|
|
|
queue.enqueue(event);
|
|
expect(queue.size).toBe(1);
|
|
expect(queue.isEmpty).toBe(false);
|
|
|
|
const drained = queue.drain(10);
|
|
expect(drained).toHaveLength(1);
|
|
expect(drained[0].event_id).toBe('e1');
|
|
expect(queue.isEmpty).toBe(true);
|
|
});
|
|
|
|
it('should respect maxSize with FIFO eviction', () => {
|
|
const queue = new EventQueue(3);
|
|
|
|
queue.enqueue(makeEvent('e1'));
|
|
queue.enqueue(makeEvent('e2'));
|
|
queue.enqueue(makeEvent('e3'));
|
|
expect(queue.size).toBe(3);
|
|
|
|
// Adding a 4th should evict the oldest (e1)
|
|
queue.enqueue(makeEvent('e4'));
|
|
expect(queue.size).toBe(3);
|
|
|
|
const drained = queue.drain(10);
|
|
expect(drained.map((e) => e.event_id)).toEqual(['e2', 'e3', 'e4']);
|
|
});
|
|
|
|
it('should drain up to maxItems', () => {
|
|
const queue = new EventQueue(10);
|
|
queue.enqueue(makeEvent('e1'));
|
|
queue.enqueue(makeEvent('e2'));
|
|
queue.enqueue(makeEvent('e3'));
|
|
|
|
const drained = queue.drain(2);
|
|
expect(drained).toHaveLength(2);
|
|
expect(drained.map((e) => e.event_id)).toEqual(['e1', 'e2']);
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
|
|
it('should remove drained items from the queue', () => {
|
|
const queue = new EventQueue(10);
|
|
queue.enqueue(makeEvent('e1'));
|
|
queue.enqueue(makeEvent('e2'));
|
|
|
|
queue.drain(1);
|
|
expect(queue.size).toBe(1);
|
|
|
|
const remaining = queue.drain(10);
|
|
expect(remaining[0].event_id).toBe('e2');
|
|
});
|
|
|
|
it('should report isEmpty correctly', () => {
|
|
const queue = new EventQueue(5);
|
|
expect(queue.isEmpty).toBe(true);
|
|
|
|
queue.enqueue(makeEvent('e1'));
|
|
expect(queue.isEmpty).toBe(false);
|
|
|
|
queue.drain(1);
|
|
expect(queue.isEmpty).toBe(true);
|
|
});
|
|
|
|
it('should report size correctly', () => {
|
|
const queue = new EventQueue(10);
|
|
expect(queue.size).toBe(0);
|
|
|
|
queue.enqueue(makeEvent('e1'));
|
|
expect(queue.size).toBe(1);
|
|
|
|
queue.enqueue(makeEvent('e2'));
|
|
expect(queue.size).toBe(2);
|
|
|
|
queue.drain(1);
|
|
expect(queue.size).toBe(1);
|
|
});
|
|
|
|
it('should return empty array when draining empty queue', () => {
|
|
const queue = new EventQueue(5);
|
|
const drained = queue.drain(10);
|
|
expect(drained).toEqual([]);
|
|
});
|
|
|
|
it('should prepend events to the front of the queue', () => {
|
|
const queue = new EventQueue(10);
|
|
queue.enqueue(makeEvent('e3'));
|
|
|
|
queue.prepend([makeEvent('e1'), makeEvent('e2')]);
|
|
expect(queue.size).toBe(3);
|
|
|
|
const drained = queue.drain(10);
|
|
expect(drained.map((e) => e.event_id)).toEqual(['e1', 'e2', 'e3']);
|
|
});
|
|
|
|
it('should respect maxSize when prepending', () => {
|
|
const queue = new EventQueue(3);
|
|
queue.enqueue(makeEvent('e3'));
|
|
queue.enqueue(makeEvent('e4'));
|
|
|
|
// Only 1 slot available, so only first event should be prepended
|
|
queue.prepend([makeEvent('e1'), makeEvent('e2')]);
|
|
expect(queue.size).toBe(3);
|
|
|
|
const drained = queue.drain(10);
|
|
expect(drained.map((e) => e.event_id)).toEqual(['e1', 'e3', 'e4']);
|
|
});
|
|
});
|