first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join, isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import type { TestCase } from '../harness/harness-types';
|
||||
|
||||
/** Path to the default prompts CSV fixture */
|
||||
const DEFAULT_PROMPTS_PATH = join(__dirname, '..', 'fixtures', 'default-prompts.csv');
|
||||
|
||||
type ParsedCsvRow = string[];
|
||||
|
||||
function isHeaderRow(row: ParsedCsvRow) {
|
||||
return row.some((cell) => cell.trim().toLowerCase() === 'prompt');
|
||||
}
|
||||
|
||||
function detectColumnIndex(header: ParsedCsvRow, name: string) {
|
||||
const normalized = name.toLowerCase();
|
||||
const index = header.findIndex((cell) => cell.trim().toLowerCase() === normalized);
|
||||
return index >= 0 ? index : undefined;
|
||||
}
|
||||
|
||||
function sanitizeValue(value: string | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function parseCsv(content: string): ParsedCsvRow[] {
|
||||
try {
|
||||
const rows = parse(content.replace(/^\ufeff/, ''), {
|
||||
columns: false,
|
||||
skip_empty_lines: true,
|
||||
trim: true,
|
||||
relax_column_count: true,
|
||||
}) as ParsedCsvRow[];
|
||||
|
||||
return rows.map((row) => row.map((cell) => cell ?? ''));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown parsing error';
|
||||
throw new Error(`Failed to parse CSV file: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTestCasesFromCsv(csvPath: string): TestCase[] {
|
||||
const resolvedPath = isAbsolute(csvPath) ? csvPath : resolve(process.cwd(), csvPath);
|
||||
|
||||
if (!existsSync(resolvedPath)) {
|
||||
throw new Error(`CSV file not found at ${resolvedPath}`);
|
||||
}
|
||||
|
||||
const rows = parseCsv(readFileSync(resolvedPath, 'utf8'));
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new Error('The provided CSV file is empty');
|
||||
}
|
||||
|
||||
const hasHeader = isHeaderRow(rows[0]);
|
||||
const header = hasHeader ? rows[0] : undefined;
|
||||
const dataRows = hasHeader ? rows.slice(1) : rows;
|
||||
|
||||
if (dataRows.length === 0) {
|
||||
throw new Error('No prompt rows found in the provided CSV file');
|
||||
}
|
||||
|
||||
// Find column index by name(s), returns undefined if no header
|
||||
const findColumn = (...names: string[]): number | undefined => {
|
||||
if (!header) return undefined;
|
||||
for (const name of names) {
|
||||
const idx = detectColumnIndex(header, name);
|
||||
if (idx !== undefined) return idx;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const promptIdx = findColumn('prompt') ?? 0;
|
||||
const idIdx = findColumn('id');
|
||||
const dosIdx = findColumn('dos', 'do');
|
||||
const dontsIdx = findColumn('donts', 'dont');
|
||||
const annotationsIdx = findColumn('annotations');
|
||||
|
||||
const getCell = (row: ParsedCsvRow, idx: number | undefined): string =>
|
||||
idx !== undefined ? sanitizeValue(row[idx]) : '';
|
||||
|
||||
const parseAnnotations = (raw: string, rowIndex: number): Record<string, unknown> | undefined => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
console.warn(
|
||||
`Warning: invalid JSON in annotations column for row ${rowIndex + (hasHeader ? 1 : 0)}, skipping`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const testCases: TestCase[] = [];
|
||||
|
||||
for (let i = 0; i < dataRows.length; i++) {
|
||||
const row = dataRows[i];
|
||||
const prompt = getCell(row, promptIdx);
|
||||
|
||||
if (!prompt) continue;
|
||||
|
||||
const dos = getCell(row, dosIdx);
|
||||
const donts = getCell(row, dontsIdx);
|
||||
|
||||
const testCase: TestCase = {
|
||||
id: getCell(row, idIdx) || `csv-case-${i + 1}`,
|
||||
prompt,
|
||||
};
|
||||
|
||||
const annotationsRaw = getCell(row, annotationsIdx);
|
||||
|
||||
if (dos || donts || annotationsRaw) {
|
||||
testCase.context = {};
|
||||
if (dos) testCase.context.dos = dos;
|
||||
if (donts) testCase.context.donts = donts;
|
||||
if (annotationsRaw) {
|
||||
const annotations = parseAnnotations(annotationsRaw, i + 1);
|
||||
if (annotations) {
|
||||
testCase.context.annotations = annotations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testCases.push(testCase);
|
||||
}
|
||||
|
||||
if (testCases.length === 0) {
|
||||
throw new Error('No valid prompts found in the provided CSV file');
|
||||
}
|
||||
|
||||
return testCases;
|
||||
}
|
||||
|
||||
/** Cached default test cases */
|
||||
let cachedDefaultTestCases: TestCase[] | null = null;
|
||||
|
||||
/**
|
||||
* Load the default test cases from the bundled CSV fixture.
|
||||
* Results are cached after first load.
|
||||
*/
|
||||
export function loadDefaultTestCases(): TestCase[] {
|
||||
cachedDefaultTestCases ??= loadTestCasesFromCsv(DEFAULT_PROMPTS_PATH);
|
||||
return cachedDefaultTestCases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available test case IDs from the default fixture.
|
||||
*/
|
||||
export function getDefaultTestCaseIds(): string[] {
|
||||
return loadDefaultTestCases()
|
||||
.map((tc) => tc.id)
|
||||
.filter((id): id is string => id !== undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user