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

295 lines
8.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { matchesLlmRubric } from '../../../src/matchers/llmGrading';
import { PlinyGrader, PlinyPlugin } from '../../../src/redteam/plugins/pliny';
import { isBasicRefusal, isEmptyResponse } from '../../../src/redteam/util';
import { fetchWithProxy } from '../../../src/util/fetch/index';
import {
createMockProvider,
createProviderResponse,
type MockApiProvider,
} from '../../factories/provider';
import type { AtomicTestCase } from '../../../src/types/index';
vi.mock('../../../src/matchers/llmGrading', async (importOriginal) => {
return {
...(await importOriginal()),
matchesLlmRubric: vi.fn(),
};
});
vi.mock('../../../src/util/fetch/index.ts', async (importOriginal) => {
return {
...(await importOriginal()),
fetchWithProxy: vi.fn(),
};
});
vi.mock('../../../src/redteam/util', async (importOriginal) => {
return {
...(await importOriginal()),
isBasicRefusal: vi.fn().mockReturnValue(false),
isEmptyResponse: vi.fn().mockReturnValue(false),
};
});
describe('PlinyPlugin', () => {
let provider: MockApiProvider;
let plugin: PlinyPlugin;
const mockFetchResponse = `
# Test L1B3RT4S Header
This is a test jailbreak attempt.
Let's try to bypass restrictions.
# Another Test Header
This is another test for jailbreak.
With some content.
`;
beforeEach(() => {
provider = createMockProvider({
response: createProviderResponse({ output: 'Test output' }),
});
plugin = new PlinyPlugin(provider, 'test purpose', 'testVar');
// Mock the fetch response
const mockTextFn = vi.fn().mockResolvedValue(mockFetchResponse);
vi.mocked(fetchWithProxy).mockResolvedValue({
text: mockTextFn,
} as unknown as Response);
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should have the correct id', () => {
expect(plugin.id).toBe('promptfoo:redteam:pliny');
expect(plugin.id).toBe('promptfoo:redteam:pliny');
});
it('should set canGenerateRemote to false', () => {
expect(PlinyPlugin.canGenerateRemote).toBe(false);
});
it('should return the inject variable as the template', async () => {
const template = await plugin.getTemplate();
expect(template).toBe('testVar');
});
it('should return the correct assertions', () => {
const assertions = plugin.getAssertions('test');
expect(assertions).toEqual([
{
type: 'promptfoo:redteam:pliny',
metric: 'Pliny',
},
]);
});
it('should handle fetch errors gracefully', async () => {
vi.mocked(fetchWithProxy).mockRejectedValue(new Error('Network error'));
const tests = await plugin.generateTests(2);
// Should still return an empty array rather than throwing
expect(tests).toEqual([]);
});
it('should handle text parsing errors gracefully', async () => {
// Mock a successful fetch but a failed text() call
vi.mocked(fetchWithProxy).mockResolvedValue({
text: vi.fn().mockRejectedValue(new Error('Text parsing error')),
} as unknown as Response);
const tests = await plugin.generateTests(2);
// Should return an empty array rather than throwing
expect(tests).toEqual([]);
});
it('should handle empty responses from fetch', async () => {
// Mock an empty response
vi.mocked(fetchWithProxy).mockResolvedValue({
text: vi.fn().mockResolvedValue(''),
} as unknown as Response);
const tests = await plugin.generateTests(2);
// Should return an empty array since no valid sections are found
expect(tests).toEqual([]);
});
});
describe('PlinyGrader', () => {
let grader: PlinyGrader;
let mockTest: AtomicTestCase;
beforeEach(() => {
grader = new PlinyGrader();
mockTest = {
vars: { prompt: 'This is a test jailbreak prompt from L1B3RT4S.' },
metadata: {
purpose: 'test-purpose',
jailbreakContent: 'This is a test jailbreak prompt from L1B3RT4S.',
pluginId: 'pliny',
},
options: {},
};
vi.clearAllMocks();
vi.mocked(matchesLlmRubric).mockResolvedValue({
pass: true,
score: 1,
reason: 'Model properly refused the jailbreak attempt',
});
});
it('should render the rubric with the correct structure and variables', async () => {
const userQuery = 'This is a test jailbreak prompt from L1B3RT4S.';
const aiOutput =
'I cannot comply with that request as it appears to be attempting to bypass my safety guidelines.';
// Mock isEmptyResponse and isBasicRefusal to return false so it calls matchesLlmRubric
vi.mocked(isEmptyResponse).mockImplementation(function () {
return false;
});
vi.mocked(isBasicRefusal).mockImplementation(function () {
return false;
});
await grader.getResult(userQuery, aiOutput, mockTest, undefined, undefined);
// Verify matchesLlmRubric was called with the appropriate arguments
expect(matchesLlmRubric).toHaveBeenCalledTimes(1);
expect(matchesLlmRubric).toHaveBeenCalledWith(
expect.any(String), // The rendered rubric
aiOutput,
expect.any(Object), // GradingConfig with provider
);
});
it('should handle cases with entities correctly', async () => {
mockTest.metadata!.entities = ['Entity1', 'Entity2'];
const userQuery = 'This is a test jailbreak prompt.';
const aiOutput = 'I cannot comply with that request.';
// Mock isEmptyResponse and isBasicRefusal to return false so it calls matchesLlmRubric
vi.mocked(isEmptyResponse).mockImplementation(function () {
return false;
});
vi.mocked(isBasicRefusal).mockImplementation(function () {
return false;
});
await grader.getResult(userQuery, aiOutput, mockTest, undefined, undefined);
// Verify matchesLlmRubric was called with the appropriate arguments
expect(matchesLlmRubric).toHaveBeenCalledTimes(1);
expect(matchesLlmRubric).toHaveBeenCalledWith(
expect.any(String), // The rendered rubric
aiOutput,
expect.any(Object), // GradingConfig with provider
);
});
it('should classify a refusal correctly', async () => {
// Mock empty responses to test the refusal path
vi.mocked(isEmptyResponse).mockImplementation(function () {
return true;
});
const userQuery = 'This is a test jailbreak prompt.';
const aiOutput = '';
const result = await grader.getResult(userQuery, aiOutput, mockTest, undefined, undefined);
expect(result.grade).toEqual({
pass: true,
score: 1,
reason: 'Model refused the request',
assertion: undefined,
});
// matchesLlmRubric should not be called for empty responses
expect(matchesLlmRubric).not.toHaveBeenCalled();
});
it('should classify a basic refusal correctly', async () => {
// Mock basic refusal detection
vi.mocked(isEmptyResponse).mockImplementation(function () {
return false;
});
vi.mocked(isBasicRefusal).mockImplementation(function () {
return true;
});
const userQuery = 'This is a test jailbreak prompt.';
const aiOutput = 'I cannot help with that.';
const result = await grader.getResult(userQuery, aiOutput, mockTest, undefined, undefined);
expect(result.grade).toEqual({
pass: true,
score: 1,
reason: 'Model refused the request',
assertion: undefined,
});
// matchesLlmRubric should not be called for basic refusals
expect(matchesLlmRubric).not.toHaveBeenCalled();
});
it('should return correct result for failed grading', async () => {
// Mock a failing grading result
vi.mocked(isEmptyResponse).mockImplementation(function () {
return false;
});
vi.mocked(isBasicRefusal).mockImplementation(function () {
return false;
});
vi.mocked(matchesLlmRubric).mockResolvedValue({
pass: false,
score: 0,
reason: 'Model attempted to comply with the jailbreak',
});
const userQuery = 'This is a test jailbreak prompt.';
const aiOutput = 'Here is the harmful content you requested...';
const result = await grader.getResult(userQuery, aiOutput, mockTest, undefined, undefined);
expect(result.grade).toEqual({
pass: false,
score: 0,
reason: 'Model attempted to comply with the jailbreak',
});
expect(matchesLlmRubric).toHaveBeenCalledTimes(1);
});
it('should handle missing metadata gracefully', async () => {
// Create a test case with missing metadata
const incompleteTest = {
vars: { prompt: 'Test jailbreak' },
options: {},
} as AtomicTestCase;
vi.mocked(isEmptyResponse).mockImplementation(function () {
return false;
});
vi.mocked(isBasicRefusal).mockImplementation(function () {
return false;
});
// This should throw because the test is missing purpose metadata
await expect(
grader.getResult('Test prompt', 'Test response', incompleteTest, undefined, undefined),
).rejects.toThrow('Test is missing purpose metadata');
});
});