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
305 lines
11 KiB
TypeScript
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.',
|
|
);
|
|
});
|
|
});
|
|
});
|