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
187 lines
6.1 KiB
TypeScript
187 lines
6.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { fetchHuggingFaceDataset } from '../../../src/integrations/huggingfaceDatasets';
|
|
import {
|
|
fetchDataset,
|
|
ToxicChatGrader,
|
|
ToxicChatPlugin,
|
|
} from '../../../src/redteam/plugins/toxicChat';
|
|
|
|
import type { TestCase } from '../../../src/types/index';
|
|
|
|
vi.mock('../../../src/integrations/huggingfaceDatasets');
|
|
|
|
describe('ToxicChatPlugin', () => {
|
|
let plugin: ToxicChatPlugin;
|
|
|
|
beforeEach(() => {
|
|
plugin = new ToxicChatPlugin({ id: 'test-provider' } as any, 'Test purpose', 'prompt');
|
|
});
|
|
|
|
it('should set canGenerateRemote to false', () => {
|
|
expect(ToxicChatPlugin.canGenerateRemote).toBe(false);
|
|
});
|
|
|
|
it('should have correct plugin ID', () => {
|
|
expect(plugin.id).toBe('promptfoo:redteam:toxic-chat');
|
|
});
|
|
|
|
it('should return correct template', async () => {
|
|
const template = await plugin.getTemplate();
|
|
expect(template).toBe('prompt');
|
|
});
|
|
|
|
it('should generate correct assertions', () => {
|
|
const assertions = plugin.getAssertions('test prompt');
|
|
expect(assertions).toHaveLength(1);
|
|
expect(assertions[0]).toEqual({
|
|
type: 'promptfoo:redteam:toxic-chat',
|
|
metric: 'ToxicChat',
|
|
});
|
|
});
|
|
|
|
it('should generate tests correctly', async () => {
|
|
const mockData: TestCase[] = [
|
|
{ vars: { user_input: 'toxic prompt', toxicity: 1, jailbreaking: 0 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mockData);
|
|
|
|
const tests = await plugin.generateTests(1);
|
|
|
|
expect(tests).toHaveLength(1);
|
|
expect(tests[0].vars).toEqual({ prompt: 'toxic prompt' });
|
|
expect(tests[0].assert).toHaveLength(1);
|
|
expect(tests[0].assert?.[0]?.type).toBe('promptfoo:redteam:toxic-chat');
|
|
});
|
|
});
|
|
|
|
describe('ToxicChatGrader', () => {
|
|
let grader: ToxicChatGrader;
|
|
|
|
beforeEach(() => {
|
|
grader = new ToxicChatGrader();
|
|
});
|
|
|
|
it('should have the correct plugin ID', () => {
|
|
expect(grader.id).toBe('promptfoo:redteam:toxic-chat');
|
|
});
|
|
|
|
it('should have appropriate rubric', () => {
|
|
expect(grader.rubric).toContain('evaluating an AI Agent');
|
|
expect(grader.rubric).toContain('toxic prompts and resists jailbreaking attempts');
|
|
expect(grader.rubric).toContain('ToxicChat dataset');
|
|
expect(grader.rubric).toContain('FAIL (score = 0)');
|
|
expect(grader.rubric).toContain('PASS (score = 1)');
|
|
expect(grader.rubric).toContain('reason, pass, score');
|
|
});
|
|
});
|
|
|
|
describe('fetchDataset', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it('should fetch and filter toxic prompts', async () => {
|
|
const mock: TestCase[] = [
|
|
{ vars: { user_input: 'toxic prompt', toxicity: 1, jailbreaking: 0 } },
|
|
{ vars: { user_input: 'safe prompt', toxicity: 0, jailbreaking: 0 } },
|
|
{ vars: { user_input: 'jailbreak attempt', toxicity: 0, jailbreaking: 1 } },
|
|
{ vars: { user_input: 'both toxic and jailbreak', toxicity: 1, jailbreaking: 1 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(3);
|
|
|
|
expect(result).toHaveLength(3);
|
|
// Should only include toxic or jailbreaking prompts
|
|
expect(result[0]?.vars?.prompt).toBe('toxic prompt');
|
|
expect(result[1]?.vars?.prompt).toBe('jailbreak attempt');
|
|
expect(result[2]?.vars?.prompt).toBe('both toxic and jailbreak');
|
|
});
|
|
|
|
it('should handle missing user_input field', async () => {
|
|
const mock: TestCase[] = [
|
|
{ vars: { toxicity: 1, jailbreaking: 0 } }, // Missing user_input
|
|
{ vars: { user_input: 'valid prompt', toxicity: 1, jailbreaking: 0 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(2);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.vars?.prompt).toBe('valid prompt');
|
|
});
|
|
|
|
it('should handle non-string user_input', async () => {
|
|
const mock: TestCase[] = [
|
|
{ vars: { user_input: 123, toxicity: 1, jailbreaking: 0 } }, // Non-string
|
|
{ vars: { user_input: 'valid prompt', toxicity: 1, jailbreaking: 0 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(2);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.vars?.prompt).toBe('valid prompt');
|
|
});
|
|
|
|
it('should filter out safe prompts', async () => {
|
|
const mock: TestCase[] = [
|
|
{ vars: { user_input: 'safe prompt 1', toxicity: 0, jailbreaking: 0 } },
|
|
{ vars: { user_input: 'safe prompt 2', toxicity: 0, jailbreaking: 0 } },
|
|
{ vars: { user_input: 'toxic prompt', toxicity: 1, jailbreaking: 0 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(5);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.vars?.prompt).toBe('toxic prompt');
|
|
});
|
|
|
|
it('should handle missing vars', async () => {
|
|
const mock: TestCase[] = [
|
|
{ vars: undefined }, // Missing vars
|
|
{ vars: { user_input: 'valid prompt', toxicity: 1, jailbreaking: 0 } },
|
|
];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(2);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.vars?.prompt).toBe('valid prompt');
|
|
});
|
|
|
|
it('should respect the limit parameter', async () => {
|
|
const mock: TestCase[] = Array(10)
|
|
.fill(null)
|
|
.map((_, i) => ({
|
|
vars: { user_input: `prompt ${i}`, toxicity: 1, jailbreaking: 0 },
|
|
}));
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
const result = await fetchDataset(3);
|
|
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
|
|
it('should handle fetch errors gracefully', async () => {
|
|
vi.mocked(fetchHuggingFaceDataset).mockRejectedValue(new Error('Network error'));
|
|
|
|
const result = await fetchDataset(3);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should call fetchHuggingFaceDataset with correct parameters', async () => {
|
|
const mock: TestCase[] = [{ vars: { user_input: 'test', toxicity: 1, jailbreaking: 0 } }];
|
|
vi.mocked(fetchHuggingFaceDataset).mockResolvedValue(mock);
|
|
|
|
await fetchDataset(5);
|
|
|
|
expect(fetchHuggingFaceDataset).toHaveBeenCalledWith(
|
|
'huggingface://datasets/lmsys/toxic-chat?config=toxicchat0124&split=train',
|
|
25, // 5 * 5 for filtering
|
|
);
|
|
});
|
|
});
|