Files
PaperClipAI/tests/runner-e2e/chat-catalog-inventory.ts
T

39 lines
2.7 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFileSync, readdirSync } from "node:fs";
import { parseDocument } from "yaml";
import type { ChatScenario } from "./chat-feature-cases.js";
export interface CatalogChatScenario extends ChatScenario { feature: string; sourceFile: string }
const directory = new URL("./chat-scenarios/", import.meta.url);
export const chatCatalogScenarios: readonly CatalogChatScenario[] = readdirSync(directory)
.filter(name => name.endsWith(".yaml") && name !== "defaults.yaml").sort().flatMap(name => {
const document = parseDocument(readFileSync(new URL(name, directory), "utf8"), { uniqueKeys: true });
if (document.errors.length) throw new Error(`Invalid chat catalog file ${name}`);
const catalog = document.toJS();
const feature = name.slice(0, -5);
if (catalog.schema_version !== 1 || catalog.feature !== feature || catalog.status !== "planned" || !Array.isArray(catalog.scenarios))
throw new Error(`Unsupported chat catalog file ${name}`);
return catalog.scenarios.map((scenario: ChatScenario) => {
if (!scenario.id.startsWith(`${feature}.`) || [scenario.context, scenario.query, scenario.expected_outcome].some(value => typeof value !== "string" || !value.trim())
|| !Array.isArray(scenario.capabilities) || scenario.capabilities.length === 0)
throw new Error(`Invalid scenario in ${name}`);
return { ...scenario, feature, sourceFile: `tests/runner-e2e/chat-scenarios/${name}` };
});
}).sort((a, b) => a.id.localeCompare(b.id));
if (chatCatalogScenarios.length !== 244 || new Set(chatCatalogScenarios.map(row => row.id)).size !== 244)
throw new Error("Frozen chat catalog must contain exactly 244 unique scenarios");
/** Canonical semantic digest; whitespace changes alone do not alter scenario meaning. */
export const chatCatalogSemanticDigest = createHash("sha256").update(JSON.stringify(chatCatalogScenarios.map(
({ id, capabilities, context, query, expected_outcome }) => ({ id, capabilities, context, query, expected_outcome }),
))).digest("hex");
/** Campaign freeze also binds original file bytes and shared context defaults. */
export const chatCatalogInventoryDigest = createHash("sha256").update(JSON.stringify({
defaultsSha256: createHash("sha256").update(readFileSync(new URL("defaults.yaml", directory))).digest("hex"),
scenarios: chatCatalogScenarios.map(({ id, capabilities, context, query, expected_outcome, sourceFile }) => ({
id,
scenarioSha256: createHash("sha256").update(JSON.stringify({ id, capabilities, context, query, expected_outcome })).digest("hex"),
sourceSha256: createHash("sha256").update(readFileSync(new URL(`../../${sourceFile}`, import.meta.url))).digest("hex"),
})),
})).digest("hex");