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

829 lines
26 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import logger from '../../src/logger';
import {
generateSpanId,
generateTraceContextIfNeeded,
generateTraceId,
generateTraceparent,
isOtlpReceiverStarted,
isTracingEnabled,
resetTracingState,
startOtlpReceiverIfNeeded,
stopOtlpReceiverIfNeeded,
} from '../../src/tracing/evaluatorTracing';
import { getTraceStore } from '../../src/tracing/store';
import { mockProcessEnv } from '../util/utils';
import type { TestCase, TestSuite } from '../../src/types/index';
const mockStartOTLPReceiver = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockStopOTLPReceiver = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockUpdateOTLPReceiverOptions = vi.hoisted(() => vi.fn());
const mockRegisterOTLPReceiverTracePolicy = vi.hoisted(() => vi.fn());
const mockCreateTrace = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
// Mock the logger
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
// Mock the trace store
vi.mock('../../src/tracing/store', () => ({
getTraceStore: vi.fn(() => ({
createTrace: mockCreateTrace,
})),
}));
vi.mock('../../src/tracing/otlpReceiver', () => ({
startOTLPReceiver: mockStartOTLPReceiver,
stopOTLPReceiver: mockStopOTLPReceiver,
updateOTLPReceiverOptions: mockUpdateOTLPReceiverOptions,
registerOTLPReceiverTracePolicy: mockRegisterOTLPReceiverTracePolicy,
}));
describe('evaluatorTracing', () => {
beforeEach(() => {
vi.resetAllMocks();
mockStartOTLPReceiver.mockReset();
mockStartOTLPReceiver.mockResolvedValue(undefined);
mockStopOTLPReceiver.mockReset();
mockStopOTLPReceiver.mockResolvedValue(undefined);
mockUpdateOTLPReceiverOptions.mockReset();
mockRegisterOTLPReceiverTracePolicy.mockReset();
mockCreateTrace.mockReset();
mockCreateTrace.mockResolvedValue(undefined);
vi.mocked(getTraceStore).mockReturnValue({
createTrace: mockCreateTrace,
} as unknown as ReturnType<typeof getTraceStore>);
resetTracingState();
// Reset environment variables
mockProcessEnv({ PROMPTFOO_TRACING_ENABLED: undefined });
});
describe('generateTraceId', () => {
it('should generate a 32-character hex string', () => {
const traceId = generateTraceId();
expect(traceId).toMatch(/^[a-f0-9]{32}$/);
expect(traceId).toHaveLength(32);
});
it('should generate unique IDs', () => {
const id1 = generateTraceId();
const id2 = generateTraceId();
expect(id1).not.toBe(id2);
});
});
describe('generateSpanId', () => {
it('should generate a 16-character hex string', () => {
const spanId = generateSpanId();
expect(spanId).toMatch(/^[a-f0-9]{16}$/);
expect(spanId).toHaveLength(16);
});
it('should generate unique IDs', () => {
const id1 = generateSpanId();
const id2 = generateSpanId();
expect(id1).not.toBe(id2);
});
});
describe('generateTraceparent', () => {
it('should generate valid W3C Trace Context format with sampled flag', () => {
const traceId = 'a'.repeat(32);
const spanId = 'b'.repeat(16);
const traceparent = generateTraceparent(traceId, spanId, true);
expect(traceparent).toBe(`00-${traceId}-${spanId}-01`);
});
it('should generate valid W3C Trace Context format without sampled flag', () => {
const traceId = 'a'.repeat(32);
const spanId = 'b'.repeat(16);
const traceparent = generateTraceparent(traceId, spanId, false);
expect(traceparent).toBe(`00-${traceId}-${spanId}-00`);
});
it('should default to sampled=true', () => {
const traceId = 'a'.repeat(32);
const spanId = 'b'.repeat(16);
const traceparent = generateTraceparent(traceId, spanId);
expect(traceparent).toBe(`00-${traceId}-${spanId}-01`);
});
});
describe('generateTraceContextIfNeeded', () => {
it('should return null when tracing is not enabled', async () => {
const test: TestCase = {
vars: { foo: 'bar' },
};
const result = await generateTraceContextIfNeeded(test, {}, 0, 0);
expect(result).toBeNull();
});
it('should generate trace context when tracing is enabled via metadata', async () => {
const test: TestCase = {
vars: { foo: 'bar' },
metadata: {
tracingEnabled: true,
evaluationId: 'eval-123',
testCaseId: 'test-456',
},
};
const result = await generateTraceContextIfNeeded(test, {}, 0, 0);
expect(result).not.toBeNull();
expect(result!.traceparent).toMatch(/^00-[a-f0-9]{32}-[a-f0-9]{16}-01$/);
expect(result!.evaluationId).toBe('eval-123');
expect(result!.testCaseId).toBe('test-456');
});
it('should generate trace context when tracing is enabled via environment', async () => {
mockProcessEnv({ PROMPTFOO_TRACING_ENABLED: 'true' });
const test: TestCase = {
vars: { foo: 'bar' },
};
const result = await generateTraceContextIfNeeded(test, {}, 5, 10);
expect(result).not.toBeNull();
expect(result!.traceparent).toMatch(/^00-[a-f0-9]{32}-[a-f0-9]{16}-01$/);
expect(result!.evaluationId).toMatch(/^eval-/);
expect(result!.testCaseId).toBe('5-10');
});
it('should generate trace context when tracing is enabled via YAML config', async () => {
const test: TestCase = {
vars: { foo: 'bar' },
};
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
},
} as unknown as TestSuite;
const result = await generateTraceContextIfNeeded(test, {}, 3, 7, testSuite);
expect(result).not.toBeNull();
expect(result!.traceparent).toMatch(/^00-[a-f0-9]{32}-[a-f0-9]{16}-01$/);
expect(result!.evaluationId).toMatch(/^eval-/);
expect(result!.testCaseId).toBe('3-7');
});
it('should persist command tool-name overrides in trace metadata', async () => {
const test: TestCase = {
vars: { foo: 'bar' },
};
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash'],
},
} as unknown as TestSuite;
const traceStoreModule = await import('../../src/tracing/store');
const getTraceStoreSpy = vi.spyOn(traceStoreModule, 'getTraceStore').mockReturnValue({
createTrace: mockCreateTrace,
} as unknown as ReturnType<typeof traceStoreModule.getTraceStore>);
try {
await generateTraceContextIfNeeded(test, {}, 2, 4, testSuite);
expect(mockCreateTrace).toHaveBeenCalledWith(
expect.objectContaining({
metadata: expect.objectContaining({
commandToolNames: ['bash'],
}),
}),
);
} finally {
getTraceStoreSpy.mockRestore();
}
});
it('should persist OTLP redaction config in trace metadata', async () => {
const test: TestCase = {
vars: { foo: 'bar' },
};
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: {
http: {
enabled: true,
redactAttributes: ['authorization'],
},
},
},
} as unknown as TestSuite;
await generateTraceContextIfNeeded(test, {}, 2, 4, testSuite);
expect(mockCreateTrace).toHaveBeenCalledWith(
expect.objectContaining({
metadata: expect.objectContaining({
otlpHttpRedactAttributes: ['authorization'],
}),
}),
);
});
});
describe('isTracingEnabled', () => {
it('should return false when no tracing is configured', () => {
const test: TestCase = { vars: {} };
expect(isTracingEnabled(test)).toBe(false);
});
it('should return true when test metadata has tracingEnabled', () => {
const test: TestCase = {
vars: {},
metadata: { tracingEnabled: true },
};
expect(isTracingEnabled(test)).toBe(true);
});
it('should return true when environment variable is set', () => {
mockProcessEnv({ PROMPTFOO_TRACING_ENABLED: 'true' });
const test: TestCase = { vars: {} };
expect(isTracingEnabled(test)).toBe(true);
});
it('should return true when testSuite.tracing.enabled is true', () => {
const test: TestCase = { vars: {} };
const testSuite = {
providers: [],
prompts: [],
tracing: { enabled: true },
} as unknown as TestSuite;
expect(isTracingEnabled(test, testSuite)).toBe(true);
});
it('should return false when testSuite.tracing.enabled is false', () => {
const test: TestCase = { vars: {} };
const testSuite = {
providers: [],
prompts: [],
tracing: { enabled: false },
} as unknown as TestSuite;
expect(isTracingEnabled(test, testSuite)).toBe(false);
});
it('should return true when any source enables tracing', () => {
const test: TestCase = {
vars: {},
metadata: { tracingEnabled: false },
};
const testSuite = {
providers: [],
prompts: [],
tracing: { enabled: true },
} as unknown as TestSuite;
// testSuite enables tracing even though metadata doesn't
expect(isTracingEnabled(test, testSuite)).toBe(true);
});
});
describe('isOtlpReceiverStarted', () => {
it('should return false initially', () => {
expect(isOtlpReceiverStarted()).toBe(false);
});
});
describe('startOtlpReceiverIfNeeded', () => {
it('should pass tracing configuration to the logger as sanitizable context', async () => {
const secret = 'Bearer secret-token';
const tracing = {
enabled: true,
forwarding: {
enabled: true,
endpoint: 'https://example.com/v1/traces',
headers: { authorization: secret },
},
};
const testSuite = {
providers: [],
prompts: [],
tracing,
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(logger.debug).toHaveBeenCalledWith(
'[EvaluatorTracing] Checking tracing configuration',
{
tracing,
testSuiteKeys: ['providers', 'prompts', 'tracing'],
},
);
expect(
vi.mocked(logger.debug).mock.calls.some(([message]) => String(message).includes(secret)),
).toBe(false);
});
it('should pass configured acceptFormats to the OTLP receiver', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: {
http: {
enabled: true,
port: 4318,
host: '0.0.0.0',
acceptFormats: ['protobuf'],
},
},
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '0.0.0.0', ['protobuf'], {
commandToolNames: undefined,
redactAttributes: undefined,
});
});
it('should swallow receiver start errors by default', async () => {
mockStartOTLPReceiver.mockRejectedValueOnce(new Error('EADDRINUSE: port 4318'));
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
await expect(startOtlpReceiverIfNeeded(testSuite)).resolves.toBe(false);
expect(isOtlpReceiverStarted()).toBe(false);
});
it('should rethrow receiver start errors when failOnReceiverStartFailure is true', async () => {
mockStartOTLPReceiver.mockRejectedValueOnce(new Error('EADDRINUSE: port 4318'));
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
failOnReceiverStartFailure: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
await expect(startOtlpReceiverIfNeeded(testSuite)).rejects.toThrow(
/Failed to start OTLP tracing receiver: EADDRINUSE/,
);
});
it('should prune the trace store when storage.retentionDays is set', async () => {
const deleteOldTraces = vi.fn().mockResolvedValue(undefined);
const { getTraceStore } = await import('../../src/tracing/store');
vi.mocked(getTraceStore).mockReturnValue({
deleteOldTraces,
} as unknown as ReturnType<typeof getTraceStore>);
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
storage: { type: 'sqlite', retentionDays: 7 },
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(deleteOldTraces).toHaveBeenCalledWith(7);
});
it('should prune the trace store without requiring an OTLP HTTP receiver', async () => {
const deleteOldTraces = vi.fn().mockResolvedValue(undefined);
vi.mocked(getTraceStore).mockReturnValue({
deleteOldTraces,
} as unknown as ReturnType<typeof getTraceStore>);
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
storage: { type: 'sqlite', retentionDays: 7 },
},
} as unknown as TestSuite);
expect(deleteOldTraces).toHaveBeenCalledWith(7);
expect(mockStartOTLPReceiver).not.toHaveBeenCalled();
});
it('should prune the trace store when tracing is enabled by environment', async () => {
const deleteOldTraces = vi.fn().mockResolvedValue(undefined);
vi.mocked(getTraceStore).mockReturnValue({
deleteOldTraces,
} as unknown as ReturnType<typeof getTraceStore>);
mockProcessEnv({ PROMPTFOO_TRACING_ENABLED: 'true' });
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
storage: { type: 'sqlite', retentionDays: 7 },
},
} as unknown as TestSuite);
expect(deleteOldTraces).toHaveBeenCalledWith(7);
expect(mockStartOTLPReceiver).not.toHaveBeenCalled();
});
it('should not prune when retentionDays is omitted', async () => {
const deleteOldTraces = vi.fn().mockResolvedValue(undefined);
const { getTraceStore } = await import('../../src/tracing/store');
vi.mocked(getTraceStore).mockReturnValue({
deleteOldTraces,
} as unknown as ReturnType<typeof getTraceStore>);
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(deleteOldTraces).not.toHaveBeenCalled();
});
it('should pass redactAttributes to the OTLP receiver', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
redactAttributes: ['tool.arguments', 'authorization'],
},
},
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '127.0.0.1', ['json', 'protobuf'], {
commandToolNames: undefined,
redactAttributes: ['tool.arguments', 'authorization'],
});
});
it('should pass commandToolNames to the OTLP receiver', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash', 'shell'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
},
},
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '127.0.0.1', ['json', 'protobuf'], {
commandToolNames: ['bash', 'shell'],
redactAttributes: undefined,
});
});
it('should pass the current evaluation policy when starting the OTLP receiver', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
redactAttributes: ['authorization'],
},
},
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite, 'eval-current');
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '127.0.0.1', ['json', 'protobuf'], {
commandToolNames: ['bash'],
redactAttributes: ['authorization'],
tracePolicy: {
evaluationId: 'eval-current',
commandToolNames: ['bash'],
redactAttributes: ['authorization'],
},
});
});
it('should default acceptFormats to both when omitted', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: {
http: {
enabled: true,
port: 4318,
host: '0.0.0.0',
},
},
},
} as unknown as TestSuite;
await startOtlpReceiverIfNeeded(testSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '0.0.0.0', ['json', 'protobuf'], {
commandToolNames: undefined,
redactAttributes: undefined,
});
});
it('should prune the trace store when tracing is already running', async () => {
const deleteOldTraces = vi.fn().mockResolvedValue(undefined);
const { getTraceStore } = await import('../../src/tracing/store');
vi.mocked(getTraceStore).mockReturnValue({
deleteOldTraces,
} as unknown as ReturnType<typeof getTraceStore>);
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite);
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
},
},
storage: { type: 'sqlite', retentionDays: 7 },
},
} as unknown as TestSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
expect(deleteOldTraces).toHaveBeenCalledWith(7);
});
it('should not mutate receiver defaults when another traced evaluation is already running', async () => {
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite);
await startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
acceptFormats: ['json'],
redactAttributes: ['authorization'],
},
},
},
} as unknown as TestSuite);
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
expect(mockUpdateOTLPReceiverOptions).not.toHaveBeenCalled();
});
it('should register the current evaluation policy when reusing an active receiver', async () => {
await startOtlpReceiverIfNeeded(
{
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite,
'eval-a',
);
await startOtlpReceiverIfNeeded(
{
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
redactAttributes: ['authorization'],
},
},
},
} as unknown as TestSuite,
'eval-b',
);
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
expect(mockRegisterOTLPReceiverTracePolicy).toHaveBeenCalledWith({
evaluationId: 'eval-b',
commandToolNames: ['bash'],
redactAttributes: ['authorization'],
});
expect(mockUpdateOTLPReceiverOptions).not.toHaveBeenCalled();
});
it('should preserve the first receiver defaults during overlapping startup', async () => {
let resolveStart!: () => void;
mockStartOTLPReceiver.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
resolveStart = resolve;
}),
);
const firstStart = startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['bash'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
acceptFormats: ['json'],
redactAttributes: ['authorization'],
},
},
},
} as unknown as TestSuite);
const secondStart = startOtlpReceiverIfNeeded({
providers: [],
prompts: [],
tracing: {
enabled: true,
commandToolNames: ['shell'],
otlp: {
http: {
enabled: true,
port: 4318,
host: '127.0.0.1',
acceptFormats: ['protobuf'],
redactAttributes: ['cookie'],
},
},
},
} as unknown as TestSuite);
await vi.waitFor(() => expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1));
resolveStart();
await Promise.all([firstStart, secondStart]);
expect(mockStartOTLPReceiver).toHaveBeenCalledWith(4318, '127.0.0.1', ['json'], {
commandToolNames: ['bash'],
redactAttributes: ['authorization'],
});
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
expect(mockUpdateOTLPReceiverOptions).not.toHaveBeenCalled();
});
it('should keep the receiver live until every overlapping evaluation releases its lease', async () => {
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
const firstLease = await startOtlpReceiverIfNeeded(testSuite);
const secondLease = await startOtlpReceiverIfNeeded(testSuite);
await stopOtlpReceiverIfNeeded(firstLease);
expect(mockStopOTLPReceiver).not.toHaveBeenCalled();
expect(isOtlpReceiverStarted()).toBe(true);
await stopOtlpReceiverIfNeeded(secondLease);
expect(mockStopOTLPReceiver).toHaveBeenCalledTimes(1);
expect(isOtlpReceiverStarted()).toBe(false);
});
it('should wait for an in-flight shutdown before starting a new receiver', async () => {
let resolveStop!: () => void;
mockStopOTLPReceiver.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
resolveStop = resolve;
}),
);
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
const lease = await startOtlpReceiverIfNeeded(testSuite);
const stopping = stopOtlpReceiverIfNeeded(lease);
const restarting = startOtlpReceiverIfNeeded(testSuite);
await vi.waitFor(() => expect(mockStopOTLPReceiver).toHaveBeenCalledTimes(1));
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
resolveStop();
await Promise.all([stopping, restarting]);
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(2);
});
it('should preserve a receiver lease when an overlapping shutdown fails', async () => {
let rejectStop!: (error: Error) => void;
mockStopOTLPReceiver.mockImplementationOnce(
() =>
new Promise<void>((_resolve, reject) => {
rejectStop = reject;
}),
);
const testSuite = {
providers: [],
prompts: [],
tracing: {
enabled: true,
otlp: { http: { enabled: true, port: 4318, host: '127.0.0.1' } },
},
} as unknown as TestSuite;
const lease = await startOtlpReceiverIfNeeded(testSuite);
const stopping = stopOtlpReceiverIfNeeded(lease);
const restarting = startOtlpReceiverIfNeeded(testSuite);
await vi.waitFor(() => expect(mockStopOTLPReceiver).toHaveBeenCalledTimes(1));
rejectStop(new Error('shutdown failed'));
await expect(stopping).resolves.toBeUndefined();
await expect(restarting).resolves.toBe(true);
expect(mockStartOTLPReceiver).toHaveBeenCalledTimes(1);
expect(isOtlpReceiverStarted()).toBe(true);
await stopOtlpReceiverIfNeeded(true);
expect(mockStopOTLPReceiver).toHaveBeenCalledTimes(2);
expect(isOtlpReceiverStarted()).toBe(false);
});
});
});