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
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
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
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { Severity } from '../../src/redteam/constants';
|
|
import { getRiskCategorySeverityMap, getUnifiedConfig } from '../../src/redteam/sharedFrontend';
|
|
|
|
import type { Plugin } from '../../src/redteam/constants';
|
|
import type { SavedRedteamConfig } from '../../src/redteam/types';
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
describe('getRiskCategorySeverityMap', () => {
|
|
it('should return default severity map when no plugins provided', () => {
|
|
const result = getRiskCategorySeverityMap();
|
|
expect(result).toBeDefined();
|
|
expect(result['contracts']).toBe(Severity.Medium);
|
|
});
|
|
|
|
it('should override default severities with plugin severities', () => {
|
|
const plugins = [
|
|
{ id: 'contracts' as Plugin, severity: Severity.High },
|
|
{ id: 'politics' as Plugin, severity: Severity.Critical },
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
expect(result['contracts']).toBe(Severity.High);
|
|
expect(result['politics']).toBe(Severity.Critical);
|
|
});
|
|
|
|
it('should handle plugins without severity override', () => {
|
|
const plugins = [
|
|
{ id: 'contracts' as Plugin },
|
|
{ id: 'politics' as Plugin, severity: Severity.Critical },
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
expect(result['contracts']).toBe(Severity.Medium); // Default severity
|
|
expect(result['politics']).toBe(Severity.Critical);
|
|
});
|
|
|
|
it('should add severity entry for specific policy ID when plugin is a custom policy', () => {
|
|
const plugins = [
|
|
{
|
|
id: 'policy' as Plugin,
|
|
severity: Severity.Low,
|
|
config: { policy: { id: 'abc123', text: 'Do not discuss cats', name: 'No Cats' } },
|
|
},
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
// Should have entry for both 'policy' and the specific policy ID
|
|
expect(result['policy']).toBe(Severity.Low);
|
|
expect(result['abc123' as Plugin]).toBe(Severity.Low);
|
|
});
|
|
|
|
it('should handle multiple custom policies with different severities', () => {
|
|
const plugins = [
|
|
{
|
|
id: 'policy' as Plugin,
|
|
severity: Severity.Low,
|
|
config: { policy: { id: 'policy-1', text: 'Policy 1', name: 'Policy 1' } },
|
|
},
|
|
{
|
|
id: 'policy' as Plugin,
|
|
severity: Severity.Critical,
|
|
config: { policy: { id: 'policy-2', text: 'Policy 2', name: 'Policy 2' } },
|
|
},
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
// Each specific policy ID should have its own severity
|
|
expect(result['policy-1' as Plugin]).toBe(Severity.Low);
|
|
expect(result['policy-2' as Plugin]).toBe(Severity.Critical);
|
|
// The generic 'policy' key gets the last value (Critical)
|
|
expect(result['policy']).toBe(Severity.Critical);
|
|
});
|
|
|
|
it('should not add policy ID entry when policy config is missing', () => {
|
|
const plugins = [
|
|
{
|
|
id: 'policy' as Plugin,
|
|
severity: Severity.Low,
|
|
// No config.policy.id
|
|
},
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
expect(result['policy']).toBe(Severity.Low);
|
|
// Should not throw or add undefined keys
|
|
expect(Object.keys(result).filter((k) => k === 'undefined')).toHaveLength(0);
|
|
});
|
|
|
|
it('should not add policy ID entry for non-policy plugins', () => {
|
|
const plugins = [
|
|
{
|
|
id: 'contracts' as Plugin,
|
|
severity: Severity.High,
|
|
config: { policy: { id: 'should-not-be-added' } },
|
|
},
|
|
];
|
|
|
|
const result = getRiskCategorySeverityMap(plugins);
|
|
|
|
expect(result['contracts']).toBe(Severity.High);
|
|
expect(result['should-not-be-added' as Plugin]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('getUnifiedConfig', () => {
|
|
const baseConfig: SavedRedteamConfig = {
|
|
description: 'Test config',
|
|
prompts: ['test prompt'],
|
|
target: {
|
|
id: 'test-target',
|
|
config: {
|
|
sessionSource: 'test-session',
|
|
stateful: true,
|
|
apiKey: 'test-key',
|
|
},
|
|
},
|
|
plugins: ['test-plugin'],
|
|
strategies: ['basic'],
|
|
purpose: 'testing',
|
|
applicationDefinition: {},
|
|
entities: [],
|
|
};
|
|
|
|
it('should transform config correctly', () => {
|
|
const result = getUnifiedConfig(baseConfig);
|
|
|
|
expect(result.description).toBe('Test config');
|
|
expect(result.prompts).toEqual(['test prompt']);
|
|
// @ts-ignore
|
|
expect(result.targets[0].config.sessionSource).toBeUndefined();
|
|
// @ts-ignore
|
|
expect(result.targets[0].config.stateful).toBeUndefined();
|
|
expect(result.redteam.purpose).toBe('testing');
|
|
});
|
|
|
|
it('should handle defaultTest transformation', () => {
|
|
const configWithDefaultTest: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
defaultTest: {
|
|
vars: { test: 'value' },
|
|
options: { someOption: true },
|
|
},
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithDefaultTest);
|
|
|
|
expect(typeof result.defaultTest).toBe('object');
|
|
expect(result.defaultTest).toBeDefined();
|
|
// Type assertion since we've verified it's an object above
|
|
const defaultTest = result.defaultTest as Exclude<
|
|
typeof result.defaultTest,
|
|
string | undefined
|
|
>;
|
|
expect(defaultTest.vars).toEqual({ test: 'value' });
|
|
expect(defaultTest.options!.transformVars).toBe('{ ...vars, sessionId: context.uuid }');
|
|
});
|
|
|
|
it('should transform plugins correctly', () => {
|
|
const configWithPlugins: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
plugins: ['simple-plugin', { id: 'complex-plugin', config: { setting: true } }],
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithPlugins);
|
|
|
|
expect(result.redteam.plugins).toEqual([
|
|
{ id: 'simple-plugin' },
|
|
{ id: 'complex-plugin', config: { setting: true } },
|
|
]);
|
|
});
|
|
|
|
it('should transform strategies with stateful config', () => {
|
|
const configWithStrategies: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
strategies: ['basic', 'goat', { id: 'custom', config: { option: true } }],
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithStrategies);
|
|
|
|
expect(result.redteam.strategies).toEqual([
|
|
{ id: 'basic' },
|
|
{ id: 'goat', config: { stateful: true } },
|
|
{ id: 'custom', config: { option: true, stateful: true } },
|
|
]);
|
|
});
|
|
|
|
it('should handle maxConcurrency configuration', () => {
|
|
const configWithMaxConcurrency: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
maxConcurrency: 5,
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithMaxConcurrency);
|
|
expect(result.redteam.maxConcurrency).toBe(5);
|
|
|
|
const configWithoutMaxConcurrency = getUnifiedConfig(baseConfig);
|
|
expect(configWithoutMaxConcurrency.redteam.maxConcurrency).toBeUndefined();
|
|
});
|
|
|
|
it('should include testGenerationInstructions if provided', () => {
|
|
const configWithInstructions: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
testGenerationInstructions: 'Generate more tests',
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithInstructions);
|
|
|
|
expect(result.redteam.testGenerationInstructions).toBe('Generate more tests');
|
|
});
|
|
|
|
it('should include maxCharsPerMessage if provided', () => {
|
|
const configWithMaxChars: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
maxCharsPerMessage: 125,
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithMaxChars);
|
|
|
|
expect(result.redteam.maxCharsPerMessage).toBe(125);
|
|
});
|
|
|
|
it('should omit plugin config if empty', () => {
|
|
const configWithEmptyPluginConfig: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
plugins: [{ id: 'plugin-empty', config: {} }],
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithEmptyPluginConfig);
|
|
|
|
expect(result.redteam.plugins).toEqual([{ id: 'plugin-empty' }]);
|
|
});
|
|
|
|
it('should omit strategy config if not needed', () => {
|
|
const configWithSimpleStrategy: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
strategies: [{ id: 'basic', config: {} }],
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithSimpleStrategy);
|
|
|
|
expect(result.redteam.strategies).toEqual([{ id: 'basic' }]);
|
|
});
|
|
|
|
it('should add stateful to multi-turn strategies if stateful is true', () => {
|
|
const configWithMultiTurn: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
strategies: ['goat'],
|
|
target: {
|
|
...baseConfig.target,
|
|
config: {
|
|
...baseConfig.target.config,
|
|
stateful: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithMultiTurn);
|
|
|
|
expect(result.redteam.strategies).toEqual([{ id: 'goat', config: { stateful: true } }]);
|
|
});
|
|
|
|
it('should not add stateful config for multi-turn strategies if stateful is false', () => {
|
|
const configWithNonStateful: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
strategies: ['goat'],
|
|
target: {
|
|
...baseConfig.target,
|
|
config: {
|
|
...baseConfig.target.config,
|
|
stateful: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithNonStateful);
|
|
|
|
expect(result.redteam.strategies).toEqual([{ id: 'goat' }]);
|
|
});
|
|
|
|
it('should include frameworks when provided', () => {
|
|
const configWithFrameworks: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
frameworks: ['owasp:llm', 'nist:ai:measure'],
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithFrameworks);
|
|
|
|
expect(result.redteam.frameworks).toEqual(['owasp:llm', 'nist:ai:measure']);
|
|
});
|
|
|
|
it('should include provider when provided as string', () => {
|
|
const configWithProvider: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
provider: 'openai:chat:gpt-4',
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithProvider);
|
|
|
|
expect(result.redteam.provider).toBe('openai:chat:gpt-4');
|
|
});
|
|
|
|
it('should include provider when provided as object', () => {
|
|
const configWithProvider: SavedRedteamConfig = {
|
|
...baseConfig,
|
|
provider: {
|
|
id: 'openai:chat',
|
|
config: {
|
|
apiBaseUrl: 'http://192.168.1.1:1111/v1',
|
|
apiKey: 'sk-test',
|
|
model: 'Qwen3-test',
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = getUnifiedConfig(configWithProvider);
|
|
|
|
expect(result.redteam.provider).toEqual({
|
|
id: 'openai:chat',
|
|
config: {
|
|
apiBaseUrl: 'http://192.168.1.1:1111/v1',
|
|
apiKey: 'sk-test',
|
|
model: 'Qwen3-test',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should not include provider when not provided', () => {
|
|
const result = getUnifiedConfig(baseConfig);
|
|
|
|
expect(result.redteam.provider).toBeUndefined();
|
|
});
|
|
});
|