Files
pikasTech-HWLAB/scripts/m2-dev-deploy-smoke.mjs
T
2026-05-21 17:57:51 +00:00

200 lines
9.9 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEV_ENDPOINT,
ENVIRONMENT_DEV,
SERVICE_IDS
} from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixturePath = path.join(repoRoot, "fixtures/dev-deploy-smoke/dev-deploy-smoke.json");
const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const digestPattern = /^(sha256:[a-f0-9]{64}|not_applicable)$/;
const tagPattern = /^m2-dev-deploy-smoke-[a-f0-9]{7}$/;
const allowedFailureClassifications = new Set([
"network_blocker",
"runtime_blocker",
"agent_blocker",
"observability_blocker",
"safety_blocker"
]);
function parseArgs(argv) {
const flags = new Set();
const rest = [];
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg.startsWith("--")) {
flags.add(arg);
} else {
rest.push(arg);
}
}
return { flags, rest };
}
function loadFixture() {
return readFile(fixturePath, "utf8").then((raw) => JSON.parse(raw));
}
function assertTimestamp(value, context) {
assert.equal(typeof value, "string", `${context} must be a string`);
assert.match(value, timestampPattern, `${context} must be an RFC 3339 UTC timestamp`);
}
function assertServiceContract(entry) {
assert.ok(SERVICE_IDS.includes(entry.serviceId), `unknown serviceId ${entry.serviceId}`);
assert.equal(entry.deployEnv, ENVIRONMENT_DEV, `${entry.serviceId} deployEnv must be dev`);
assert.ok(!Object.hasOwn(entry, "endpoint"), `${entry.serviceId} service contract must not carry endpoint`);
assert.equal(typeof entry.commitId, "string", `${entry.serviceId} commitId must be a string`);
assert.match(entry.commitId, /^[a-f0-9]{7}$/u, `${entry.serviceId} commitId must be a short sha`);
assert.equal(typeof entry.image, "string", `${entry.serviceId} image must be a string`);
assert.ok(entry.image.length > 0, `${entry.serviceId} image must not be empty`);
assert.equal(typeof entry.tag, "string", `${entry.serviceId} tag must be a string`);
assert.match(entry.tag, tagPattern, `${entry.serviceId} tag must encode the smoke commit`);
assert.equal(typeof entry.digest, "string", `${entry.serviceId} digest must be a string`);
assert.match(entry.digest, digestPattern, `${entry.serviceId} digest must be sha256 or not_applicable`);
if (entry.digest === "not_applicable") {
assert.equal(typeof entry.digestReason, "string", `${entry.serviceId} digestReason required when digest is not_applicable`);
assert.ok(entry.digestReason.length > 0, `${entry.serviceId} digestReason must not be empty`);
} else {
assert.equal(entry.digestReason, undefined, `${entry.serviceId} digestReason must be absent when digest is present`);
}
assert.equal(typeof entry.buildSource, "string", `${entry.serviceId} buildSource must be a string`);
assert.ok(entry.buildSource.includes("origin/main"), `${entry.serviceId} buildSource must point at origin/main`);
assertTimestamp(entry.healthTimestamp, `${entry.serviceId} healthTimestamp`);
assert.equal(typeof entry.routePhase, "number", `${entry.serviceId} routePhase must be numeric`);
assert.ok(entry.routePhase > 0, `${entry.serviceId} routePhase must be positive`);
assert.ok(entry.phase.length > 0, `${entry.serviceId} phase must not be empty`);
assert.equal(typeof entry.health, "object", `${entry.serviceId} health must be an object`);
assert.ok(entry.health && !Array.isArray(entry.health), `${entry.serviceId} health must be a plain object`);
assert.ok(typeof entry.health.status === "string", `${entry.serviceId} health.status must be a string`);
assert.equal(typeof entry.failureClassification, "string", `${entry.serviceId} failureClassification must be a string`);
assert.ok(entry.failureClassification.endsWith("_blocker"), `${entry.serviceId} failureClassification must end in _blocker`);
assert.ok(
allowedFailureClassifications.has(entry.failureClassification),
`${entry.serviceId} failureClassification must be one of the frozen blocker classes`
);
}
function assertRoutePhaseOrder(entries) {
const phases = entries.map((entry) => entry.routePhase);
assert.deepEqual(phases, [...phases].sort((a, b) => a - b), "route phases must be sorted");
assert.equal(new Set(phases).size, phases.length, "route phases must be unique");
}
function assertAllServiceIds(entries) {
const serviceIds = entries.map((entry) => entry.serviceId);
assert.equal(new Set(serviceIds).size, serviceIds.length, "service ids must be unique in fixture");
for (const serviceId of serviceIds) {
assert.ok(SERVICE_IDS.includes(serviceId), `fixture uses unknown serviceId ${serviceId}`);
}
}
function assertCloudSurface(entry) {
const healthArtifacts = entry.health.artifacts ?? [];
assert.equal(entry.serviceId, "hwlab-cloud-api", "cloud surface entry must start at cloud-api");
assert.equal(entry.health.db?.fixtureOnly, true, "cloud surface DB note must be fixture-only");
assert.equal(entry.health.db?.liveDbEvidence, false, "cloud surface fixture must not claim live DB evidence");
assert.deepEqual(
entry.health.db?.missingEnv,
["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"],
"cloud surface fixture must name missing DB env without values"
);
assert.equal(healthArtifacts.length, 2, "cloud surface must include cloud-api and cloud-web artifacts");
assert.deepEqual(
healthArtifacts.map((artifact) => artifact.serviceId),
["hwlab-cloud-api", "hwlab-cloud-web"],
"cloud surface artifacts must list api/web"
);
for (const artifact of healthArtifacts) {
assert.ok(SERVICE_IDS.includes(artifact.serviceId), `cloud surface artifact ${artifact.serviceId} must be known`);
assert.equal(typeof artifact.image, "string", `cloud surface artifact ${artifact.serviceId} image must exist`);
assert.equal(typeof artifact.tag, "string", `cloud surface artifact ${artifact.serviceId} tag must exist`);
assert.match(artifact.tag, tagPattern, `cloud surface artifact ${artifact.serviceId} tag must encode the smoke commit`);
assert.match(artifact.digest, /^sha256:[a-f0-9]{64}$/u, `cloud surface artifact ${artifact.serviceId} digest must be sha256`);
assert.equal(typeof artifact.commitId, "string", `cloud surface artifact ${artifact.serviceId} commitId must exist`);
assert.equal(typeof artifact.buildSource, "string", `cloud surface artifact ${artifact.serviceId} buildSource must exist`);
assert.equal(artifact.deployEnv, ENVIRONMENT_DEV, `cloud surface artifact ${artifact.serviceId} deployEnv must be dev`);
assertTimestamp(artifact.healthTimestamp ?? entry.healthTimestamp, `cloud surface artifact ${artifact.serviceId} healthTimestamp`);
}
}
function assertFixture(fixture) {
assert.equal(fixture.smokeId, "m2-dev-deploy-smoke");
assert.equal(fixture.issue, "pikasTech/HWLAB#23");
assert.equal(fixture.environment, ENVIRONMENT_DEV);
assert.equal(fixture.endpoint, DEV_ENDPOINT);
assert.equal(fixture.defaultMode, "dry-run");
assert.equal(fixture.safetyGates.dryRunOnly, true, "dry-run must be the default");
assert.ok(Array.isArray(fixture.safetyGates.realRequestsRequire), "real request gates must exist");
assert.ok(fixture.safetyGates.realRequestsRequire.includes("explicit --live flag"));
const entries = fixture.serviceContracts;
assert.ok(Array.isArray(entries) && entries.length >= 8, "serviceContracts must contain route observations");
assertAllServiceIds(entries);
assertRoutePhaseOrder(entries);
for (const entry of entries) {
assertServiceContract(entry);
}
const cloudSurface = entries.find((entry) => entry.phase === "cloud-surface");
assert.ok(cloudSurface, "cloud-surface entry must exist");
assertCloudSurface(cloudSurface);
const agentSkills = entries.find((entry) => entry.serviceId === "hwlab-agent-skills");
assert.equal(agentSkills.digest, "not_applicable");
assert.equal(agentSkills.digestReason, "local dry-run fixture for repo-local skills bundle");
const observations = fixture.observations;
const expectedObservationIds = [];
for (const entry of entries) {
expectedObservationIds.push(entry.serviceId);
if (entry.serviceId === "hwlab-cloud-api") {
expectedObservationIds.push("hwlab-cloud-web");
}
}
assert.deepEqual(observations.serviceIds, expectedObservationIds, "observation service ids must track validated entries");
assert.deepEqual(
observations.routePhases,
["master-edge-proxy", "frp-tunnel", "d601-router", "cloud-surface", "agent-runtime", "sim-and-patch-panel"],
"route phase labels must stay frozen"
);
assertTimestamp(observations.healthTimestamp, "observations.healthTimestamp");
}
async function main() {
const { flags } = parseArgs(process.argv.slice(2));
const isLive = flags.has("--live") || flags.has("--allow-live");
if (isLive) {
assert.ok(!flags.has("--dry-run"), "do not combine --live with --dry-run");
assert.ok(flags.has("--confirm-dev"), "live mode requires --confirm-dev");
assert.ok(flags.has("--confirmed-non-production"), "live mode requires --confirmed-non-production");
assert.ok(!flags.has("--prod"), "PROD requests are forbidden");
assert.ok(!flags.has("--heavyweight-e2e"), "heavyweight e2e is forbidden");
assert.ok(!flags.has("--read-secret"), "secret reads are forbidden");
assert.ok(!flags.has("--force-push"), "force push is forbidden");
}
const fixture = await loadFixture();
assertFixture(fixture);
assert.equal(fixture.endpoint, DEV_ENDPOINT, "fixture must stay pinned to the frozen DEV endpoint");
process.stdout.write(`[m2-smoke] validated ${fixture.serviceContracts.length} service contracts\n`);
process.stdout.write(`[m2-smoke] mode=${isLive ? "live" : "dry-run"} endpoint=${fixture.endpoint}\n`);
process.stdout.write("[m2-smoke] no real DEV/PROD request was made\n");
}
try {
await main();
} catch (error) {
process.stderr.write(`[m2-smoke] ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}