Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

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();
});
});