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
174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
ADDITIONAL_PLUGINS,
|
|
ALL_PLUGINS,
|
|
BASE_PLUGINS,
|
|
BIAS_PLUGINS,
|
|
CONFIG_REQUIRED_PLUGINS,
|
|
HARM_PLUGINS,
|
|
PII_PLUGINS,
|
|
} from '../../../src/redteam/constants';
|
|
|
|
describe('Plugin IDs', () => {
|
|
const findPluginIdAssignments = (fileContent: string): string[] => {
|
|
// Look for patterns like `id = 'plugin-name'` or `PLUGIN_ID = 'plugin-name'`
|
|
const idAssignmentRegex = /\b(id|PLUGIN_ID)\s*=\s*['"]([^'"]+)['"]/g;
|
|
const matches = [];
|
|
let match;
|
|
while ((match = idAssignmentRegex.exec(fileContent)) !== null) {
|
|
matches.push(match[2]);
|
|
}
|
|
return matches;
|
|
};
|
|
|
|
it('should use plugin IDs that match those defined in constants', () => {
|
|
// Get all plugin implementation files
|
|
const pluginDir = path.resolve(__dirname, '../../../src/redteam/plugins');
|
|
const pluginFiles = fs
|
|
.readdirSync(pluginDir)
|
|
.filter(
|
|
(file) =>
|
|
file.endsWith('.ts') &&
|
|
!file.endsWith('.d.ts') &&
|
|
file !== 'index.ts' &&
|
|
file !== 'base.ts',
|
|
);
|
|
|
|
// Track all plugin IDs used in implementations
|
|
const usedPluginIds: string[] = [];
|
|
|
|
pluginFiles.forEach((file) => {
|
|
const filePath = path.join(pluginDir, file);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const ids = findPluginIdAssignments(content);
|
|
|
|
if (ids.length > 0) {
|
|
usedPluginIds.push(...ids);
|
|
}
|
|
});
|
|
|
|
// Also check subdirectories
|
|
const subdirectories = fs
|
|
.readdirSync(pluginDir, { withFileTypes: true })
|
|
.filter((dirent) => dirent.isDirectory())
|
|
.map((dirent) => dirent.name);
|
|
|
|
subdirectories.forEach((subdir) => {
|
|
const subdirPath = path.join(pluginDir, subdir);
|
|
const subFiles = fs
|
|
.readdirSync(subdirPath)
|
|
.filter((file) => file.endsWith('.ts') && !file.endsWith('.d.ts') && file !== 'index.ts');
|
|
|
|
subFiles.forEach((file) => {
|
|
const filePath = path.join(subdirPath, file);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const ids = findPluginIdAssignments(content);
|
|
|
|
if (ids.length > 0) {
|
|
usedPluginIds.push(...ids);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Filter out duplicates
|
|
const uniqueIds = [...new Set(usedPluginIds)];
|
|
|
|
// Create a comprehensive list of all expected plugin IDs, including the prefixed versions
|
|
const expectedPrefixedPluginIds = new Set<string>();
|
|
|
|
// Add common plugin format - 'promptfoo:redteam:plugin-name'
|
|
ALL_PLUGINS.forEach((pluginId) => {
|
|
if (typeof pluginId === 'string' && !pluginId.includes(':')) {
|
|
expectedPrefixedPluginIds.add(`promptfoo:redteam:${pluginId}`);
|
|
}
|
|
});
|
|
|
|
// Add harm plugins which might have different prefixes
|
|
Object.keys(HARM_PLUGINS).forEach((harmPlugin) => {
|
|
if (typeof harmPlugin === 'string') {
|
|
const fullPluginId = `promptfoo:redteam:${harmPlugin}`;
|
|
expectedPrefixedPluginIds.add(fullPluginId);
|
|
}
|
|
});
|
|
|
|
// Add PII plugins with their prefixes
|
|
PII_PLUGINS.forEach((piiPlugin) => {
|
|
expectedPrefixedPluginIds.add(`promptfoo:redteam:${piiPlugin}`);
|
|
});
|
|
|
|
// Add special case for general PII plugin
|
|
expectedPrefixedPluginIds.add('promptfoo:redteam:pii');
|
|
|
|
// Add special case for general harmful plugin
|
|
expectedPrefixedPluginIds.add('promptfoo:redteam:harmful');
|
|
|
|
// Add special cases from harm sub-categories
|
|
// These are handled specially in the constants file as nested objects
|
|
uniqueIds.forEach((id) => {
|
|
if (id.startsWith('promptfoo:redteam:harmful:')) {
|
|
expectedPrefixedPluginIds.add(id);
|
|
}
|
|
});
|
|
|
|
// Handle special case: 'policy' without prefix
|
|
uniqueIds.forEach((id) => {
|
|
if (id === 'policy') {
|
|
// This is a special case where the ID is not prefixed in the code
|
|
}
|
|
});
|
|
|
|
// For each plugin ID found in the implementations, check if it matches an expected format
|
|
const unexpectedPlugins: { id: string; baseId: string }[] = [];
|
|
const idsMissingPrefix: string[] = [];
|
|
|
|
uniqueIds.forEach((id) => {
|
|
if (!id.startsWith('promptfoo:redteam:') && id !== 'policy') {
|
|
idsMissingPrefix.push(id);
|
|
}
|
|
|
|
if (!expectedPrefixedPluginIds.has(id) && id !== 'policy') {
|
|
const baseId = id.replace('promptfoo:redteam:', '');
|
|
const isHarmSubcategory = baseId.startsWith('harmful:');
|
|
|
|
// Special case for harm subcategories
|
|
if (!isHarmSubcategory) {
|
|
// Non-harm plugins should match one of the plugin types in constants
|
|
const allPluginsList = [
|
|
...BASE_PLUGINS,
|
|
...ADDITIONAL_PLUGINS,
|
|
...CONFIG_REQUIRED_PLUGINS,
|
|
...PII_PLUGINS,
|
|
...BIAS_PLUGINS,
|
|
'pii', // Add the general pii plugin
|
|
'bias', // Add the general bias plugin
|
|
'harmful', // Add the general harmful plugin
|
|
...Object.keys(HARM_PLUGINS),
|
|
];
|
|
|
|
const matchesExpectedPlugin = allPluginsList.some((p) => baseId === p);
|
|
|
|
if (!matchesExpectedPlugin) {
|
|
unexpectedPlugins.push({ id, baseId });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Make a single assertion for all unexpected plugins
|
|
expect(
|
|
{ unexpectedPlugins, idsMissingPrefix },
|
|
[
|
|
'Unexpected plugin IDs:',
|
|
...unexpectedPlugins.map(({ id, baseId }) => ` - ${id} (base: ${baseId})`),
|
|
idsMissingPrefix.length > 0 ? 'IDs missing promptfoo:redteam: prefix:' : '',
|
|
...idsMissingPrefix.map((id) => ` - ${id}`),
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n'),
|
|
).toEqual({ unexpectedPlugins: [], idsMissingPrefix: [] });
|
|
});
|
|
});
|