Files
latticelm/tests/bin/compliance-test.ts

271 lines
6.9 KiB
TypeScript

import {
testTemplates,
runAllTests,
type TestConfig,
type TestResult,
} from "../src/compliance-tests.ts";
const colors = {
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
};
interface CliArgs {
baseUrl?: string;
apiKey?: string;
model?: string;
authHeader?: string;
noBearer?: boolean;
noAuth?: boolean;
filter?: string[];
verbose?: boolean;
json?: boolean;
help?: boolean;
}
function parseArgs(argv: string[]): CliArgs {
const args: CliArgs = {};
let i = 0;
while (i < argv.length) {
const arg = argv[i];
const nextArg = argv[i + 1];
switch (arg) {
case "--base-url":
case "-u":
args.baseUrl = nextArg;
i += 2;
break;
case "--api-key":
case "-k":
args.apiKey = nextArg;
i += 2;
break;
case "--model":
case "-m":
args.model = nextArg;
i += 2;
break;
case "--auth-header":
args.authHeader = nextArg;
i += 2;
break;
case "--no-bearer":
args.noBearer = true;
i += 1;
break;
case "--no-auth":
args.noAuth = true;
i += 1;
break;
case "--filter":
case "-f":
args.filter = nextArg.split(",").map((s) => s.trim());
i += 2;
break;
case "--verbose":
case "-v":
args.verbose = true;
i += 1;
break;
case "--json":
args.json = true;
i += 1;
break;
case "--help":
case "-h":
args.help = true;
i += 1;
break;
default:
i += 1;
}
}
return args;
}
function printHelp() {
console.log(`
Usage: npm run test:compliance -- [options]
Options:
-u, --base-url <url> Gateway base URL (default: http://localhost:8080)
-k, --api-key <key> API key (or set OPENRESPONSES_API_KEY env var)
--no-auth Skip authentication header entirely
-m, --model <model> Model name (default: gpt-4o-mini)
--auth-header <name> Auth header name (default: Authorization)
--no-bearer Disable Bearer prefix in auth header
-f, --filter <ids> Filter tests by ID (comma-separated)
-v, --verbose Verbose output with request/response details
--json Output results as JSON
-h, --help Show this help message
Test IDs:
${testTemplates.map((t) => t.id).join(", ")}
Examples:
npm run test:compliance
npm run test:compliance -- --model claude-3-5-sonnet-20241022
npm run test:compliance -- --filter basic-response,streaming-response
npm run test:compliance -- --verbose --filter basic-response
npm run test:compliance -- --json > results.json
`);
}
function getStatusIcon(status: TestResult["status"]): string {
switch (status) {
case "passed":
return colors.green("✓");
case "failed":
return colors.red("✗");
case "running":
return colors.yellow("◉");
case "pending":
return colors.gray("○");
}
}
function printResult(result: TestResult, verbose: boolean) {
const icon = getStatusIcon(result.status);
const duration = result.duration ? ` (${result.duration}ms)` : "";
const events =
result.streamEvents !== undefined ? ` [${result.streamEvents} events]` : "";
const name =
result.status === "failed" ? colors.red(result.name) : result.name;
console.log(`${icon} ${name}${duration}${events}`);
if (result.status === "failed" && result.errors?.length) {
for (const error of result.errors) {
console.log(` ${colors.red("✗")} ${error}`);
}
if (verbose) {
if (result.request) {
console.log(`\n Request:`);
console.log(
` ${JSON.stringify(result.request, null, 2).split("\n").join("\n ")}`,
);
}
if (result.response) {
console.log(`\n Response:`);
const responseStr =
typeof result.response === "string"
? result.response
: JSON.stringify(result.response, null, 2);
console.log(` ${responseStr.split("\n").join("\n ")}`);
}
}
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printHelp();
process.exit(0);
}
const baseUrl = args.baseUrl || "http://localhost:8080";
const apiKey = args.apiKey || process.env.OPENRESPONSES_API_KEY || "";
if (!apiKey && !args.noAuth) {
// No auth is fine for local gateway without auth enabled
}
const config: TestConfig = {
baseUrl,
apiKey,
model: args.model || "gpt-4o-mini",
authHeaderName: args.authHeader || "Authorization",
useBearerPrefix: !args.noBearer,
};
if (args.filter?.length) {
const availableIds = testTemplates.map((t) => t.id);
const invalidFilters = args.filter.filter(
(id) => !availableIds.includes(id),
);
if (invalidFilters.length) {
console.error(
`${colors.red("Error:")} Invalid test IDs: ${invalidFilters.join(", ")}`,
);
console.error(`Available test IDs: ${availableIds.join(", ")}`);
process.exit(1);
}
}
const allUpdates: TestResult[] = [];
const onProgress = (result: TestResult) => {
if (args.filter && !args.filter.includes(result.id)) {
return;
}
allUpdates.push(result);
if (!args.json && result.status !== "running") {
printResult(result, args.verbose || false);
}
};
if (!args.json) {
console.log(`Running compliance tests against: ${baseUrl}`);
console.log(`Model: ${config.model}`);
if (args.filter) {
console.log(`Filter: ${args.filter.join(", ")}`);
}
console.log();
}
await runAllTests(config, onProgress);
const finalResults = allUpdates.filter(
(r) => r.status === "passed" || r.status === "failed",
);
const passed = finalResults.filter((r) => r.status === "passed").length;
const failed = finalResults.filter((r) => r.status === "failed").length;
if (args.json) {
console.log(
JSON.stringify(
{
summary: { passed, failed, total: finalResults.length },
results: finalResults,
},
null,
2,
),
);
} else {
console.log(`\n${"=".repeat(50)}`);
console.log(
`Results: ${colors.green(`${passed} passed`)}, ${colors.red(`${failed} failed`)}, ${finalResults.length} total`,
);
if (failed > 0) {
console.log(`\nFailed tests:`);
for (const r of finalResults) {
if (r.status === "failed") {
console.log(`\n${r.name}:`);
for (const e of r.errors || []) {
console.log(` - ${e}`);
}
}
}
} else {
console.log(`\n${colors.green("✓ All tests passed!")}`);
}
}
process.exit(failed > 0 ? 1 : 0);
}
main().catch((error) => {
console.error(colors.red("Fatal error:"), error);
process.exit(1);
});