test: add m2 dev deploy smoke dry-run
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user