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

712 lines
20 KiB
TypeScript

import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { mockGlobal } from '../util/utils';
const mockRandomUUID = vi.fn(() => 'test-uuid');
const restoreCrypto = mockGlobal('crypto', {
...crypto,
randomUUID: mockRandomUUID,
} as Crypto);
afterAll(() => {
restoreCrypto();
});
// Mock logger
vi.mock('../../src/logger', () => ({
default: {
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
}));
// Dynamic import after mocking - initialized in beforeAll
let TraceStore: typeof import('../../src/tracing/store').TraceStore;
describe('TraceStore', () => {
let traceStore: InstanceType<typeof TraceStore>;
let mockDb: any;
let mockDeleteChain: any;
beforeAll(async () => {
const mod = await import('../../src/tracing/store');
TraceStore = mod.TraceStore;
});
beforeEach(async () => {
// Reset the UUID mock
mockRandomUUID.mockReturnValue('test-uuid');
// Create mock database methods that properly chain
const mockInsertChain = {
values: vi.fn().mockReturnThis(),
onConflictDoNothing: vi.fn().mockReturnThis(),
run: vi.fn().mockResolvedValue(undefined),
};
const mockSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(() => Promise.resolve([])),
};
mockDeleteChain = {
where: vi.fn().mockReturnThis(),
run: vi.fn().mockResolvedValue(undefined),
};
mockDb = {
insert: vi.fn(() => mockInsertChain),
select: vi.fn(() => mockSelectChain),
delete: vi.fn(() => mockDeleteChain),
transaction: vi.fn(async (callback) => callback(mockDb)),
};
// Create trace store and inject mock DB
traceStore = new TraceStore();
// Use private property access to inject the mock
(traceStore as any).db = mockDb;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('createTrace', () => {
it('should create a new trace record', async () => {
const traceData = {
traceId: 'test-trace-id',
evaluationId: 'test-eval-id',
testCaseId: 'test-case-id',
metadata: { test: 'data' },
};
await traceStore.createTrace(traceData);
expect(mockDb.insert).toHaveBeenCalledWith(expect.anything());
expect(mockDb.insert().values).toHaveBeenCalledWith({
id: 'test-uuid',
traceId: 'test-trace-id',
evaluationId: 'test-eval-id',
testCaseId: 'test-case-id',
createdAt: expect.any(Number),
metadata: { test: 'data' },
});
expect(mockDb.insert().values().onConflictDoNothing).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.anything(),
}),
);
});
it('should handle errors when creating trace', async () => {
const error = new Error('Database error');
mockDb.insert().values().onConflictDoNothing().run.mockRejectedValueOnce(error);
const traceData = {
traceId: 'test-trace-id',
evaluationId: 'test-eval-id',
testCaseId: 'test-case-id',
};
await expect(traceStore.createTrace(traceData)).rejects.toThrow('Database error');
});
});
describe('getTraceMetadata', () => {
// This three-way contract is the single source of truth for ingest-time redaction
// (otlpReceiver.getRedactAttributePatterns): object => use stored policy, {} => no
// policy (no redaction), undefined => no trace row (fall back to receiver default).
it('returns the stored metadata object when the trace row has metadata', async () => {
mockDb
.select()
.from()
.where()
.limit.mockResolvedValueOnce([
{ metadata: { otlpHttpRedactAttributes: ['authorization'] } },
]);
await expect(traceStore.getTraceMetadata('trace-1')).resolves.toEqual({
otlpHttpRedactAttributes: ['authorization'],
});
});
it('returns an empty object (not undefined) when the row exists but metadata is null', async () => {
mockDb
.select()
.from()
.where()
.limit.mockResolvedValueOnce([{ metadata: null }]);
await expect(traceStore.getTraceMetadata('trace-2')).resolves.toEqual({});
});
it('returns undefined when no trace row exists', async () => {
mockDb.select().from().where().limit.mockResolvedValueOnce([]);
await expect(traceStore.getTraceMetadata('trace-3')).resolves.toBeUndefined();
});
});
describe('addSpans', () => {
it('should add spans to an existing trace', async () => {
// Mock trace exists check
mockDb
.select()
.from()
.where()
.limit.mockResolvedValueOnce([{ traceId: 'test-trace-id' }]);
const spans = [
{
spanId: 'span-1',
name: 'operation-1',
startTime: 1000,
endTime: 2000,
attributes: { key: 'value' },
},
{
spanId: 'span-2',
parentSpanId: 'span-1',
name: 'operation-2',
startTime: 1100,
endTime: 1900,
statusCode: 0,
statusMessage: 'OK',
},
];
await traceStore.addSpans('test-trace-id', spans);
expect(mockDb.select).toHaveBeenCalledWith();
expect(mockDb.select().from).toHaveBeenCalledWith(expect.anything());
expect(mockDb.select().from().where).toHaveBeenCalledWith(expect.anything());
expect(mockDb.insert).toHaveBeenCalledWith(expect.anything());
expect(mockDb.insert().values).toHaveBeenCalledWith([
{
id: 'test-uuid',
traceId: 'test-trace-id',
spanId: 'span-1',
parentSpanId: undefined,
name: 'operation-1',
startTime: 1000,
endTime: 2000,
attributes: { key: 'value' },
statusCode: undefined,
statusMessage: undefined,
},
{
id: 'test-uuid',
traceId: 'test-trace-id',
spanId: 'span-2',
parentSpanId: 'span-1',
name: 'operation-2',
startTime: 1100,
endTime: 1900,
attributes: undefined,
statusCode: 0,
statusMessage: 'OK',
},
]);
});
it('should skip spans if trace does not exist', async () => {
// Mock trace does not exist
mockDb.select().from().where().limit.mockResolvedValueOnce([]);
const spans = [
{
spanId: 'span-1',
name: 'operation-1',
startTime: 1000,
},
];
await traceStore.addSpans('non-existent-trace', spans);
// Should check for trace existence
expect(mockDb.select).toHaveBeenCalledWith();
// Should not insert spans
expect(mockDb.insert).not.toHaveBeenCalled();
});
it('should handle errors when adding spans', async () => {
// Mock trace exists
mockDb
.select()
.from()
.where()
.limit.mockResolvedValueOnce([{ traceId: 'test-trace-id' }]);
// Mock insert error
const error = new Error('Insert failed');
mockDb.insert().values().run.mockRejectedValueOnce(error);
const spans = [
{
spanId: 'span-1',
name: 'operation-1',
startTime: 1000,
},
];
await expect(traceStore.addSpans('test-trace-id', spans)).rejects.toThrow('Insert failed');
});
});
describe('getTracesByEvaluation', () => {
it('should retrieve all traces for an evaluation', async () => {
const mockTraces = [
{ id: '1', traceId: 'trace-1', evaluationId: 'eval-1', testCaseId: 'test-case-1' },
{ id: '2', traceId: 'trace-2', evaluationId: 'eval-1', testCaseId: 'test-case-2' },
];
const mockSpans = {
'trace-1': [
{
id: '1',
traceId: 'trace-1',
spanId: 'span-1-1',
parentSpanId: null,
name: 'span one',
startTime: 1000,
endTime: null,
attributes: null,
statusCode: null,
statusMessage: null,
},
{
id: '2',
traceId: 'trace-1',
spanId: 'span-1-2',
parentSpanId: 'span-1-1',
name: 'span two',
startTime: 2000,
endTime: 2500,
attributes: { foo: 'bar' },
statusCode: 1,
statusMessage: 'ok',
},
],
'trace-2': [
{
id: '3',
traceId: 'trace-2',
spanId: 'span-2-1',
parentSpanId: null,
name: 'span three',
startTime: 3000,
endTime: null,
attributes: null,
statusCode: null,
statusMessage: null,
},
],
};
// Set up the main traces query
const tracesSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockTraces)),
};
// Set up span queries for each trace
const spanQuery1 = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans['trace-1'])),
};
const spanQuery2 = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans['trace-2'])),
};
// Mock the select calls in sequence: first for traces, then for each trace's spans
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValueOnce(tracesSelectChain)
.mockReturnValueOnce(spanQuery1)
.mockReturnValueOnce(spanQuery2);
const result = await traceStore.getTracesByEvaluation('eval-1');
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: undefined,
spans: [
{
spanId: 'span-1-1',
parentSpanId: undefined,
name: 'span one',
startTime: 1000,
endTime: undefined,
attributes: undefined,
statusCode: undefined,
statusMessage: undefined,
},
{
spanId: 'span-1-2',
parentSpanId: 'span-1-1',
name: 'span two',
startTime: 2000,
endTime: 2500,
attributes: { foo: 'bar' },
statusCode: 1,
statusMessage: 'ok',
},
],
});
expect(result[1]).toEqual({
traceId: 'trace-2',
evaluationId: 'eval-1',
testCaseId: 'test-case-2',
metadata: undefined,
spans: [
{
spanId: 'span-2-1',
parentSpanId: undefined,
name: 'span three',
startTime: 3000,
endTime: undefined,
attributes: undefined,
statusCode: undefined,
statusMessage: undefined,
},
],
});
});
it('should redact sensitive span attributes for an evaluation', async () => {
const mockTrace = {
id: '1',
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: null,
};
const mockSpans = [
{
id: '1',
traceId: 'trace-1',
spanId: 'span-1',
parentSpanId: null,
name: 'http request',
startTime: 1000,
endTime: null,
attributes: {
authorization: 'Bearer trace-secret',
'x-api-key': 'gateway-secret',
headers: {
'api-key': 'header-secret',
set_cookie: 'session=trace-secret',
accept: 'application/json',
},
nested: {
api_token: 'nested-secret',
safe: 'ok',
},
'gen_ai.request.max_tokens': 4096,
'gen_ai.usage.input_tokens': 100,
'gen_ai.usage.output_tokens': 50,
'gen_ai.usage.total_tokens': 150,
},
statusCode: null,
statusMessage: null,
},
];
const tracesSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve([mockTrace])),
};
const spanQuery = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans)),
};
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValueOnce(tracesSelectChain)
.mockReturnValueOnce(spanQuery);
const result = await traceStore.getTracesByEvaluation('eval-1');
expect(result[0].spans[0].attributes).toEqual({
authorization: '<redacted>',
'x-api-key': '<redacted>',
headers: {
'api-key': '<redacted>',
set_cookie: '<redacted>',
accept: 'application/json',
},
nested: {
api_token: '<redacted>',
safe: 'ok',
},
'gen_ai.request.max_tokens': 4096,
'gen_ai.usage.input_tokens': 100,
'gen_ai.usage.output_tokens': 50,
'gen_ai.usage.total_tokens': 150,
});
});
it('should allow callers to retrieve raw span attributes for an evaluation', async () => {
const mockTrace = {
id: '1',
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: null,
};
const mockSpans = [
{
id: '1',
traceId: 'trace-1',
spanId: 'span-1',
parentSpanId: null,
name: 'http request',
startTime: 1000,
endTime: null,
attributes: {
authorization: 'Bearer trace-secret',
'x-api-key': 'gateway-secret',
headers: {
'api-key': 'header-secret',
set_cookie: 'session=trace-secret',
accept: 'application/json',
},
},
statusCode: null,
statusMessage: null,
},
];
const tracesSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve([mockTrace])),
};
const spanQuery = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans)),
};
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValueOnce(tracesSelectChain)
.mockReturnValueOnce(spanQuery);
const result = await traceStore.getTracesByEvaluation('eval-1', {
sanitizeAttributes: false,
});
expect(result[0].spans[0].attributes).toEqual(mockSpans[0].attributes);
});
it('should return empty array if no traces found', async () => {
mockDb.select().from().where.mockResolvedValueOnce([]);
const result = await traceStore.getTracesByEvaluation('non-existent-eval');
expect(result).toEqual([]);
});
});
describe('getTrace', () => {
it('should retrieve a single trace with spans', async () => {
const mockTrace = {
id: '1',
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: { source: 'test' },
};
const mockSpans = [
{
id: '1',
traceId: 'trace-1',
spanId: 'span-1',
parentSpanId: null,
name: 'span one',
startTime: 1000,
endTime: null,
attributes: null,
statusCode: null,
statusMessage: null,
},
{
id: '2',
traceId: 'trace-1',
spanId: 'span-2',
parentSpanId: 'span-1',
name: 'span two',
startTime: 2000,
endTime: 2500,
attributes: {
foo: 'bar',
authorization: 'Bearer trace-secret',
headers: {
Cookie: 'session=trace-secret',
'x-api-key': 'gateway-secret',
accept: 'application/json',
},
nested: {
'api-key': 'nested-header-secret',
api_key: 'nested-secret',
safe: 'ok',
},
},
statusCode: 1,
statusMessage: 'ok',
},
];
// Mock trace query - update the select chain to include limit
const traceSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(() => Promise.resolve([mockTrace])),
};
// Mock spans query
const spanQuery = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans)),
};
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValueOnce(traceSelectChain)
.mockReturnValueOnce(spanQuery);
const result = await traceStore.getTrace('trace-1');
expect(result).toEqual({
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: { source: 'test' },
spans: [
{
spanId: 'span-1',
parentSpanId: undefined,
name: 'span one',
startTime: 1000,
endTime: undefined,
attributes: undefined,
statusCode: undefined,
statusMessage: undefined,
},
{
spanId: 'span-2',
parentSpanId: 'span-1',
name: 'span two',
startTime: 2000,
endTime: 2500,
attributes: {
foo: 'bar',
authorization: '<redacted>',
headers: {
Cookie: '<redacted>',
'x-api-key': '<redacted>',
accept: 'application/json',
},
nested: {
'api-key': '<redacted>',
api_key: '<redacted>',
safe: 'ok',
},
},
statusCode: 1,
statusMessage: 'ok',
},
],
});
});
it('should allow callers to retrieve raw span attributes for a single trace', async () => {
const mockTrace = {
id: '1',
traceId: 'trace-1',
evaluationId: 'eval-1',
testCaseId: 'test-case-1',
metadata: null,
};
const mockSpans = [
{
id: '1',
traceId: 'trace-1',
spanId: 'span-1',
parentSpanId: null,
name: 'tool call',
startTime: 1000,
endTime: null,
attributes: {
api_key: 'trace-aware-assertion-secret',
token: 'trace-aware-token',
arguments: { api_key: 'nested-secret' },
},
statusCode: null,
statusMessage: null,
},
];
const traceSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(() => Promise.resolve([mockTrace])),
};
const spanQuery = {
from: vi.fn().mockReturnThis(),
where: vi.fn(() => Promise.resolve(mockSpans)),
};
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValueOnce(traceSelectChain)
.mockReturnValueOnce(spanQuery);
const result = await traceStore.getTrace('trace-1', { sanitizeAttributes: false });
expect(result?.spans[0].attributes).toEqual(mockSpans[0].attributes);
});
it('should return null if trace not found', async () => {
// Mock trace query - update the select chain to include limit
const traceSelectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(() => Promise.resolve([])),
};
vi.spyOn(mockDb, 'select')
.mockImplementation(() => ({}))
.mockReturnValue(traceSelectChain);
const result = await traceStore.getTrace('non-existent-trace');
expect(result).toBeNull();
});
});
describe('deleteOldTraces', () => {
it('should delete dependent spans before traces older than retention period', async () => {
const retentionDays = 30;
await traceStore.deleteOldTraces(retentionDays);
expect(mockDb.transaction).toHaveBeenCalledWith(expect.any(Function));
expect(mockDb.delete).toHaveBeenCalledTimes(2);
expect(mockDeleteChain.where).toHaveBeenCalledTimes(2);
expect(mockDeleteChain.run).toHaveBeenCalledTimes(2);
});
it('should handle errors when deleting old traces', async () => {
const error = new Error('Delete failed');
mockDeleteChain.run.mockRejectedValueOnce(error);
await expect(traceStore.deleteOldTraces(30)).rejects.toThrow('Delete failed');
});
});
});