mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* 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)
170 lines
6.4 KiB
JavaScript
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)`);
|
|
}
|