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>
220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { EventBuilder } from '../src/event-builder.js';
|
|
import { ResolvedConfig } from '../src/config.js';
|
|
import {
|
|
TaskType,
|
|
Complexity,
|
|
Harness,
|
|
Provider,
|
|
Outcome,
|
|
QualityGate,
|
|
RepoSizeCategory,
|
|
} from '../src/types/events.js';
|
|
|
|
function makeConfig(): ResolvedConfig {
|
|
return {
|
|
serverUrl: 'https://tel.example.com',
|
|
apiKey: 'a'.repeat(64),
|
|
instanceId: 'my-instance-uuid',
|
|
enabled: true,
|
|
submitIntervalMs: 300_000,
|
|
maxQueueSize: 1000,
|
|
batchSize: 100,
|
|
requestTimeoutMs: 10_000,
|
|
predictionCacheTtlMs: 21_600_000,
|
|
dryRun: false,
|
|
maxRetries: 3,
|
|
onError: () => {},
|
|
};
|
|
}
|
|
|
|
describe('EventBuilder', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should build a complete TaskCompletionEvent', () => {
|
|
const builder = new EventBuilder(makeConfig());
|
|
const event = builder.build({
|
|
task_duration_ms: 15000,
|
|
task_type: TaskType.IMPLEMENTATION,
|
|
complexity: Complexity.HIGH,
|
|
harness: Harness.CLAUDE_CODE,
|
|
model: 'claude-3-opus',
|
|
provider: Provider.ANTHROPIC,
|
|
estimated_input_tokens: 2000,
|
|
estimated_output_tokens: 1000,
|
|
actual_input_tokens: 2200,
|
|
actual_output_tokens: 1100,
|
|
estimated_cost_usd_micros: 100000,
|
|
actual_cost_usd_micros: 110000,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [QualityGate.BUILD, QualityGate.TEST, QualityGate.LINT],
|
|
quality_gates_failed: [],
|
|
context_compactions: 2,
|
|
context_rotations: 1,
|
|
context_utilization_final: 0.75,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
language: 'typescript',
|
|
repo_size_category: RepoSizeCategory.MEDIUM,
|
|
});
|
|
|
|
expect(event.task_type).toBe(TaskType.IMPLEMENTATION);
|
|
expect(event.complexity).toBe(Complexity.HIGH);
|
|
expect(event.model).toBe('claude-3-opus');
|
|
expect(event.quality_gates_run).toEqual([
|
|
QualityGate.BUILD,
|
|
QualityGate.TEST,
|
|
QualityGate.LINT,
|
|
]);
|
|
expect(event.language).toBe('typescript');
|
|
expect(event.repo_size_category).toBe(RepoSizeCategory.MEDIUM);
|
|
});
|
|
|
|
it('should auto-generate event_id as UUID', () => {
|
|
const builder = new EventBuilder(makeConfig());
|
|
const event = builder.build({
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.TESTING,
|
|
complexity: Complexity.LOW,
|
|
harness: Harness.AIDER,
|
|
model: 'gpt-4',
|
|
provider: Provider.OPENAI,
|
|
estimated_input_tokens: 100,
|
|
estimated_output_tokens: 50,
|
|
actual_input_tokens: 100,
|
|
actual_output_tokens: 50,
|
|
estimated_cost_usd_micros: 1000,
|
|
actual_cost_usd_micros: 1000,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [],
|
|
quality_gates_failed: [],
|
|
context_compactions: 0,
|
|
context_rotations: 0,
|
|
context_utilization_final: 0.3,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
});
|
|
|
|
// UUID format: 8-4-4-4-12 hex chars
|
|
expect(event.event_id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
);
|
|
|
|
// Each event should get a unique ID
|
|
const event2 = builder.build({
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.TESTING,
|
|
complexity: Complexity.LOW,
|
|
harness: Harness.AIDER,
|
|
model: 'gpt-4',
|
|
provider: Provider.OPENAI,
|
|
estimated_input_tokens: 100,
|
|
estimated_output_tokens: 50,
|
|
actual_input_tokens: 100,
|
|
actual_output_tokens: 50,
|
|
estimated_cost_usd_micros: 1000,
|
|
actual_cost_usd_micros: 1000,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [],
|
|
quality_gates_failed: [],
|
|
context_compactions: 0,
|
|
context_rotations: 0,
|
|
context_utilization_final: 0.3,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
});
|
|
|
|
expect(event.event_id).not.toBe(event2.event_id);
|
|
});
|
|
|
|
it('should auto-set timestamp to ISO 8601', () => {
|
|
const now = new Date('2026-02-07T10:00:00.000Z');
|
|
vi.setSystemTime(now);
|
|
|
|
const builder = new EventBuilder(makeConfig());
|
|
const event = builder.build({
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.DEBUGGING,
|
|
complexity: Complexity.MEDIUM,
|
|
harness: Harness.OPENCODE,
|
|
model: 'claude-3-sonnet',
|
|
provider: Provider.ANTHROPIC,
|
|
estimated_input_tokens: 500,
|
|
estimated_output_tokens: 200,
|
|
actual_input_tokens: 500,
|
|
actual_output_tokens: 200,
|
|
estimated_cost_usd_micros: 5000,
|
|
actual_cost_usd_micros: 5000,
|
|
quality_gate_passed: false,
|
|
quality_gates_run: [QualityGate.TEST],
|
|
quality_gates_failed: [QualityGate.TEST],
|
|
context_compactions: 0,
|
|
context_rotations: 0,
|
|
context_utilization_final: 0.4,
|
|
outcome: Outcome.FAILURE,
|
|
retry_count: 1,
|
|
});
|
|
|
|
expect(event.timestamp).toBe('2026-02-07T10:00:00.000Z');
|
|
});
|
|
|
|
it('should set instance_id from config', () => {
|
|
const config = makeConfig();
|
|
const builder = new EventBuilder(config);
|
|
const event = builder.build({
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.PLANNING,
|
|
complexity: Complexity.LOW,
|
|
harness: Harness.UNKNOWN,
|
|
model: 'test-model',
|
|
provider: Provider.UNKNOWN,
|
|
estimated_input_tokens: 0,
|
|
estimated_output_tokens: 0,
|
|
actual_input_tokens: 0,
|
|
actual_output_tokens: 0,
|
|
estimated_cost_usd_micros: 0,
|
|
actual_cost_usd_micros: 0,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [],
|
|
quality_gates_failed: [],
|
|
context_compactions: 0,
|
|
context_rotations: 0,
|
|
context_utilization_final: 0,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
});
|
|
|
|
expect(event.instance_id).toBe('my-instance-uuid');
|
|
});
|
|
|
|
it('should set schema_version to 1.0', () => {
|
|
const builder = new EventBuilder(makeConfig());
|
|
const event = builder.build({
|
|
task_duration_ms: 1000,
|
|
task_type: TaskType.REFACTORING,
|
|
complexity: Complexity.CRITICAL,
|
|
harness: Harness.KILO_CODE,
|
|
model: 'gemini-pro',
|
|
provider: Provider.GOOGLE,
|
|
estimated_input_tokens: 3000,
|
|
estimated_output_tokens: 2000,
|
|
actual_input_tokens: 3000,
|
|
actual_output_tokens: 2000,
|
|
estimated_cost_usd_micros: 80000,
|
|
actual_cost_usd_micros: 80000,
|
|
quality_gate_passed: true,
|
|
quality_gates_run: [QualityGate.TYPECHECK],
|
|
quality_gates_failed: [],
|
|
context_compactions: 5,
|
|
context_rotations: 2,
|
|
context_utilization_final: 0.95,
|
|
outcome: Outcome.SUCCESS,
|
|
retry_count: 0,
|
|
});
|
|
|
|
expect(event.schema_version).toBe('1.0');
|
|
});
|
|
});
|