/** * Fixture-driven migration tests. * * Each fixture directory under `fixtures/` contains: * - input.yml (GitHub Actions workflow) * - expected.gitlab-ci.yml (expected GitLab CI output) * - expected-report.json (expected diagnostic + provenance shape) * * The test asserts canonical-YAML equality on the output and shape * equality on the report. Adding a new fixture requires no manifest * edit — just drop the three files into a new directory. */ import { describe, test, expect } from "vitest"; import { readFileSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; import { parseYAML } from "@intentius/chant/yaml"; import { transform } from "./index"; const FIXTURE_ROOT = join(__dirname, "fixtures"); interface Fixture { name: string; dir: string } function* walkFixtures(root: string, prefix = ""): Generator { for (const entry of readdirSync(root)) { const p = join(root, entry); if (!statSync(p).isDirectory()) continue; const name = prefix ? `${prefix}/${entry}` : entry; const files = readdirSync(p); if (files.includes("input.yml") && files.includes("expected.gitlab-ci.yml")) { yield { name, dir: p }; } else { yield* walkFixtures(p, name); } } } function canonicalYaml(text: string): unknown { // Strip leading comments so the "Generated by chant migrate from ..." // banner doesn't have to match byte-for-byte when sourceFile differs. const stripped = text.replace(/^#[^\n]*\n+/g, ""); if (!stripped.trim()) return {}; return parseYAML(stripped); } interface ReportShape { totals: { error: number; warning: number; info: number }; ruleIds: string[]; needsReview: Array<{ line: number; ruleId: string }>; provenance: { literalRewrites: number; actionMappings: Array<{ action: string; tier: number | undefined }> }; } function reportShape(diagnostics: Array>, provenance: Array>): ReportShape { const totals = { error: 0, warning: 0, info: 0 }; for (const d of diagnostics) { const sev = d.severity as string; if (sev in totals) totals[sev as "error" | "warning" | "info"]++; } const ruleIds = Array.from(new Set(diagnostics.map((d) => d.ruleId as string))).sort(); const needsReview = diagnostics .filter((d) => d.severity === "warning" || d.severity === "error") .map((d) => ({ line: d.line as number, ruleId: d.ruleId as string })); const literalRewrites = provenance.filter((p) => p.category === "literal" || p.category === "rewrite").length; const actionMappings = provenance .filter((p) => p.category === "action-map") .map((p) => ({ action: p.actionRef as string, tier: p.mappingTier as number | undefined })); return { totals, ruleIds, needsReview, provenance: { literalRewrites, actionMappings } }; } describe("github → gitlab fixtures", () => { for (const f of walkFixtures(FIXTURE_ROOT)) { test(f.name, async () => { const input = readFileSync(join(f.dir, "input.yml"), "utf-8"); const expectedYaml = readFileSync(join(f.dir, "expected.gitlab-ci.yml"), "utf-8"); const expectedReport: ReportShape = JSON.parse( readFileSync(join(f.dir, "expected-report.json"), "utf-8"), ); const result = await transform(input, { sourceFile: "input.yml" }); // Canonical YAML compare (parse + re-stringify absorbs whitespace/key order) expect(canonicalYaml(result.output)).toEqual(canonicalYaml(expectedYaml)); // Shape compare const actualShape = reportShape( result.diagnostics as unknown as Array>, result.provenance as unknown as Array>, ); expect(actualShape).toEqual(expectedReport); }); } });