Files
OmniRoute/scripts/homolog/run.mjs
Diego Rodrigues de Sa e Souza c97d2a6ae2 feat(homolog): real-environment E2E homologation suite (npm run homolog) (#7133)
* feat(homolog): scaffolding da suíte de homologação E2E (deps + npm run homolog)

* feat(homolog): L0 avaliador de paridade de deploy (TDD)

* feat(homolog): L1a ciclo de vida de API key efêmera (login admin -> create -> revoke)

* feat(homolog): L1b suite httpYac de API (models, chat, auth de management, health)

* feat(homolog): L1c checker SSE de streaming real (TDD no parser)

* feat(homolog): L2 smoke de providers reais via promptfoo gerado do catálogo

* feat(homolog): L4a Playwright homolog config + login storageState

* feat(homolog): L4b smoke de todas as rotas do dashboard (descoberta via fs)

* feat(homolog): L4c fluxo criar/revogar API key pela UI

* fix(homolog): resiliencia real-environment — stream:false no smoke promptfoo, retry de socket keep-alive, key efemera com sufixo unico

* feat(homolog): L5 orquestrador npm run homolog + relatorio CTRF unificado

* docs(homolog): guia de operacao da suite + fragment de changelog + allowlist env-doc-sync

* fix(homolog): paraleliza o sweep de rotas do dashboard (fullyParallel + 8 workers)

* fix(homolog): isola outputs crus em homolog-report/raw para nao quebrar o ctrf merge

* fix(homolog): outputDir absoluto do reporter CTRF da UI (path relativo escapava do worktree)

* chore(quality): allowlist the 5 homolog-suite devDependencies (ctrf-io trio, httpyac, promptfoo) after registry verification

* chore(quality): register the homolog Playwright suite as a test-discovery collector (run.mjs -> tests/homolog/ui)
2026-07-14 16:24:11 -03:00

170 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
import { execSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import { evaluateParity } from "./lib/parity.mjs";
import { createEphemeralKey } from "./lib/adminClient.mjs";
import { checkSse } from "./lib/sseCheck.mjs";
import { promptfooToCtrf } from "./lib/promptfooToCtrf.mjs";
// ── env ──────────────────────────────────────────────────────────────────
if (fs.existsSync(".env.homolog")) {
for (const line of fs.readFileSync(".env.homolog", "utf8").split("\n")) {
const m = line.match(/^([A-Z_]+)=(.*)$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
}
const BASE = process.env.HOMOLOG_BASE_URL;
if (!BASE || !process.env.HOMOLOG_ADMIN_PASSWORD) {
console.error("Configure .env.homolog (HOMOLOG_BASE_URL, HOMOLOG_ADMIN_PASSWORD)");
process.exit(2);
}
fs.rmSync("homolog-report", { recursive: true, force: true });
// raw/ fica FORA do merge CTRF: `ctrf merge` tenta mesclar qualquer *.json com
// chave "results" e quebra no output cru do promptfoo.
fs.mkdirSync("homolog-report/raw", { recursive: true });
const layers = []; // {name, ok, detail}
const record = (name, ok, detail = "") => {
layers.push({ name, ok, detail });
console.log(`${ok ? "✅" : "❌"} ${name}${detail ? `${detail}` : ""}`);
};
// ── L0 saúde/paridade ────────────────────────────────────────────────────
const expectedVersion =
process.env.HOMOLOG_EXPECT_VERSION || JSON.parse(fs.readFileSync("package.json", "utf8")).version;
const healthRes = await fetch(`${BASE}/api/monitoring/health`);
const health = await healthRes.json().catch(() => ({}));
const parity = evaluateParity(health, { expectedVersion, httpStatus: healthRes.status });
record("L0 saúde/paridade", parity.ok, parity.failures.join("; "));
if (!parity.ok) {
console.error("Deploy divergente — abortando.");
writeSummary(layers, BASE, expectedVersion);
process.exit(1);
}
// ── chave efêmera ────────────────────────────────────────────────────────
const eph = await createEphemeralKey(BASE, process.env.HOMOLOG_ADMIN_PASSWORD);
process.env.HOMOLOG_API_KEY = eph.key;
try {
// modelo de smoke = 1º do tier crítico presente no catálogo
const models = (
await (
await fetch(`${BASE}/v1/models`, { headers: { Authorization: `Bearer ${eph.key}` } })
).json()
).data;
const critical = (process.env.HOMOLOG_CRITICAL_PROVIDERS || "openai").split(",");
const smokeModel =
models.find((m) => critical.some((p) => m.id.startsWith(`${p}/`)))?.id || models[0].id;
// ── L1 httpYac + SSE ───────────────────────────────────────────────────
const hy = spawnSync(
"npx",
[
"httpyac",
"send",
"tests/homolog/api/core.http",
"--all",
"--var",
`baseUrl=${BASE}`,
"--var",
`apiKey=${eph.key}`,
"--var",
`smokeModel=${smokeModel}`,
"--junit",
"--output",
"none",
],
{ encoding: "utf8" }
);
fs.writeFileSync("homolog-report/httpyac-junit.xml", hy.stdout || "");
record("L1 API (httpYac)", hy.status === 0);
const sse = await checkSse(BASE, eph.key, smokeModel);
record("L1 SSE streaming", sse.ok, (sse.failures || []).join("; "));
// ── L2 providers reais ─────────────────────────────────────────────────
try {
execSync("node scripts/homolog/gen-promptfoo.mjs", { stdio: "inherit", env: process.env });
spawnSync(
"npx",
[
"promptfoo",
"eval",
"-c",
"homolog-report/promptfooconfig.yaml",
"-o",
"homolog-report/raw/promptfoo.json",
"--no-cache",
],
{ encoding: "utf8", env: process.env }
);
const pfOut = JSON.parse(fs.readFileSync("homolog-report/raw/promptfoo.json", "utf8"));
const pfCtrf = promptfooToCtrf(pfOut);
fs.writeFileSync("homolog-report/providers-ctrf.json", JSON.stringify(pfCtrf, null, 2));
record(
"L2 providers reais",
pfCtrf.results.summary.failed === 0,
`${pfCtrf.results.summary.passed}/${pfCtrf.results.summary.tests} providers OK`
);
} catch (err) {
// gerador/eval quebrando é falha da camada — o run continua para o L4 e o cleanup
record("L2 providers reais", false, err.message);
}
// ── L4 UI ──────────────────────────────────────────────────────────────
const pw = spawnSync(
"npx",
["playwright", "test", "-c", "tests/homolog/ui/playwright.config.ts"],
{
stdio: "inherit",
env: process.env,
}
);
record("L4 UI (Playwright)", pw.status === 0);
} finally {
await eph
.revoke()
.then(() => record("cleanup: key efêmera revogada", true))
.catch((e) => record("cleanup: key efêmera revogada", false, e.message));
}
// ── L5 relatório unificado ───────────────────────────────────────────────
spawnSync(
"npx",
["junit-to-ctrf", "homolog-report/httpyac-junit.xml", "-o", "homolog-report/api-ctrf.json"],
{
stdio: "inherit",
}
);
spawnSync(
"npx",
[
"ctrf",
"merge",
"homolog-report",
"--output",
"homolog-ctrf.json",
"--output-dir",
"homolog-report",
],
{
stdio: "inherit",
}
);
writeSummary(layers, BASE, expectedVersion);
const failed = layers.filter((l) => !l.ok);
process.exit(failed.length ? 1 : 0);
function writeSummary(rows, base, version) {
const md = [
"# Homologação — relatório",
"",
`Alvo: ${base} · versão esperada: ${version}`,
"",
"| camada | resultado | detalhe |",
"|---|---|---|",
...rows.map((l) => `| ${l.name} | ${l.ok ? "✅" : "❌"} | ${l.detail} |`),
].join("\n");
fs.writeFileSync("homolog-report/summary.md", md);
console.log(`\n${md}\n\nRelatório: homolog-report/ (CTRF unificado: homolog-ctrf.json)`);
}