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

179 lines
5.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { RedteamPluginBase } from '../../../src/redteam/plugins/base';
import { getPiiLeakTestsForCategory, PiiGrader } from '../../../src/redteam/plugins/pii';
import type { PluginActionParams } from '../../../src/types/index';
describe('PiiGrader', () => {
let grader: PiiGrader;
beforeEach(() => {
grader = new PiiGrader();
});
it('should render rubric with variables', () => {
const vars = {
prompt: 'test prompt',
purpose: 'test purpose',
entities: ['entity1', 'entity2'],
};
const renderedRubric = grader.renderRubric(vars);
expect(renderedRubric).toContain('test prompt');
expect(renderedRubric).toContain('test purpose');
expect(renderedRubric).toContain('entity1');
expect(renderedRubric).toContain('entity2');
});
it('should render rubric without entities', () => {
const vars = {
prompt: 'test prompt',
purpose: 'test purpose',
};
const renderedRubric = grader.renderRubric(vars);
expect(renderedRubric).toContain('test prompt');
expect(renderedRubric).toContain('test purpose');
expect(renderedRubric).not.toContain('AllowedEntities');
});
it('should render prior conversation context when provided', () => {
const renderedRubric = grader.renderRubric({
prompt: 'What email did I give you?',
purpose: 'test purpose',
conversationTranscript:
'Turn 1:\nUser: My email is alice@example.com\nAssistant: Acknowledged.',
});
expect(renderedRubric).toContain('<ConversationHistory>');
expect(renderedRubric).toContain('User: My email is alice@example.com');
expect(renderedRubric).toContain('Assistant: Acknowledged.');
expect(renderedRubric).toContain('Current user input:');
expect(renderedRubric).toContain('<UserQuery>');
expect(renderedRubric).toContain('What email did I give you?');
expect(renderedRubric).toContain('<purpose>');
});
});
describe('getPiiLeakTestsForCategory', () => {
const mockProvider = {
callApi: vi.fn(),
};
const params: PluginActionParams = {
provider: mockProvider as any,
purpose: 'test purpose',
injectVar: 'prompt',
n: 2,
delayMs: 0,
config: {
examples: ['Example 1', 'Example 2'],
modifiers: {} as Record<string, unknown>,
},
};
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(RedteamPluginBase, 'appendModifiers');
});
it('should apply modifiers to prompt template before API call', async () => {
mockProvider.callApi.mockResolvedValue({
output: 'Prompt: Test prompt 1\nPrompt: Test prompt 2',
});
await getPiiLeakTestsForCategory(params, 'pii:direct');
expect(RedteamPluginBase.appendModifiers).toHaveBeenCalledWith(
expect.any(String),
params.config,
);
expect(RedteamPluginBase.appendModifiers).toHaveBeenCalledTimes(1);
});
it('should handle undefined config modifiers', async () => {
const paramsWithoutModifiers = {
...params,
config: { examples: ['Example 1'] },
};
mockProvider.callApi.mockResolvedValue({
output: 'Prompt: Test prompt',
});
await getPiiLeakTestsForCategory(paramsWithoutModifiers, 'pii:direct');
expect(RedteamPluginBase.appendModifiers).toHaveBeenCalledWith(
expect.any(String),
paramsWithoutModifiers.config,
);
});
it('should handle empty config modifiers', async () => {
const paramsWithEmptyModifiers = {
...params,
config: { examples: ['Example 1'], modifiers: {} },
};
mockProvider.callApi.mockResolvedValue({
output: 'Prompt: Test prompt',
});
await getPiiLeakTestsForCategory(paramsWithEmptyModifiers, 'pii:direct');
expect(RedteamPluginBase.appendModifiers).toHaveBeenCalledWith(
expect.any(String),
paramsWithEmptyModifiers.config,
);
});
it('should generate test cases for valid category', async () => {
mockProvider.callApi.mockResolvedValue({
output: 'Prompt: Test prompt 1\nPrompt: Test prompt 2',
});
const result = await getPiiLeakTestsForCategory(params, 'pii:direct');
expect(result[0]!.vars!.prompt).toBe('Test prompt 1');
expect(result[1]!.vars!.prompt).toBe('Test prompt 2');
expect(result[0]!.assert![0]!.type).toBe('promptfoo:redteam:pii');
expect(result[0]!.assert![0]!.metric).toBe('PIILeak');
});
it('should throw error for invalid category', async () => {
await expect(getPiiLeakTestsForCategory(params, 'invalid')).rejects.toThrow(
'Category invalid not found',
);
});
it('should handle empty provider response', async () => {
mockProvider.callApi.mockResolvedValue({
output: '',
});
const result = await getPiiLeakTestsForCategory(params, 'pii:direct');
expect(result).toHaveLength(0);
});
it('should handle non-string provider response', async () => {
mockProvider.callApi.mockResolvedValue({
output: { foo: 'bar' },
});
const result = await getPiiLeakTestsForCategory(params, 'pii:direct');
expect(result).toHaveLength(0);
});
it('should filter out non-prompt lines', async () => {
mockProvider.callApi.mockResolvedValue({
output: 'Some text\nPrompt: Test prompt\nOther text',
});
const result = await getPiiLeakTestsForCategory(params, 'pii:direct');
expect(result).toHaveLength(1);
expect(result[0]!.vars!.prompt).toBe('Test prompt');
});
});