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

168 lines
5.8 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { describe, expect, it } from 'vitest';
const PLUGINS_DIR = path.join(__dirname, '../../../src/redteam/plugins');
const DOCS_DIR = path.join(__dirname, '../../../site/docs/red-team/plugins');
function getFiles(dir: string, extension: string, excludes: string[] = []): string[] {
return fs
.readdirSync(dir)
.filter((file) => file.endsWith(extension) && !excludes.includes(file))
.map((file) => file.replace(extension, ''));
}
function toKebabCase(str: string): string {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
function toCamelCase(str: string): string {
return str.replace(/-./g, (x) => x[1].toUpperCase());
}
async function loadPluginsFromConstants(): Promise<Set<string>> {
const constantsPath = path.join(__dirname, '../../../src/redteam/constants/plugins.ts');
// Import the module to get the actual values
const pluginsModule = await import(constantsPath);
const allPlugins = new Set<string>();
// Collect all plugins from different arrays
const pluginArrays = [
'FOUNDATION_PLUGINS',
'AGENTIC_PLUGINS',
'BASE_PLUGINS',
'ADDITIONAL_PLUGINS',
'CONFIG_REQUIRED_PLUGINS',
'PII_PLUGINS',
'BIAS_PLUGINS',
];
// Include remote-only plugin IDs (includes coding-agent plugins)
if (pluginsModule.REMOTE_ONLY_PLUGIN_IDS) {
(pluginsModule.REMOTE_ONLY_PLUGIN_IDS as string[]).forEach((p: string) => allPlugins.add(p));
}
// Include collection IDs so docs entries for collections (e.g., coding-agent:core) aren't flagged as orphaned
if (pluginsModule.COLLECTIONS) {
(pluginsModule.COLLECTIONS as string[]).forEach((c: string) => allPlugins.add(c));
}
for (const arrayName of pluginArrays) {
const array = pluginsModule[arrayName];
if (Array.isArray(array)) {
array.forEach((plugin: string) => allPlugins.add(plugin));
}
}
// Add harm plugins
if (pluginsModule.HARM_PLUGINS) {
Object.keys(pluginsModule.HARM_PLUGINS).forEach((plugin) => allPlugins.add(plugin));
}
return allPlugins;
}
async function loadPluginsFromDocs(): Promise<Set<string>> {
const docsPath = path.join(__dirname, '../../../site/docs/_shared/data/plugins.ts');
// Import the module to get the actual values
const docsModule = await import(docsPath);
const allPlugins = new Set<string>();
// Extract pluginId from each plugin in the PLUGINS array
if (docsModule.PLUGINS && Array.isArray(docsModule.PLUGINS)) {
docsModule.PLUGINS.forEach((plugin: any) => {
if (plugin.pluginId) {
allPlugins.add(plugin.pluginId);
}
});
} else {
throw new Error('PLUGINS array not found in docs file');
}
return allPlugins;
}
describe('Plugin Documentation', () => {
const pluginFiles = getFiles(PLUGINS_DIR, '.ts', [
'index.ts',
'base.ts',
'dataExfil.ts', // Grader class, not a user-facing plugin
'imageDatasetPluginBase.ts',
'imageDatasetUtils.ts',
'multiInputFormat.ts',
]);
const docFiles = getFiles(DOCS_DIR, '.md', ['_category_.json']);
describe('Plugin files and documentation files', () => {
it('should have matching plugin and documentation files', () => {
const pluginSet = new Set(pluginFiles.map(toKebabCase));
const docSet = new Set(docFiles);
// Check that all plugins have corresponding docs
pluginSet.forEach((plugin) => {
expect(docSet.has(plugin)).toBe(true);
});
});
it('should have correct naming conventions for plugins and docs', () => {
pluginFiles.forEach((plugin) => {
const kebabPlugin = toKebabCase(plugin);
expect(docFiles).toContain(kebabPlugin);
expect(pluginFiles).toContain(toCamelCase(kebabPlugin));
});
});
});
describe('Plugin data synchronization', () => {
it('should have all plugins from constants in documentation data', async () => {
const constantsPlugins = await loadPluginsFromConstants();
const docsPlugins = await loadPluginsFromDocs();
// Collections are shorthand aliases (e.g., 'default', 'foundation') that don't need
// individual docs entries — they expand to plugin IDs that have their own entries.
const pluginsModule = await import(
path.join(__dirname, '../../../src/redteam/constants/plugins.ts')
);
const collections = new Set(pluginsModule.COLLECTIONS as string[]);
// Find plugins missing from docs (excluding collection aliases)
const missingFromDocs = [...constantsPlugins].filter(
(plugin) => !docsPlugins.has(plugin) && !collections.has(plugin),
);
expect(
missingFromDocs,
`Plugins missing from documentation data. Add these plugins to site/docs/_shared/data/plugins.ts:\n${missingFromDocs.map((plugin) => ` - ${plugin}`).join('\n')}`,
).toEqual([]);
});
it('should not have orphaned plugins in documentation data', async () => {
const constantsPlugins = await loadPluginsFromConstants();
const docsPlugins = await loadPluginsFromDocs();
// Find plugins missing from constants
const missingFromConstants = [...docsPlugins].filter(
(plugin) => !constantsPlugins.has(plugin),
);
expect(
missingFromConstants,
`Orphaned plugins in documentation data. Remove these from site/docs/_shared/data/plugins.ts or add them to src/redteam/constants/plugins.ts:\n${missingFromConstants.map((plugin) => ` - ${plugin}`).join('\n')}`,
).toEqual([]);
});
it('should have plugin constants and documentation data files', async () => {
const constantsPath = path.join(__dirname, '../../../src/redteam/constants/plugins.ts');
const docsPath = path.join(__dirname, '../../../site/docs/_shared/data/plugins.ts');
expect(fs.existsSync(constantsPath)).toBe(true);
expect(fs.existsSync(docsPath)).toBe(true);
});
});
});