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

287 lines
8.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { OtelConfig } from '../../src/tracing/otelConfig';
// Create mock functions that will be used across tests
const mockRegister = vi.fn();
const mockShutdown = vi.fn().mockResolvedValue(undefined);
const mockForceFlush = vi.fn().mockResolvedValue(undefined);
const mockAddSpanProcessor = vi.fn();
const mockSetLogger = vi.fn();
// Track constructor calls
let nodeTracerProviderCalls: unknown[] = [];
let otlpExporterCalls: unknown[] = [];
let localExporterCalls: unknown[] = [];
let batchProcessorCalls: unknown[] = [];
let resourceCalls: unknown[] = [];
vi.mock('@opentelemetry/sdk-trace-node', () => {
// Use a class-like constructor function
return {
NodeTracerProvider: class MockNodeTracerProvider {
constructor(options: unknown) {
nodeTracerProviderCalls.push(options);
}
register = mockRegister;
shutdown = mockShutdown;
forceFlush = mockForceFlush;
addSpanProcessor = mockAddSpanProcessor;
},
BatchSpanProcessor: class MockBatchSpanProcessor {
exporter: unknown;
constructor(exporter: unknown) {
this.exporter = exporter;
batchProcessorCalls.push(exporter);
}
},
};
});
vi.mock('@opentelemetry/exporter-trace-otlp-http', () => ({
OTLPTraceExporter: class MockOTLPTraceExporter {
url: string | undefined;
constructor(config: { url?: string } = {}) {
this.url = config.url;
otlpExporterCalls.push(config);
}
},
}));
vi.mock('@opentelemetry/core', () => ({
W3CTraceContextPropagator: class MockW3CTraceContextPropagator {},
}));
vi.mock('@opentelemetry/resources', () => ({
resourceFromAttributes: (attrs: Record<string, unknown>) => {
resourceCalls.push(attrs);
return { attributes: attrs };
},
}));
vi.mock('@opentelemetry/semantic-conventions', () => ({
ATTR_SERVICE_NAME: 'service.name',
ATTR_SERVICE_VERSION: 'service.version',
}));
vi.mock('@opentelemetry/api', () => ({
diag: {
setLogger: mockSetLogger,
},
DiagConsoleLogger: class MockDiagConsoleLogger {},
DiagLogLevel: {
DEBUG: 0,
},
propagation: {
setGlobalPropagator: vi.fn(),
},
}));
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
}));
vi.mock('../../src/version', () => ({
VERSION: '1.0.0-test',
}));
vi.mock('../../src/tracing/localSpanExporter', () => ({
LocalSpanExporter: class MockLocalSpanExporter {
constructor() {
localExporterCalls.push({});
}
export = vi.fn();
shutdown = vi.fn();
},
}));
describe('otelSdk', () => {
// Module functions - will be re-imported in beforeEach
let initializeOtel: typeof import('../../src/tracing/otelSdk').initializeOtel;
let shutdownOtel: typeof import('../../src/tracing/otelSdk').shutdownOtel;
let flushOtel: typeof import('../../src/tracing/otelSdk').flushOtel;
let isOtelInitialized: typeof import('../../src/tracing/otelSdk').isOtelInitialized;
beforeEach(async () => {
// Clear all mocks and call tracking
vi.clearAllMocks();
nodeTracerProviderCalls = [];
otlpExporterCalls = [];
localExporterCalls = [];
batchProcessorCalls = [];
resourceCalls = [];
// Reset mock implementations
mockShutdown.mockResolvedValue(undefined);
mockForceFlush.mockResolvedValue(undefined);
// Reset modules to clear singleton state
vi.resetModules();
// Re-import the module
const module = await import('../../src/tracing/otelSdk');
initializeOtel = module.initializeOtel;
shutdownOtel = module.shutdownOtel;
flushOtel = module.flushOtel;
isOtelInitialized = module.isOtelInitialized;
});
afterEach(async () => {
vi.resetAllMocks();
});
const defaultConfig: OtelConfig = {
enabled: true,
serviceName: 'test-service',
endpoint: undefined,
localExport: true,
debug: false,
};
describe('initializeOtel', () => {
it('should not initialize when disabled', () => {
initializeOtel({ ...defaultConfig, enabled: false });
expect(isOtelInitialized()).toBe(false);
expect(mockRegister).not.toHaveBeenCalled();
});
it('should initialize and register provider', () => {
initializeOtel(defaultConfig);
expect(isOtelInitialized()).toBe(true);
expect(nodeTracerProviderCalls.length).toBe(1);
expect(mockRegister).toHaveBeenCalled();
});
it('should add local span processor when localExport is true', () => {
initializeOtel(defaultConfig);
expect(localExporterCalls.length).toBe(1);
// Span processors are now passed via constructor, so we check the constructor args
expect(nodeTracerProviderCalls.length).toBe(1);
const constructorArg = nodeTracerProviderCalls[0] as { spanProcessors?: unknown[] };
expect(constructorArg.spanProcessors).toBeDefined();
expect(constructorArg.spanProcessors?.length).toBeGreaterThanOrEqual(1);
});
it('should add OTLP exporter when endpoint is configured', () => {
initializeOtel({
...defaultConfig,
endpoint: 'http://localhost:4318/v1/traces',
});
expect(otlpExporterCalls.length).toBe(1);
expect(otlpExporterCalls[0]).toEqual({ url: 'http://localhost:4318/v1/traces' });
// Both local and OTLP exporters - now passed via constructor
const constructorArg = nodeTracerProviderCalls[0] as { spanProcessors?: unknown[] };
expect(constructorArg.spanProcessors?.length).toBe(2);
});
it('should skip local export when localExport is false', () => {
initializeOtel({
...defaultConfig,
localExport: false,
endpoint: 'http://localhost:4318',
});
expect(localExporterCalls.length).toBe(0);
// Only OTLP exporter - now passed via constructor
const constructorArg = nodeTracerProviderCalls[0] as { spanProcessors?: unknown[] };
expect(constructorArg.spanProcessors?.length).toBe(1);
});
it('should enable debug logging when debug is true', () => {
initializeOtel({
...defaultConfig,
debug: true,
});
expect(mockSetLogger).toHaveBeenCalled();
});
it('should not reinitialize when already initialized', () => {
initializeOtel(defaultConfig);
const firstCallCount = mockRegister.mock.calls.length;
initializeOtel(defaultConfig);
expect(mockRegister.mock.calls.length).toBe(firstCallCount);
});
it('should create resource with service name and version', () => {
initializeOtel(defaultConfig);
expect(resourceCalls.length).toBe(1);
expect(resourceCalls[0]).toEqual({
'service.name': 'test-service',
'service.version': '1.0.0-test',
});
});
});
describe('shutdownOtel', () => {
it('should not fail when not initialized', async () => {
await expect(shutdownOtel()).resolves.toBeUndefined();
});
it('should call provider shutdown when initialized', async () => {
initializeOtel(defaultConfig);
await shutdownOtel();
expect(mockShutdown).toHaveBeenCalled();
expect(isOtelInitialized()).toBe(false);
});
it('should handle shutdown errors gracefully', async () => {
mockShutdown.mockRejectedValue(new Error('Shutdown failed'));
initializeOtel(defaultConfig);
await expect(shutdownOtel()).resolves.toBeUndefined();
expect(isOtelInitialized()).toBe(false);
});
});
describe('flushOtel', () => {
it('should not fail when not initialized', async () => {
await expect(flushOtel()).resolves.toBeUndefined();
});
it('should call provider forceFlush when initialized', async () => {
initializeOtel(defaultConfig);
await flushOtel();
expect(mockForceFlush).toHaveBeenCalled();
});
it('should handle flush errors gracefully', async () => {
mockForceFlush.mockRejectedValue(new Error('Flush failed'));
initializeOtel(defaultConfig);
await expect(flushOtel()).resolves.toBeUndefined();
});
});
describe('isOtelInitialized', () => {
it('should return false before initialization', () => {
expect(isOtelInitialized()).toBe(false);
});
it('should return true after initialization', () => {
initializeOtel(defaultConfig);
expect(isOtelInitialized()).toBe(true);
});
it('should return false after shutdown', async () => {
initializeOtel(defaultConfig);
await shutdownOtel();
expect(isOtelInitialized()).toBe(false);
});
});
});