Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-staging-live-uat-readiness.test.mjs
T

190 lines
8.9 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { afterEach, describe, it } from "vitest";
import { formatStagingLiveUatReadinessResult, verifyStagingLiveUatReadiness } from "./verify-staging-live-uat-readiness.mjs";
const fixtures = [];
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const VALID_STAGING_DOC = [
"# Staging, Live, and UAT Readiness",
"",
"## Scope",
"Manual runtime and provider-console evidence stays separate from automatic verification.",
"",
"## Readiness stages",
"Web, API, worker, Postgres database, Redis, S3 storage, Mailjet, Stripe, and ClamAV checks are staged.",
"",
"## Local static gates",
"Run typecheck, lint, verify:ci-baseline, verify:env-drift, verify:health-baseline, verify:openapi-security, verify:db-migrate-status, verify:deploy-workflow, verify:release-docs, verify:docs-links, and verify:staging-live-uat-readiness.",
"",
"## Staging environment contract",
"Record target labels for web, API, worker, database, Redis, storage, Mailjet, Stripe, and ClamAV.",
"",
"## Runtime smoke records",
"Use request ID, job ID, queue name, and redacted diagnostic labels for provider runtime smoke records.",
"",
"## Code-gap tracking before live confidence",
"Track API-key cutover, storage lifecycle, Imports final submit, Retention operations, legacy API migration, and compatibility boundary decisions.",
"",
"## Browser UAT matrix",
"Browser UAT covers MFA, onboarding, and review-before-apply journeys manually.",
"",
"## Final evidence packet",
"Collect local gates, runtime smoke status, browser UAT status, rollback class, and redaction review.",
"",
"## Redaction boundary",
"Never record raw secrets, customer data, provider payloads, or credential URLs.",
"",
"## Verification",
"Run `pnpm run verify:staging-live-uat-readiness`.",
].join("\n");
const SUPPORTING_DOCS = {
"docs/production-checklist.md": "# Production Checklist\n\nManual runtime checks and redaction rules.",
"docs/release-checklist.md": "# Release Checklist\n\nManual runtime evidence remains redacted.",
"docs/env-vars.md": "# Environment Variables\n\nUse key names only.",
"docs/deployment.md": "# Deployment\n\nDeployment target labels only.",
"docs/database.md": "# Database\n\nMigration and backup evidence uses labels.",
"docs/worker.md": "# Worker\n\nQueue and job evidence is redacted.",
"docs/mail.md": "# Mail\n\nMailjet status is recorded without payloads.",
"docs/security.md": "# Security\n\nRedaction boundary.",
"docs/observability.md": "# Observability\n\nRequest ID and job ID correlation.",
};
const VALID_PACKAGE_JSON = {
scripts: {
typecheck: "tsc -b",
lint: "eslint .",
"verify:ci-baseline": "node scripts/verify-ci-baseline.mjs",
"verify:env-drift": "node scripts/check-env-drift.mjs",
"verify:health-baseline": "node scripts/verify-health-baseline.mjs",
"verify:openapi-security": "tsx scripts/openapi-guard.ts",
"verify:db-migrate-status": "node scripts/verify-db-migrate-status.mjs",
"verify:deploy-workflow": "node scripts/verify-gitea-deploy-workflow.mjs",
"verify:release-docs": "node scripts/verify-release-docs.mjs",
"verify:docs-links": "tsx scripts/docs-link-check.ts",
"verify:staging-live-uat-readiness": "node scripts/verify-staging-live-uat-readiness.mjs",
},
};
const SUPPORTING_CODE = {
"packages/db/src/api-keys.server.ts": "export function findLegacyApiKeyByKey() { const allowPlaintextFallback = true; const keyHash = 'hash'; return { allowPlaintextFallback, keyHash }; }\n",
"packages/config/src/catalog.ts": "export const flag = 'API_KEY_LEGACY_PLAINTEXT_FALLBACK';\n",
"packages/storage/src/access/index.ts": "export function authorizeStorageUploadConstraints() { return ['unsupported_content_type', 'object_too_large']; }\n",
"apps/web/app/routes/app.$tenant/internal/imports/submit.tsx": "import { findImportJobForInspection } from '@toolctrl/db/import-jobs.server'; import { buildImportJob, enqueueJob } from '@toolctrl/queue'; export { findImportJobForInspection, buildImportJob, enqueueJob };\n",
"apps/api/src/exportRetentionQueue.ts": "export function queueExportArtifactRetention() { return ['expired-export-artifacts', queueNames.maintenance]; }\n",
"apps/api/src/routes/maintenance-routes.ts": "app.post('/v1/maintenance/export-retention/enqueue', handler); authorizeStorageUploadConstraints();\n",
"apps/api/src/openapi.ts": "const operationId = 'enqueueExportArtifactRetention'; const code = 'maintenance_queue_unavailable';\n",
"apps/worker/src/handlers.ts": "if (payload.task === 'expired-export-artifacts') listPending({ take: payload.take ?? payload.olderThanHours });\n",
};
function writeFixtureFile(rootDir, relativePath, content) {
const absolutePath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, content);
}
function createFixture(overrides = {}) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "staging-live-uat-"));
fixtures.push(rootDir);
writeFixtureFile(rootDir, "docs/staging-live-uat.md", overrides["docs/staging-live-uat.md"] ?? VALID_STAGING_DOC);
for (const [relativePath, content] of Object.entries(SUPPORTING_DOCS)) {
if (overrides[relativePath] === null) continue;
writeFixtureFile(rootDir, relativePath, overrides[relativePath] ?? content);
}
for (const [relativePath, content] of Object.entries(SUPPORTING_CODE)) {
if (overrides[relativePath] === null) continue;
writeFixtureFile(rootDir, relativePath, overrides[relativePath] ?? content);
}
writeFixtureFile(rootDir, "package.json", JSON.stringify(overrides["package.json"] ?? VALID_PACKAGE_JSON, null, 2));
return rootDir;
}
afterEach(() => {
while (fixtures.length > 0) {
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
}
});
describe("verify-staging-live-uat-readiness", () => {
it("passes for a complete readiness runbook and package script contract", () => {
const rootDir = createFixture();
const result = verifyStagingLiveUatReadiness(rootDir);
const output = formatStagingLiveUatReadinessResult(result);
assert.deepEqual(result.failures, []);
assert.match(output, /^PASS staging\/live\/UAT readiness verification/);
assert.match(output, /10 readiness documentation files/);
assert.match(output, /11 package scripts/);
assert.match(output, /8 code evidence files/);
});
it("fails when the staging runbook is missing a required section", () => {
const rootDir = createFixture({
"docs/staging-live-uat.md": VALID_STAGING_DOC.replace("## Browser UAT matrix", "## Browser checks"),
});
const result = verifyStagingLiveUatReadiness(rootDir);
assert.match(result.failures.join("\n"), /missing required section heading "## Browser UAT matrix"/);
});
it("fails when a required local readiness package script is missing", () => {
const packageJson = JSON.parse(JSON.stringify(VALID_PACKAGE_JSON));
delete packageJson.scripts["verify:db-migrate-status"];
const rootDir = createFixture({ "package.json": packageJson });
const result = verifyStagingLiveUatReadiness(rootDir);
assert.match(result.failures.join("\n"), /missing required readiness script "verify:db-migrate-status"/);
});
it("fails when the health baseline readiness script is missing", () => {
const packageJson = JSON.parse(JSON.stringify(VALID_PACKAGE_JSON));
delete packageJson.scripts["verify:health-baseline"];
const rootDir = createFixture({ "package.json": packageJson });
const result = verifyStagingLiveUatReadiness(rootDir);
assert.match(result.failures.join("\n"), /missing required readiness script "verify:health-baseline"/);
});
it("fails when staging-blocking code evidence is missing", () => {
const rootDir = createFixture({
"apps/web/app/routes/app.$tenant/internal/imports/submit.tsx": null,
});
const result = verifyStagingLiveUatReadiness(rootDir);
assert.match(result.failures.join("\n"), /app\/routes\/app\.\$tenant\/internal\/imports\/submit\.tsx: missing required code evidence/);
});
it("rejects automatic browser test commands in readiness docs", () => {
const rootDir = createFixture({
"docs/staging-live-uat.md": `${VALID_STAGING_DOC}\n\nRun pnpm exec playwright test for browser proof.\n`,
});
const result = verifyStagingLiveUatReadiness(rootDir);
assert.match(result.failures.join("\n"), /forbidden automatic browser\/E2E command pattern "playwright-test-command"/);
});
it("runs as a CLI and prints a passing summary for the repository", () => {
const output = execFileSync(process.execPath, ["scripts/verify-staging-live-uat-readiness.mjs"], {
cwd: repoRoot,
encoding: "utf8",
});
assert.match(output, /^PASS staging\/live\/UAT readiness verification/);
});
});