From 619fbc830ee97f2607ce1f3983416a91e8674344 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 15:29:21 +0000 Subject: [PATCH] test: add m2 dev deploy smoke dry-run --- docs/m2-dev-deploy-smoke.md | 71 +++++ .../dev-deploy-smoke/dev-deploy-smoke.json | 280 ++++++++++++++++++ scripts/m2-dev-deploy-smoke.mjs | 192 ++++++++++++ 3 files changed, 543 insertions(+) create mode 100644 docs/m2-dev-deploy-smoke.md create mode 100644 fixtures/dev-deploy-smoke/dev-deploy-smoke.json create mode 100644 scripts/m2-dev-deploy-smoke.mjs diff --git a/docs/m2-dev-deploy-smoke.md b/docs/m2-dev-deploy-smoke.md new file mode 100644 index 00000000..63ac80ce --- /dev/null +++ b/docs/m2-dev-deploy-smoke.md @@ -0,0 +1,71 @@ +# M2 DEV Deployment Smoke Dry-Run + +`m2-dev-deploy-smoke` is a local dry-run smoke harness for the HWLAB DEV +deployment shape. It validates the frozen route phases, service identities, +artifact metadata, and safety gates without contacting the real DEV ingress by +default. + +Run the dry-run from the repository root: + +```sh +node scripts/m2-dev-deploy-smoke.mjs --dry-run +``` + +## What It Checks + +- Parses `fixtures/dev-deploy-smoke/dev-deploy-smoke.json`. +- Verifies the route phases in order: + `master-edge-proxy`, `frp-tunnel`, `d601-router`, `cloud-surface`, + `agent-runtime`, and `sim-and-patch-panel`. +- Validates the frozen HWLAB service IDs for master edge proxy, frp tunnel, + D601 `hwlab-dev/hwlab-router`, cloud-api/web, agent, sim, and patch-panel. +- Checks `commitId`, `image`, `digest` or `not_applicable`, `tag`, + `buildSource`, `deployEnv`, `healthTimestamp`, and failure classification + for every recorded service contract. +- Confirms the cloud surface bundles `hwlab-cloud-api` and `hwlab-cloud-web` + artifacts together under the same route phase. +- Keeps the default mode in dry-run and refuses real requests unless an + explicit live gate is supplied. + +## Live Gate + +The live gate is intentionally strict. It is only for a future real DEV smoke +observation, not for deployment, and this harness stays fixture-only by +default. + +Required flags: + +```sh +node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production +``` + +Before using `--live`, confirm all of the following: + +- the target remains `http://74.48.78.17:6667`; +- the workspace is not a PROD or shared deployment context; +- no prohibited action is being attempted; +- the request is observational only and does not mutate runtime state. + +The script rejects live execution if any of these are missing: + +- `--confirm-dev` +- `--confirmed-non-production` +- `--prod` +- `--heavyweight-e2e` +- `--read-secret` +- `--force-push` + +## Boundary + +This harness does not: + +- deploy to DEV or PROD; +- call the real DEV endpoint in dry-run mode; +- run heavy e2e coverage; +- read secrets or tokens; +- replace HWLAB runtime services with UniDesk substitutes; +- touch L5 deploy manifests. + +The fixture is a dry-run record only. It exists to prove that the M2 smoke +entry point understands the frozen route and artifact contract before any live +DEV observation is attempted. diff --git a/fixtures/dev-deploy-smoke/dev-deploy-smoke.json b/fixtures/dev-deploy-smoke/dev-deploy-smoke.json new file mode 100644 index 00000000..4769fb31 --- /dev/null +++ b/fixtures/dev-deploy-smoke/dev-deploy-smoke.json @@ -0,0 +1,280 @@ +{ + "smokeId": "m2-dev-deploy-smoke", + "issue": "pikasTech/HWLAB#23", + "environment": "dev", + "endpoint": "http://74.48.78.17:6667", + "defaultMode": "dry-run", + "safetyGates": { + "dryRunOnly": true, + "realRequestsRequire": [ + "explicit --live flag", + "confirmed DEV endpoint", + "confirmed non-production workspace", + "no forbidden actions" + ], + "forbiddenActions": [ + "real-dev-deploy", + "prod-deploy", + "heavyweight-e2e", + "secret-read", + "force-push", + "unidesk-runtime-substitution" + ] + }, + "serviceContracts": [ + { + "routePhase": 1, + "phase": "master-edge-proxy", + "serviceId": "hwlab-edge-proxy", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-edge-proxy", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:40b2218b7a1ede3d215eaf99c56caf85cd61318da4e4348122e4e2da4c072cd8", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "ok", + "route": "public-dev-ingress", + "forwardTarget": "frp", + "artifactIdentity": "master edge proxy" + }, + "failureClassification": "network_blocker" + }, + { + "routePhase": 2, + "phase": "frp-tunnel", + "serviceId": "hwlab-tunnel-client", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-tunnel-client", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:53bdcb1f16d8a761d836e09e9bb0fe37d3f726b85769be8658d1e2702ff3d448", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "connected", + "route": "master-edge-proxy", + "forwardTarget": "D601 hwlab-dev/hwlab-router", + "artifactIdentity": "frp tunnel" + }, + "failureClassification": "network_blocker" + }, + { + "routePhase": 3, + "phase": "d601-router", + "serviceId": "hwlab-router", + "namespace": "hwlab-dev", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-router", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:be7e87979bbe78fc040d52d64b264bb8494f873c14b455dbf14681e2b4fd3dfb", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "live", + "route": "hwlab-dev/hwlab-router", + "forwardTarget": "cloud-api/web", + "artifactIdentity": "D601 router" + }, + "failureClassification": "runtime_blocker" + }, + { + "routePhase": 4, + "phase": "cloud-surface", + "serviceId": "hwlab-cloud-api", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-cloud-api", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:a147c7dbb6df1d30b0ac5e747f2f1fc42d455a55410b82578573b9b3ae2d6074", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "ok", + "live": true, + "route": "cloud-api/web", + "artifacts": [ + { + "serviceId": "hwlab-cloud-api", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-cloud-api", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:a147c7dbb6df1d30b0ac5e747f2f1fc42d455a55410b82578573b9b3ae2d6074" + , + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z" + }, + { + "serviceId": "hwlab-cloud-web", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-cloud-web", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:4a546582e6318b77530b9141dfd764c36b974442d6c7916a1294c54d0b02a14e", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z" + } + ] + }, + "failureClassification": "runtime_blocker" + }, + { + "routePhase": 5, + "phase": "agent-runtime", + "serviceId": "hwlab-agent-mgr", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-agent-mgr", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:cf2dab7d0aa49ef6e77dd8e1bacebd588bf2dbd65e86a4b8c2dc23cd248c026b", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "live", + "trace": "session-scoped", + "cleanup": "no worker leak" + }, + "failureClassification": "agent_blocker" + }, + { + "routePhase": 6, + "phase": "agent-runtime", + "serviceId": "hwlab-agent-worker", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-agent-worker", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:be2941f18376e8e39e0aa27857bb2db4300e861f2c759a7bc447e96e1f0e8bf1", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "scoped", + "projectId": "proj_mvp-l6", + "trace": "session-scoped", + "cleanup": "worker session closed" + }, + "failureClassification": "agent_blocker" + }, + { + "routePhase": 7, + "phase": "agent-runtime", + "serviceId": "hwlab-agent-skills", + "commitId": "5583e4b", + "image": "skills/hwlab-agent-runtime", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "not_applicable", + "digestReason": "local dry-run fixture for repo-local skills bundle", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "traceable", + "bundle": "repo-local skills artifact", + "cleanup": "explicit commitId required" + }, + "failureClassification": "agent_blocker" + }, + { + "routePhase": 8, + "phase": "sim-and-patch-panel", + "serviceId": "hwlab-gateway", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-gateway", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:dcf57c4a0fab873ab571c1f6314d352d38853ff2cf125356a05a06ae5cfdb444", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "live", + "route": "hardware boundary", + "artifactIdentity": "gateway boundary" + }, + "failureClassification": "runtime_blocker" + }, + { + "routePhase": 9, + "phase": "sim-and-patch-panel", + "serviceId": "hwlab-gateway-simu", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-gateway-simu", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:0bc140f52733255c937d08cea4d51639c83a71df8ac7a78bb107f60401bc392e", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "live", + "route": "simulator", + "artifactIdentity": "gateway simulator", + "observedAt": "2026-05-21T00:00:00.000Z" + }, + "failureClassification": "runtime_blocker" + }, + { + "routePhase": 10, + "phase": "sim-and-patch-panel", + "serviceId": "hwlab-box-simu", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-box-simu", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:0506757987fb7955ec0089fd9c0d895c33eb714a940fc7b8d58153368f96e8c8", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "live", + "route": "patch-panel-owned", + "artifactIdentity": "box simulator" + }, + "failureClassification": "runtime_blocker" + }, + { + "routePhase": 11, + "phase": "sim-and-patch-panel", + "serviceId": "hwlab-patch-panel", + "commitId": "5583e4b", + "image": "ghcr.io/pikastech/hwlab-patch-panel", + "tag": "m2-dev-deploy-smoke-5583e4b", + "digest": "sha256:bd3045c3235353e2437fbb11308ffe6f1ea3e52ca1e481dc80d4317a4b1600d1", + "buildSource": "git+https://github.com/pikasTech/HWLAB.git#origin/main:scripts/m2-dev-deploy-smoke.mjs", + "deployEnv": "dev", + "healthTimestamp": "2026-05-21T00:00:00.000Z", + "health": { + "status": "active", + "route": "patch-panel-owned", + "artifactIdentity": "patch panel", + "observedAt": "2026-05-21T00:00:00.000Z" + }, + "failureClassification": "runtime_blocker" + } + ], + "observations": { + "serviceIds": [ + "hwlab-edge-proxy", + "hwlab-tunnel-client", + "hwlab-router", + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-agent-skills", + "hwlab-gateway", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel" + ], + "routePhases": [ + "master-edge-proxy", + "frp-tunnel", + "d601-router", + "cloud-surface", + "agent-runtime", + "sim-and-patch-panel" + ], + "healthTimestamp": "2026-05-21T00:00:00.000Z" + } +} diff --git a/scripts/m2-dev-deploy-smoke.mjs b/scripts/m2-dev-deploy-smoke.mjs new file mode 100644 index 00000000..c4666cd6 --- /dev/null +++ b/scripts/m2-dev-deploy-smoke.mjs @@ -0,0 +1,192 @@ +#!/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(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; +}