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

305 lines
11 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resolveContext } from '../../src/assertions/contextUtils';
import * as transformUtil from '../../src/util/transform';
import type { Assertion, AtomicTestCase } from '../../src/types/index';
vi.mock('../../src/util/transform', async (importOriginal) => {
const actual = await importOriginal<typeof transformUtil>();
return {
...actual,
transform: vi.fn(),
};
});
describe('resolveContext', () => {
const mockTransform = vi.mocked(transformUtil.transform);
beforeEach(() => {
vi.clearAllMocks();
mockTransform.mockReset();
});
afterEach(() => {
mockTransform.mockReset();
vi.clearAllMocks();
});
describe('with context variable', () => {
it('should return context from test.vars.context', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = {
vars: { context: 'test context' },
options: {},
};
const result = await resolveContext(assertion, test, 'output', 'prompt');
expect(result).toBe('test context');
expect(mockTransform).not.toHaveBeenCalled();
});
it('should return context array from test.vars.context', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const contextArray = ['chunk 1', 'chunk 2', 'chunk 3'];
const test: AtomicTestCase = {
vars: { context: contextArray },
options: {},
};
const result = await resolveContext(assertion, test, 'output', 'prompt');
expect(result).toEqual(contextArray);
expect(mockTransform).not.toHaveBeenCalled();
});
it('should reject context array with non-string elements', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = {
vars: { context: ['valid', 123, 'invalid'] },
options: {},
};
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Invalid context: expected an array of strings, but found number at index 1',
);
});
it('should use fallback context when no context variable', async () => {
const assertion: Assertion = { type: 'context-recall' };
const test: AtomicTestCase = { vars: {}, options: {} };
const result = await resolveContext(assertion, test, 'output', 'prompt', 'fallback context');
expect(result).toBe('fallback context');
expect(mockTransform).not.toHaveBeenCalled();
});
});
describe('with contextTransform', () => {
it('should transform output to get context', async () => {
mockTransform.mockResolvedValue('transformed context' as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.context',
};
const test: AtomicTestCase = { vars: {}, options: {} };
const result = await resolveContext(assertion, test, { context: 'data' }, 'prompt');
expect(result).toBe('transformed context');
expect(mockTransform).toHaveBeenCalledWith(
'output.context',
{ context: 'data' },
{
vars: {},
prompt: { label: 'prompt' },
},
);
});
it('should transform output to get context array', async () => {
const mockArray = ['chunk 1', 'chunk 2', 'chunk 3'];
mockTransform.mockResolvedValue(mockArray as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.chunks',
};
const test: AtomicTestCase = { vars: {}, options: {} };
const result = await resolveContext(assertion, test, { chunks: ['a', 'b', 'c'] }, 'prompt');
expect(result).toEqual(mockArray);
expect(mockTransform).toHaveBeenCalledWith(
'output.chunks',
{ chunks: ['a', 'b', 'c'] },
{
vars: {},
prompt: { label: 'prompt' },
},
);
});
it('should prioritize contextTransform over context variable', async () => {
mockTransform.mockResolvedValue('transformed context' as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.context',
};
const test: AtomicTestCase = {
vars: { context: 'original context' },
options: {},
};
const result = await resolveContext(assertion, test, { context: 'data' }, 'prompt');
expect(result).toBe('transformed context');
expect(mockTransform).toHaveBeenCalledWith(
'output.context',
{ context: 'data' },
{
vars: { context: 'original context' },
prompt: { label: 'prompt' },
},
);
});
it('should throw error if transform returns non-string and non-string-array', async () => {
mockTransform.mockResolvedValue(123 as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.invalid',
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
`contextTransform must return a string or array of strings. Got number. Check your transform expression: ${transformUtil.INLINE_STRING_LABEL}: output.invalid`,
);
});
it('should throw error if transform returns array with non-string elements', async () => {
mockTransform.mockResolvedValue(['valid', 123, 'invalid'] as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.invalid',
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
`contextTransform must return a string or array of strings. Got object. Check your transform expression: ${transformUtil.INLINE_STRING_LABEL}: output.invalid`,
);
});
it('should throw error if transform fails', async () => {
mockTransform.mockRejectedValue(new Error('Transform failed'));
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.invalid',
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
`Failed to transform context using expression '${transformUtil.INLINE_STRING_LABEL}: output.invalid': Transform failed`,
);
});
it('should support inline function as contextTransform', async () => {
const contextFn = (output: any) => output.context;
mockTransform.mockResolvedValue('extracted context' as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: contextFn,
};
const test: AtomicTestCase = { vars: {}, options: {} };
const result = await resolveContext(
assertion,
test,
{ context: 'extracted context', data: 'other' },
'prompt',
);
expect(result).toBe('extracted context');
expect(mockTransform).toHaveBeenCalledWith(
contextFn,
{ context: 'extracted context', data: 'other' },
expect.any(Object),
);
});
it('should show [inline function] in error messages for function contextTransform', async () => {
const contextFn = (() => 42) as any;
mockTransform.mockRejectedValue(new Error('Transform failed'));
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: contextFn,
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
/Failed to transform context using expression '\[inline function\].*': Transform failed/,
);
});
});
describe('error cases', () => {
it('should throw error when no context is available', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
it('should throw error when context is empty string', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = {
vars: { context: '' },
options: {},
};
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
it('should throw error when context is empty array', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = {
vars: { context: [] },
options: {},
};
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
it('should throw error when context array contains empty strings', async () => {
const assertion: Assertion = { type: 'context-faithfulness' };
const test: AtomicTestCase = {
vars: { context: ['valid chunk', '', 'another chunk'] },
options: {},
};
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
it('should throw error when contextTransform returns empty string', async () => {
mockTransform.mockResolvedValue('' as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.empty',
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
it('should throw error when contextTransform returns empty array', async () => {
mockTransform.mockResolvedValue([] as any);
const assertion: Assertion = {
type: 'context-faithfulness',
contextTransform: 'output.empty',
};
const test: AtomicTestCase = { vars: {}, options: {} };
await expect(resolveContext(assertion, test, 'output', 'prompt')).rejects.toThrow(
'Context is required for context-based assertions. Provide either a "context" variable (string or array of strings) in your test case or use "contextTransform" to extract context from the provider response.',
);
});
});
});