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

883 lines
28 KiB
TypeScript

import { beforeEach, describe, expect, it, type MockedFunction, vi } from 'vitest';
import * as cache from '../../../src/cache';
import logger from '../../../src/logger';
import * as imageDatasetUtils from '../../../src/redteam/plugins/imageDatasetUtils';
import {
CATEGORY_NAMES,
VALID_GRADES,
VLSU_CATEGORIES,
VLSUDatasetManager,
VLSUGrader,
VLSUPlugin,
} from '../../../src/redteam/plugins/vlsu';
import { createMockProvider, createProviderResponse } from '../../factories/provider';
import type { ApiProvider, AtomicTestCase } from '../../../src/types/index';
vi.mock('../../../src/logger');
vi.mock('../../../src/cache');
vi.mock('../../../src/redteam/plugins/imageDatasetUtils', async () => ({
...(await vi.importActual('../../../src/redteam/plugins/imageDatasetUtils')),
fetchImageAsBase64: vi.fn(),
}));
// Helper to create mock CSV data matching VLSU format
function createMockCSVRecord(overrides: Partial<Record<string, string>> = {}) {
return {
prompt: 'Sample test prompt',
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
image_category: '',
text_category: '',
combined_category: 'C9: Interpersonal Violence',
web_path: 'https://example.com/image.jpg',
uuid: `test-uuid-${Math.random().toString(36).substring(7)}`,
...overrides,
};
}
// Helper to create mock CSV text
function createMockCSVText(records: Array<Record<string, string>>): string {
if (records.length === 0) {
return 'prompt,image_grade,consensus_text_grade,consensus_combined_grade,image_category,text_category,combined_category,web_path,uuid';
}
const headers = Object.keys(records[0]).join(',');
const rows = records.map((r) =>
Object.values(r)
.map((v) => `"${v}"`)
.join(','),
);
return [headers, ...rows].join('\n');
}
describe('VLSUPlugin', () => {
const mockProvider = createMockProvider({
response: createProviderResponse({
output: 'test',
tokenUsage: { total: 10, prompt: 5, completion: 5 },
}),
});
const mockFetchWithCache = cache.fetchWithCache as MockedFunction<typeof cache.fetchWithCache>;
const mockFetchImageAsBase64 = imageDatasetUtils.fetchImageAsBase64 as MockedFunction<
typeof imageDatasetUtils.fetchImageAsBase64
>;
beforeEach(() => {
vi.clearAllMocks();
VLSUDatasetManager.clearCache();
});
describe('constructor', () => {
it('should initialize with default config', () => {
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
expect(plugin.id).toBe('promptfoo:redteam:vlsu');
});
it('should validate categories in config', () => {
const config = {
categories: ['C1', 'invalid-category'] as string[],
};
new VLSUPlugin(mockProvider, 'test purpose', 'image', config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid categories: invalid-category'),
);
});
it('should validate grades in config', () => {
const config = {
grades: ['safe', 'invalid-grade'] as any,
};
new VLSUPlugin(mockProvider, 'test purpose', 'image', config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid grades: invalid-grade'),
);
});
it('should validate severity patterns in config', () => {
const config = {
severityPatterns: ['safe_safe_unsafe', 'invalid_pattern'] as string[],
};
new VLSUPlugin(mockProvider, 'test purpose', 'image', config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Invalid severity pattern: invalid_pattern'),
);
});
it('should set canGenerateRemote to false', () => {
expect(VLSUPlugin.canGenerateRemote).toBe(false);
});
});
describe('getTemplate', () => {
it('should return the inject variable', async () => {
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'myImage', {});
const template = await plugin.getTemplate();
expect(template).toBe('myImage');
});
});
describe('getAssertions', () => {
it('should generate correct assertions', () => {
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const assertions = (plugin as any).getAssertions('test prompt');
expect(assertions).toEqual([
{
type: 'promptfoo:redteam:vlsu',
metric: 'VLSU',
},
]);
});
});
describe('constants', () => {
it('should have all 15 VLSU categories', () => {
expect(VLSU_CATEGORIES).toHaveLength(15);
expect(VLSU_CATEGORIES).toContain('C1');
expect(VLSU_CATEGORIES).toContain('C15');
});
it('should have human-readable names for all categories', () => {
for (const category of VLSU_CATEGORIES) {
expect(CATEGORY_NAMES[category]).toBeDefined();
expect(typeof CATEGORY_NAMES[category]).toBe('string');
}
});
it('should have valid grades defined', () => {
expect(VALID_GRADES).toContain('safe');
expect(VALID_GRADES).toContain('unsafe');
expect(VALID_GRADES).toContain('borderline');
expect(VALID_GRADES).toContain('not_sure');
});
});
describe('generateTests', () => {
it('should generate test cases from CSV records', async () => {
const mockRecords = [
createMockCSVRecord({
prompt: 'Test prompt 1',
combined_category: 'C4: Self-Harm and Suicide',
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
createMockCSVRecord({
prompt: 'Test prompt 2',
combined_category: 'C9: Interpersonal Violence',
image_grade: 'borderline',
consensus_text_grade: 'safe',
consensus_combined_grade: 'borderline',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(2);
expect(tests).toHaveLength(2);
expect(tests[0].vars?.image).toBeDefined();
expect(tests[0].metadata?.combinedCategory).toBeDefined();
expect(tests[0].metadata?.vlsuSeverityPattern).toBeDefined();
});
it('should filter by categories when configured', async () => {
const mockRecords = [
createMockCSVRecord({ combined_category: 'C4: Self-Harm and Suicide' }),
createMockCSVRecord({ combined_category: 'C9: Interpersonal Violence' }),
createMockCSVRecord({ combined_category: 'C12: Terrorism and Violent Extremism' }),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
categories: ['C4'],
});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(tests[0].metadata?.combinedCategory).toBe('C4');
});
it('should filter by grades when configured', async () => {
const mockRecords = [
createMockCSVRecord({ consensus_combined_grade: 'safe' }),
createMockCSVRecord({ consensus_combined_grade: 'unsafe' }),
createMockCSVRecord({ consensus_combined_grade: 'borderline' }),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
grades: ['unsafe'],
});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(tests[0].metadata?.combinedGrade).toBe('unsafe');
});
it('should filter by severity patterns when configured', async () => {
const mockRecords = [
createMockCSVRecord({
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
createMockCSVRecord({
image_grade: 'unsafe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'borderline',
}),
createMockCSVRecord({
image_grade: 'borderline',
consensus_text_grade: 'borderline',
consensus_combined_grade: 'unsafe',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
severityPatterns: ['safe_safe_unsafe'],
});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(tests[0].metadata?.vlsuSeverityPattern).toBe('safe_safe_unsafe');
});
it('should filter for compositional cases when compositionalOnly is true', async () => {
const mockRecords = [
// Compositional case: safe + safe = unsafe
createMockCSVRecord({
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
// Not compositional: unsafe + safe = unsafe
createMockCSVRecord({
image_grade: 'unsafe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
// Not compositional: safe + safe = safe
createMockCSVRecord({
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'safe',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
compositionalOnly: true,
});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(tests[0].metadata?.vlsuIsCompositional).toBe(true);
expect(tests[0].metadata?.imageGrade).toBe('safe');
expect(tests[0].metadata?.textGrade).toBe('safe');
expect(tests[0].metadata?.combinedGrade).toBe('unsafe');
});
it('should throw error when no records match filters', async () => {
const mockRecords = [createMockCSVRecord({ combined_category: 'C4: Self-Harm and Suicide' })];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
categories: ['C15'], // No records match this
});
await expect(plugin.generateTests(5)).rejects.toThrow('Failed to generate tests');
});
it('should throw error when CSV fetch fails', async () => {
mockFetchWithCache.mockResolvedValue({
status: 404,
statusText: 'Not Found',
data: null,
cached: false,
} as any);
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
await expect(plugin.generateTests(5)).rejects.toThrow('Failed to generate tests');
});
it('should skip records with failed image fetches', async () => {
const mockRecords = [
createMockCSVRecord({ uuid: 'success-1' }),
createMockCSVRecord({ uuid: 'fail-1' }),
createMockCSVRecord({ uuid: 'success-2' }),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
// Fail one specific call deterministically (second call fails)
let callCount = 0;
mockFetchImageAsBase64.mockImplementation(async () => {
callCount++;
if (callCount === 2) {
return null;
}
return 'data:image/jpeg;base64,test';
});
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(3);
// Should have exactly 2 tests (one failed image fetch is skipped)
expect(tests.length).toBe(2);
});
it('should warn when fewer records are available than requested', async () => {
const mockRecords = [createMockCSVRecord()];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Requested 10 tests but only 1 records'),
);
});
it('should include prompt as separate variable by default', async () => {
const mockRecords = [createMockCSVRecord({ prompt: 'Test multimodal prompt' })];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(1);
expect(tests).toHaveLength(1);
// Image is the base64 string
expect(tests[0].vars?.image).toBe('data:image/jpeg;base64,test');
// Prompt is a separate variable
expect(tests[0].vars?.prompt).toBe('Test multimodal prompt');
});
it('should not include prompt variable when includePrompt is false', async () => {
const mockRecords = [createMockCSVRecord({ prompt: 'Test prompt' })];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
includePrompt: false,
});
const tests = await plugin.generateTests(1);
expect(tests).toHaveLength(1);
expect(tests[0].vars?.image).toBe('data:image/jpeg;base64,test');
expect(tests[0].vars?.prompt).toBeUndefined();
});
});
describe('metadata population', () => {
it('should populate all VLSU-specific metadata fields', async () => {
const mockRecords = [
createMockCSVRecord({
prompt: 'Test prompt',
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
combined_category: 'C4: Self-Harm and Suicide',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(1);
const metadata = tests[0].metadata;
expect(metadata?.vlsuCategory).toBe('C4');
expect(metadata?.vlsuCategoryName).toBe('Self-Harm and Suicide');
expect(metadata?.vlsuGrade).toBe('unsafe');
expect(metadata?.vlsuSeverityPattern).toBe('safe_safe_unsafe');
expect(metadata?.vlsuIsCompositional).toBe(true);
expect(metadata?.imageGrade).toBe('safe');
expect(metadata?.textGrade).toBe('safe');
expect(metadata?.combinedGrade).toBe('unsafe');
expect(metadata?.originalPrompt).toBe('Test prompt');
});
});
});
describe('VLSUDatasetManager', () => {
const mockFetchWithCache = cache.fetchWithCache as MockedFunction<typeof cache.fetchWithCache>;
const mockFetchImageAsBase64 = imageDatasetUtils.fetchImageAsBase64 as MockedFunction<
typeof imageDatasetUtils.fetchImageAsBase64
>;
beforeEach(() => {
vi.clearAllMocks();
VLSUDatasetManager.clearCache();
});
describe('singleton pattern', () => {
it('should return same instance on multiple calls', () => {
const instance1 = VLSUDatasetManager.getInstance();
const instance2 = VLSUDatasetManager.getInstance();
expect(instance1).toBe(instance2);
});
it('should clear cache correctly', async () => {
const mockRecords = [createMockCSVRecord()];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const manager = VLSUDatasetManager.getInstance();
await manager.getFilteredRecords(1, {});
// Clear and fetch again
VLSUDatasetManager.clearCache();
await manager.getFilteredRecords(1, {});
// Should fetch twice (cache was cleared)
expect(mockFetchWithCache).toHaveBeenCalledTimes(2);
});
});
describe('CSV caching', () => {
it('should cache CSV data after first fetch', async () => {
const mockRecords = [createMockCSVRecord()];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const manager = VLSUDatasetManager.getInstance();
await manager.getFilteredRecords(1, {});
await manager.getFilteredRecords(1, {});
// CSV should only be fetched once
expect(mockFetchWithCache).toHaveBeenCalledTimes(1);
});
});
describe('category code extraction', () => {
it('should extract category codes from full category strings', async () => {
const mockRecords = [
createMockCSVRecord({ combined_category: 'C12: Terrorism and Violent Extremism' }),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const manager = VLSUDatasetManager.getInstance();
const records = await manager.getFilteredRecords(1, {});
expect(records[0].combinedCategory).toBe('C12');
});
it('should handle empty category strings', async () => {
const mockRecords = [createMockCSVRecord({ combined_category: '' })];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const manager = VLSUDatasetManager.getInstance();
const records = await manager.getFilteredRecords(1, {});
expect(records[0].combinedCategory).toBe('');
});
});
describe('severity pattern computation', () => {
it('should compute correct severity patterns', async () => {
const mockRecords = [
createMockCSVRecord({
image_grade: 'borderline',
consensus_text_grade: 'unsafe',
consensus_combined_grade: 'unsafe',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const manager = VLSUDatasetManager.getInstance();
const records = await manager.getFilteredRecords(1, {});
expect(records[0].severityPattern).toBe('borderline_unsafe_unsafe');
});
});
});
describe('VLSUGrader', () => {
const grader = new VLSUGrader();
describe('id', () => {
it('should have correct plugin ID', () => {
expect(grader.id).toBe('promptfoo:redteam:vlsu');
});
});
describe('getRubricForTest', () => {
it('should generate rubric for C4 category', () => {
const test: AtomicTestCase = {
metadata: {
vlsuCategory: 'C4',
combinedCategory: 'C4',
},
} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).toContain('(C4)');
expect(rubric).toContain('Self-Harm and Suicide');
});
it('should generate rubric for C12 category', () => {
const test: AtomicTestCase = {
metadata: {
vlsuCategory: 'C12',
},
} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).toContain('Terrorism');
});
it('should add compositional context for compositional cases', () => {
const test: AtomicTestCase = {
metadata: {
vlsuCategory: 'C9',
isCompositional: true,
},
} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).toContain('CRITICAL');
expect(rubric).toContain('SAFE image + SAFE text = UNSAFE combination');
});
it('should not add compositional context for non-compositional cases', () => {
const test: AtomicTestCase = {
metadata: {
vlsuCategory: 'C9',
isCompositional: false,
},
} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).not.toContain('CRITICAL: This is a compositional safety case');
});
it('should use default guidance for unknown categories', () => {
const test: AtomicTestCase = {
metadata: {
vlsuCategory: 'unknown',
},
} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).toContain('compositional safety test');
});
it('should handle missing metadata', () => {
const test: AtomicTestCase = {} as AtomicTestCase;
const rubric = grader.getRubricForTest(test);
expect(rubric).toContain('unknown');
});
});
describe('guidance', () => {
it('should have guidance for all 15 VLSU categories', () => {
for (const category of VLSU_CATEGORIES) {
expect((grader as any).guidance[category]).toBeDefined();
expect(typeof (grader as any).guidance[category]).toBe('string');
}
});
it('should have default guidance', () => {
expect((grader as any).guidance.default).toBeDefined();
});
it('should have specific guidance for critical categories', () => {
expect((grader as any).guidance.C4).toContain('Self-Harm');
expect((grader as any).guidance.C12).toContain('Terrorism');
expect((grader as any).guidance.C15).toContain('exploitation');
});
});
});
describe('edge cases', () => {
const mockFetchWithCache = cache.fetchWithCache as MockedFunction<typeof cache.fetchWithCache>;
const mockFetchImageAsBase64 = imageDatasetUtils.fetchImageAsBase64 as MockedFunction<
typeof imageDatasetUtils.fetchImageAsBase64
>;
const mockProvider: ApiProvider = {
id: () => 'test-provider',
callApi: async () => ({ output: 'test', tokenUsage: { total: 10, prompt: 5, completion: 5 } }),
};
beforeEach(() => {
vi.clearAllMocks();
VLSUDatasetManager.clearCache();
});
describe('small n values', () => {
it('should handle n=1 correctly', async () => {
const mockRecords = [createMockCSVRecord()];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(1);
expect(tests).toHaveLength(1);
});
it('should handle n=0 correctly', async () => {
const mockRecords = [createMockCSVRecord()];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(0);
expect(tests).toHaveLength(0);
});
});
describe('not_sure grade handling', () => {
it('should normalize not_sure grades correctly', async () => {
const mockRecords = [
createMockCSVRecord({
image_grade: 'not_sure',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {});
const tests = await plugin.generateTests(1);
expect(tests[0].metadata?.imageGrade).toBe('not_sure');
expect(tests[0].metadata?.vlsuSeverityPattern).toBe('not_sure_safe_unsafe');
});
});
describe('mixed filters', () => {
it('should apply multiple filter types simultaneously', async () => {
const mockRecords = [
// Matches all filters
createMockCSVRecord({
combined_category: 'C9: Interpersonal Violence',
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
// Wrong category
createMockCSVRecord({
combined_category: 'C4: Self-Harm',
image_grade: 'safe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'unsafe',
}),
// Wrong grade
createMockCSVRecord({
combined_category: 'C9: Interpersonal Violence',
image_grade: 'unsafe',
consensus_text_grade: 'safe',
consensus_combined_grade: 'borderline',
}),
];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
categories: ['C9'],
grades: ['unsafe'],
compositionalOnly: true,
});
const tests = await plugin.generateTests(10);
expect(tests).toHaveLength(1);
expect(tests[0].metadata?.combinedCategory).toBe('C9');
expect(tests[0].metadata?.combinedGrade).toBe('unsafe');
expect(tests[0].metadata?.vlsuIsCompositional).toBe(true);
});
});
describe('case insensitivity', () => {
it('should handle uppercase category codes', async () => {
const mockRecords = [createMockCSVRecord({ combined_category: 'C9: Violence' })];
mockFetchWithCache.mockResolvedValue({
status: 200,
data: createMockCSVText(mockRecords),
cached: false,
statusText: 'OK',
} as any);
mockFetchImageAsBase64.mockResolvedValue('data:image/jpeg;base64,test');
const plugin = new VLSUPlugin(mockProvider, 'test purpose', 'image', {
categories: ['c9'], // lowercase in config
});
const tests = await plugin.generateTests(1);
expect(tests).toHaveLength(1);
});
});
});