Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

294 lines
9.3 KiB
TypeScript

/**
* Integration tests for OpenTelemetry tracing infrastructure.
*
* These tests verify that the OTEL SDK can be initialized and that
* provider calls correctly create spans with GenAI semantic conventions.
*/
import { SpanStatusCode } from '@opentelemetry/api';
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import {
GenAIAttributes,
getGenAITracer,
PromptfooAttributes,
withGenAISpan,
} from '../../src/tracing/genaiTracer';
import type { GenAISpanContext, GenAISpanResult } from '../../src/tracing/genaiTracer';
describe('OpenTelemetry Tracing Integration', () => {
let tracerProvider: NodeTracerProvider;
let memoryExporter: InMemorySpanExporter;
beforeAll(() => {
// Set up an in-memory exporter for testing
memoryExporter = new InMemorySpanExporter();
tracerProvider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(memoryExporter)],
});
tracerProvider.register();
});
afterAll(async () => {
await tracerProvider.shutdown();
});
beforeEach(() => {
memoryExporter.reset();
vi.resetAllMocks();
});
describe('withGenAISpan', () => {
it('should create a span with correct GenAI attributes', async () => {
const spanContext: GenAISpanContext = {
system: 'openai',
operationName: 'chat',
model: 'gpt-4',
providerId: 'openai:gpt-4',
maxTokens: 1000,
temperature: 0.7,
testIndex: 5,
promptLabel: 'test-prompt',
};
const mockResult = {
output: 'Hello, world!',
tokenUsage: { prompt: 10, completion: 5, total: 15 },
};
const resultExtractor = (): GenAISpanResult => ({
tokenUsage: { prompt: 10, completion: 5, total: 15 },
finishReasons: ['stop'],
});
const result = await withGenAISpan(spanContext, async () => mockResult, resultExtractor);
expect(result).toEqual(mockResult);
// Get the exported spans
const spans = memoryExporter.getFinishedSpans();
expect(spans.length).toBe(1);
const span = spans[0];
// Verify span name follows GenAI convention
expect(span.name).toBe('chat gpt-4');
// Verify GenAI attributes
expect(span.attributes[GenAIAttributes.SYSTEM]).toBe('openai');
expect(span.attributes[GenAIAttributes.OPERATION_NAME]).toBe('chat');
expect(span.attributes[GenAIAttributes.REQUEST_MODEL]).toBe('gpt-4');
expect(span.attributes[GenAIAttributes.REQUEST_MAX_TOKENS]).toBe(1000);
expect(span.attributes[GenAIAttributes.REQUEST_TEMPERATURE]).toBe(0.7);
// Verify Promptfoo attributes
expect(span.attributes[PromptfooAttributes.PROVIDER_ID]).toBe('openai:gpt-4');
expect(span.attributes[PromptfooAttributes.TEST_INDEX]).toBe(5);
expect(span.attributes[PromptfooAttributes.PROMPT_LABEL]).toBe('test-prompt');
// Verify response attributes
expect(span.attributes[GenAIAttributes.USAGE_INPUT_TOKENS]).toBe(10);
expect(span.attributes[GenAIAttributes.USAGE_OUTPUT_TOKENS]).toBe(5);
expect(span.attributes[GenAIAttributes.USAGE_TOTAL_TOKENS]).toBe(15);
expect(span.attributes[GenAIAttributes.RESPONSE_FINISH_REASONS]).toEqual(['stop']);
// Verify span status
expect(span.status.code).toBe(SpanStatusCode.OK);
});
it('should handle errors and set span status to ERROR', async () => {
const spanContext: GenAISpanContext = {
system: 'anthropic',
operationName: 'chat',
model: 'claude-3-opus',
providerId: 'anthropic:claude-3-opus',
};
const error = new Error('API rate limit exceeded');
await expect(
withGenAISpan(spanContext, async () => {
throw error;
}),
).rejects.toThrow('API rate limit exceeded');
const spans = memoryExporter.getFinishedSpans();
expect(spans.length).toBe(1);
const span = spans[0];
// Verify span status is ERROR
expect(span.status.code).toBe(SpanStatusCode.ERROR);
expect(span.status.message).toBe('API rate limit exceeded');
// Verify exception was recorded
expect(span.events.length).toBeGreaterThan(0);
const exceptionEvent = span.events.find((e) => e.name === 'exception');
expect(exceptionEvent).toBeDefined();
});
it('should work without result extractor', async () => {
const spanContext: GenAISpanContext = {
system: 'bedrock',
operationName: 'chat',
model: 'anthropic.claude-3-sonnet',
providerId: 'bedrock:claude-3-sonnet',
};
const result = await withGenAISpan(spanContext, async () => ({
output: 'response',
}));
expect(result).toEqual({ output: 'response' });
const spans = memoryExporter.getFinishedSpans();
expect(spans.length).toBe(1);
// Should still have basic attributes
expect(spans[0].attributes[GenAIAttributes.SYSTEM]).toBe('bedrock');
expect(spans[0].status.code).toBe(SpanStatusCode.OK);
});
it('should capture multiple nested spans correctly', async () => {
const outerContext: GenAISpanContext = {
system: 'azure',
operationName: 'chat',
model: 'gpt-4-deployment',
providerId: 'azure:gpt-4',
};
const innerContext: GenAISpanContext = {
system: 'openai',
operationName: 'embedding',
model: 'text-embedding-ada-002',
providerId: 'openai:embedding',
};
await withGenAISpan(outerContext, async () => {
// Nested span for embedding
await withGenAISpan(innerContext, async () => {
return { embedding: [0.1, 0.2, 0.3] };
});
return { output: 'response' };
});
const spans = memoryExporter.getFinishedSpans();
expect(spans.length).toBe(2);
// Inner span should finish first
const embeddingSpan = spans.find((s) => s.name.includes('embedding'));
const chatSpan = spans.find((s) => s.name.includes('chat'));
expect(embeddingSpan).toBeDefined();
expect(chatSpan).toBeDefined();
expect(embeddingSpan!.attributes[GenAIAttributes.SYSTEM]).toBe('openai');
expect(chatSpan!.attributes[GenAIAttributes.SYSTEM]).toBe('azure');
});
});
describe('getGenAITracer', () => {
it('should return a tracer with correct name', () => {
const tracer = getGenAITracer();
expect(tracer).toBeDefined();
// Tracer should be usable
const span = tracer.startSpan('test-span');
span.end();
const spans = memoryExporter.getFinishedSpans();
expect(spans.some((s) => s.name === 'test-span')).toBe(true);
});
});
describe('Token usage with completion details', () => {
it('should capture reasoning tokens in completion details', async () => {
const spanContext: GenAISpanContext = {
system: 'openai',
operationName: 'chat',
model: 'o1-preview',
providerId: 'openai:o1-preview',
};
const resultExtractor = (): GenAISpanResult => ({
tokenUsage: {
prompt: 100,
completion: 500,
total: 600,
completionDetails: {
reasoning: 450,
},
},
});
await withGenAISpan(spanContext, async () => ({ output: 'response' }), resultExtractor);
const spans = memoryExporter.getFinishedSpans();
expect(spans.length).toBe(1);
const span = spans[0];
expect(span.attributes[GenAIAttributes.USAGE_INPUT_TOKENS]).toBe(100);
expect(span.attributes[GenAIAttributes.USAGE_OUTPUT_TOKENS]).toBe(500);
expect(span.attributes[GenAIAttributes.USAGE_REASONING_TOKENS]).toBe(450);
});
it('should capture predicted token details', async () => {
const spanContext: GenAISpanContext = {
system: 'openai',
operationName: 'chat',
model: 'gpt-4-turbo',
providerId: 'openai:gpt-4-turbo',
};
const resultExtractor = (): GenAISpanResult => ({
tokenUsage: {
prompt: 50,
completion: 30,
total: 80,
completionDetails: {
acceptedPrediction: 25,
rejectedPrediction: 5,
},
},
});
await withGenAISpan(spanContext, async () => ({ output: 'response' }), resultExtractor);
const spans = memoryExporter.getFinishedSpans();
const span = spans[0];
expect(span.attributes[GenAIAttributes.USAGE_ACCEPTED_PREDICTION_TOKENS]).toBe(25);
expect(span.attributes[GenAIAttributes.USAGE_REJECTED_PREDICTION_TOKENS]).toBe(5);
});
it('should capture cached tokens', async () => {
const spanContext: GenAISpanContext = {
system: 'anthropic',
operationName: 'chat',
model: 'claude-3-sonnet',
providerId: 'anthropic:claude-3-sonnet',
};
const resultExtractor = (): GenAISpanResult => ({
tokenUsage: {
prompt: 200,
completion: 100,
total: 300,
cached: 150,
},
});
await withGenAISpan(
spanContext,
async () => ({ output: 'cached response' }),
resultExtractor,
);
const spans = memoryExporter.getFinishedSpans();
const span = spans[0];
expect(span.attributes[GenAIAttributes.USAGE_CACHED_TOKENS]).toBe(150);
});
});
});