test: add M3 source contract dry-run plan
Host commander merge: clean PR, no live write, preserves DRY-RUN vs DEV-LIVE boundary for #78/#7/#164.
This commit is contained in:
@@ -77,7 +77,7 @@ green evidence.
|
||||
| M0 | `SOURCE` | `docs/m0-contract-audit.md`, protocol schemas, evidence chain examples, M0 validator. |
|
||||
| M1 | `LOCAL` | `docs/m1-local-smoke.md`, `fixtures/mvp/runtime.json`, `scripts/m1-contract-smoke.mjs`. |
|
||||
| M2 | `DEV-LIVE` for frontend/route only | DEV deploy smoke fixture plus active read-only `16666`/`16667` endpoint smoke and accepted frontend revision. |
|
||||
| M3 | `BLOCKED` | Hardware trusted loop topology fixture and local patch-panel smoke exist, but DEV-LIVE operation/trace/audit/evidence IDs are missing. |
|
||||
| M3 | `BLOCKED` | Hardware trusted loop topology fixture, local patch-panel smoke, and the no-write DEV plan exist, but DEV-LIVE operation/trace/audit/evidence IDs for `res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` are missing. |
|
||||
| M4 | `BLOCKED` | Agent automation loop fixture and local smoke exist, but live preflight is blocked at DB live readiness. |
|
||||
| M5 | `BLOCKED` | MVP e2e dry-run plan is green, but bounded DEV-LIVE acceptance is blocked by DB/M3/M4 and source/artifact coverage. |
|
||||
|
||||
|
||||
+31
-11
@@ -72,16 +72,33 @@ remains `hwlab-patch-panel`, which calls the box internal port API.
|
||||
|
||||
## DEV Runtime Smoke
|
||||
|
||||
Use this command only for the DEV simulator runtime:
|
||||
Use the no-write plan first:
|
||||
|
||||
```sh
|
||||
node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production
|
||||
node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run
|
||||
```
|
||||
|
||||
The smoke writes `reports/dev-gate/dev-m3-hardware-loop.json`. It first checks
|
||||
the frozen DEV endpoint, `http://74.48.78.17:16667`, and stops at the first
|
||||
critical blocker from `docs/dev-acceptance-matrix.md`. When DEV ingress is not
|
||||
observable, the report is `blocked` and the script does not run the live
|
||||
The dry-run plan writes `reports/dev-gate/dev-m3-hardware-loop.json` by
|
||||
default, records `dryRunPlan.evidenceLevel: "DRY-RUN"`, lists the live write
|
||||
preconditions, enumerates the endpoints that a later live run would call, and
|
||||
keeps `liveOperation.status: "not_run"`. It does not call DEV endpoints, does
|
||||
not send `/ports/write` or `/signals/route`, and cannot be promoted to
|
||||
`DEV-LIVE`.
|
||||
|
||||
Use this command only for the approved DEV simulator runtime window:
|
||||
|
||||
```sh
|
||||
node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod
|
||||
```
|
||||
|
||||
The legacy `--confirmed-non-production` flag is accepted as compatibility, but
|
||||
new handoffs should use `--expect-non-prod`. The script refuses the default
|
||||
mode and refuses `--dry-run` combined with `--live`.
|
||||
|
||||
The live smoke writes `reports/dev-gate/dev-m3-hardware-loop.json`. It first
|
||||
checks the frozen DEV endpoint, `http://74.48.78.17:16667`, and stops at the
|
||||
first critical blocker from `docs/dev-acceptance-matrix.md`. When DEV ingress
|
||||
is not observable, the report is `blocked` and the script does not run the live
|
||||
`do.write true -> di.read true` operation.
|
||||
|
||||
If DEV ingress is reachable, the script requires explicit direct DEV simulator
|
||||
@@ -95,11 +112,14 @@ HWLAB_DEV_GATEWAY_SIMU_2_URL
|
||||
HWLAB_DEV_PATCH_PANEL_URL
|
||||
```
|
||||
|
||||
Those targets must be HWLAB DEV simulator services, not UniDesk substitutes.
|
||||
The script then checks two box simulators, two gateway simulators, active
|
||||
patch-panel wiring for `box-simu-1 DO1 -> box-simu-2 DI1`, a live direct call
|
||||
that reads `DI1=true`, and returned audit/evidence identifiers. Fixture-backed
|
||||
local M3 output is never accepted as live DEV evidence.
|
||||
Those targets must be two distinct HWLAB DEV simulator services for each
|
||||
simulator kind, plus one HWLAB DEV patch panel, not UniDesk substitutes. The
|
||||
script then checks two box simulators, two gateway simulators, active
|
||||
patch-panel wiring for
|
||||
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1`, a live direct
|
||||
call that reads `DI1=true`, and returned operation, trace, audit, and evidence
|
||||
identifiers. Fixture-backed local M3 output is never accepted as live DEV
|
||||
evidence.
|
||||
|
||||
## Read-Only Continuation Evidence
|
||||
|
||||
|
||||
@@ -58,6 +58,26 @@ Cloud Web polish, generic console work, artifact reports, and desired-state
|
||||
planning are support surfaces only. They can help explain the system, but they
|
||||
do not count as M3 PASS evidence.
|
||||
|
||||
After the DB and Code Agent provider unblock, the next acceptable M3 step is
|
||||
still not a blind live write. Run the no-write source contract first:
|
||||
|
||||
```sh
|
||||
node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run
|
||||
```
|
||||
|
||||
The dry-run output is `DRY-RUN` evidence. It must list the live write
|
||||
preconditions, the exact endpoints a later live run would call, and the
|
||||
required evidence fields while keeping `liveOperation.status: "not_run"`.
|
||||
Only an explicitly approved live window may use:
|
||||
|
||||
```sh
|
||||
node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod
|
||||
```
|
||||
|
||||
The live command remains DEV-only and must stop before mutation unless the two
|
||||
box-simu identities, two gateway-simu identities, and patch-panel-owned M3
|
||||
wiring are all observed.
|
||||
|
||||
## Manual Acceptance Steps
|
||||
|
||||
| Step | What to observe | Must have evidence | Failure class | Must not misread |
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
|
||||
"node --check scripts/validate-dev-m3-cardinality.mjs",
|
||||
"node scripts/validate-dev-m3-cardinality.mjs",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
@@ -40,13 +42,18 @@
|
||||
"environment": "dev",
|
||||
"requiredBoxSimulators": 2,
|
||||
"requiredGatewaySimulators": 2,
|
||||
"requiredPatchPanels": 1,
|
||||
"requiredRoute": "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
"realHardwareAllowed": false,
|
||||
"prodAllowed": false
|
||||
},
|
||||
"safetyGates": {
|
||||
"liveFlagRequired": true,
|
||||
"confirmDevRequired": true,
|
||||
"expectNonProdRequired": true,
|
||||
"confirmedNonProductionRequired": true,
|
||||
"dryRunPlanSupported": true,
|
||||
"dryRunCallsLiveEndpoints": false,
|
||||
"prodForbidden": true,
|
||||
"realHardwareForbidden": true,
|
||||
"secretReadForbidden": true,
|
||||
@@ -71,6 +78,16 @@
|
||||
"http://74.48.78.17:16667"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dry-run-live-write-plan",
|
||||
"status": "not_run",
|
||||
"summary": "Plan-only mode enumerated live write prerequisites and endpoints without calling DEV endpoints or mutating DEV state.",
|
||||
"evidence": [
|
||||
"endpointCount=17",
|
||||
"mutatingEndpointCount=2",
|
||||
"dryRunCallsLiveEndpoints=false"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dev-ingress-health",
|
||||
"status": "pass",
|
||||
@@ -245,6 +262,13 @@
|
||||
"classification": "direct_target_m3_wiring_missing",
|
||||
"sourceIssue": "pikasTech/HWLAB#64",
|
||||
"summary": "DEV patch-panel is callable, but live wiring does not contain res_boxsimu_1:DO1 -> res_boxsimu_2:DI1; active=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0; configured=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0."
|
||||
},
|
||||
{
|
||||
"type": "safety_blocker",
|
||||
"scope": "m3-live-write-authorization",
|
||||
"status": "open",
|
||||
"classification": "dry_run_plan_only",
|
||||
"summary": "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets."
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
@@ -471,5 +495,234 @@
|
||||
"syncableConnectionCount": 0
|
||||
},
|
||||
"summary": "Direct DEV M3 targets were probed read-only, but required identities or patch-panel M3 wiring were not satisfied."
|
||||
},
|
||||
"dryRunPlan": {
|
||||
"mode": "plan-only",
|
||||
"evidenceLevel": "DRY-RUN",
|
||||
"route": "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
"dryRunCallsLiveEndpoints": false,
|
||||
"liveWriteWillRun": false,
|
||||
"liveWriteRequiresHumanApproval": true,
|
||||
"liveCommand": "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod",
|
||||
"legacyLiveCommand": "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production",
|
||||
"liveWritePreconditions": [
|
||||
"operator has authorized a bounded DEV M3 live smoke window",
|
||||
"command includes --live, --confirm-dev, and --expect-non-prod",
|
||||
"PROD, real hardware, secret reads, service restarts, and force pushes remain forbidden",
|
||||
"DEV ingress identifies HWLAB DEV on the frozen 16666/16667 boundary",
|
||||
"two distinct hwlab-box-simu targets report res_boxsimu_1 and res_boxsimu_2",
|
||||
"two distinct hwlab-gateway-simu targets report distinct DEV gateway sessions",
|
||||
"one hwlab-patch-panel reports active wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1",
|
||||
"cross-device propagation is owned by hwlab-patch-panel; box loopback, SOURCE, LOCAL, fixture, and DRY-RUN output cannot be marked DEV-LIVE"
|
||||
],
|
||||
"endpointPlan": [
|
||||
{
|
||||
"phase": "dev-ingress-precondition",
|
||||
"targetId": "dev-api-edge",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "http://74.48.78.17:16667/health/live",
|
||||
"urlSource": "frozen-dev-endpoint",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "dev-ingress-precondition",
|
||||
"targetId": "dev-api-edge-json-rpc",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"method": "POST",
|
||||
"path": "/json-rpc",
|
||||
"url": "http://74.48.78.17:16667/json-rpc",
|
||||
"urlSource": "frozen-dev-endpoint",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false,
|
||||
"requestShape": "system.health"
|
||||
},
|
||||
{
|
||||
"phase": "dev-ingress-precondition",
|
||||
"targetId": "dev-frontend",
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "http://74.48.78.17:16666/health/live",
|
||||
"urlSource": "frozen-dev-frontend-endpoint",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "box-simu-1",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_1_URL/health/live",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_1_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "box-simu-1",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_1_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_1_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "box-simu-2",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_2_URL/health/live",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_2_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "box-simu-2",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_2_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_2_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "gateway-simu-1",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "$HWLAB_DEV_GATEWAY_SIMU_1_URL/health/live",
|
||||
"urlSource": "required-env:HWLAB_DEV_GATEWAY_SIMU_1_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "gateway-simu-1",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_GATEWAY_SIMU_1_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_GATEWAY_SIMU_1_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "gateway-simu-2",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "$HWLAB_DEV_GATEWAY_SIMU_2_URL/health/live",
|
||||
"urlSource": "required-env:HWLAB_DEV_GATEWAY_SIMU_2_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "gateway-simu-2",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_GATEWAY_SIMU_2_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_GATEWAY_SIMU_2_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"url": "$HWLAB_DEV_PATCH_PANEL_URL/health/live",
|
||||
"urlSource": "required-env:HWLAB_DEV_PATCH_PANEL_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "direct-target-precondition",
|
||||
"targetId": "patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_PATCH_PANEL_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_PATCH_PANEL_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "patch-panel-route-precondition",
|
||||
"targetId": "patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"method": "GET",
|
||||
"path": "/wiring",
|
||||
"url": "$HWLAB_DEV_PATCH_PANEL_URL/wiring",
|
||||
"urlSource": "required-env:HWLAB_DEV_PATCH_PANEL_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "live-write-source-do1",
|
||||
"targetId": "box-simu-1",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "POST",
|
||||
"path": "/ports/write",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_1_URL/ports/write",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_1_URL",
|
||||
"mutatesDevState": true,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "live-write-patch-panel-route",
|
||||
"targetId": "patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"method": "POST",
|
||||
"path": "/signals/route",
|
||||
"url": "$HWLAB_DEV_PATCH_PANEL_URL/signals/route",
|
||||
"urlSource": "required-env:HWLAB_DEV_PATCH_PANEL_URL",
|
||||
"mutatesDevState": true,
|
||||
"calledInDryRun": false
|
||||
},
|
||||
{
|
||||
"phase": "live-read-target-di1",
|
||||
"targetId": "box-simu-2",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"method": "GET",
|
||||
"path": "/status",
|
||||
"url": "$HWLAB_DEV_BOX_SIMU_2_URL/status",
|
||||
"urlSource": "required-env:HWLAB_DEV_BOX_SIMU_2_URL",
|
||||
"mutatesDevState": false,
|
||||
"calledInDryRun": false
|
||||
}
|
||||
],
|
||||
"evidenceFields": [
|
||||
"operationId",
|
||||
"traceId",
|
||||
"auditId",
|
||||
"evidenceId",
|
||||
"sourceResourceId=res_boxsimu_1",
|
||||
"sourcePort=DO1",
|
||||
"routeOwner=hwlab-patch-panel",
|
||||
"routeResponse.propagatedBy=hwlab-patch-panel",
|
||||
"targetResourceId=res_boxsimu_2",
|
||||
"targetPort=DI1",
|
||||
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
|
||||
],
|
||||
"refusalPolicy": [
|
||||
"no arguments defaults to refusal",
|
||||
"--dry-run/--plan cannot be combined with --live/--allow-live",
|
||||
"live mode refuses without --confirm-dev and --expect-non-prod",
|
||||
"a dry-run, source, local, fixture, or read-only blocker report leaves liveOperation.status as not_run or blocked, never pass"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ const issue = "pikasTech/HWLAB#38";
|
||||
const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
const deployServicesPath = "deploy/k8s/base/services.yaml";
|
||||
const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs";
|
||||
const dryRunPlanCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run";
|
||||
const liveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod";
|
||||
const legacyLiveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
|
||||
const requiredM3BoxIds = Object.freeze(["boxsimu_1", "boxsimu_2"]);
|
||||
const requiredM3BoxResources = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]);
|
||||
const requiredM3Connection = Object.freeze({
|
||||
@@ -84,10 +87,15 @@ function parseArgs(argv) {
|
||||
flags.add(arg);
|
||||
}
|
||||
|
||||
return { flags, values };
|
||||
return {
|
||||
flags,
|
||||
values,
|
||||
dryRun: flags.has("--dry-run") || flags.has("--plan"),
|
||||
live: flags.has("--live") || flags.has("--allow-live")
|
||||
};
|
||||
}
|
||||
|
||||
function requireSafetyGates(flags) {
|
||||
function requireSafetyGates({ flags, dryRun, live }) {
|
||||
const forbiddenFlags = [
|
||||
"--prod",
|
||||
"--real-hardware",
|
||||
@@ -104,13 +112,32 @@ function requireSafetyGates(flags) {
|
||||
assert.ok(!flags.has(flag), `${flag} is forbidden for DEV M3 smoke`);
|
||||
}
|
||||
|
||||
const live = flags.has("--live") || flags.has("--allow-live");
|
||||
assert.ok(live, "DEV M3 smoke requires --live or --allow-live");
|
||||
assert.ok(
|
||||
!(dryRun && live),
|
||||
"DEV M3 smoke refuses to combine --dry-run/--plan with --live/--allow-live; plan mode never runs the M3 write path"
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
mode: "dry-run",
|
||||
liveWriteAllowed: false
|
||||
};
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
live,
|
||||
"DEV M3 smoke refuses to run by default; use --dry-run for a no-write plan or --live --confirm-dev --expect-non-prod after approval"
|
||||
);
|
||||
assert.ok(flags.has("--confirm-dev"), "live DEV smoke requires --confirm-dev");
|
||||
assert.ok(
|
||||
flags.has("--confirmed-non-production"),
|
||||
"live DEV smoke requires --confirmed-non-production"
|
||||
flags.has("--expect-non-prod") || flags.has("--confirmed-non-production"),
|
||||
"live DEV smoke requires --expect-non-prod (legacy --confirmed-non-production is accepted only as compatibility)"
|
||||
);
|
||||
|
||||
return {
|
||||
mode: "live",
|
||||
liveWriteAllowed: true
|
||||
};
|
||||
}
|
||||
|
||||
function isoNow() {
|
||||
@@ -484,6 +511,152 @@ function joinUrl(baseUrl, routePath) {
|
||||
return new URL(routePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
||||
}
|
||||
|
||||
function endpointReference(target, routePath) {
|
||||
const baseUrl = process.env[target.urlEnv];
|
||||
if (baseUrl) {
|
||||
return {
|
||||
url: joinUrl(baseUrl, routePath),
|
||||
urlSource: `env:${target.urlEnv}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: `$${target.urlEnv}${routePath}`,
|
||||
urlSource: `required-env:${target.urlEnv}`
|
||||
};
|
||||
}
|
||||
|
||||
function plannedServiceEndpoint(target, routePath, { method = "GET", phase, mutatesDevState = false }) {
|
||||
const endpoint = endpointReference(target, routePath);
|
||||
return {
|
||||
phase,
|
||||
targetId: target.id,
|
||||
serviceId: target.serviceId,
|
||||
method,
|
||||
path: routePath,
|
||||
...endpoint,
|
||||
mutatesDevState,
|
||||
calledInDryRun: false
|
||||
};
|
||||
}
|
||||
|
||||
function createLiveOperationPlan() {
|
||||
const box1 = serviceTargets.find((target) => target.id === "box-simu-1");
|
||||
const box2 = serviceTargets.find((target) => target.id === "box-simu-2");
|
||||
const patchPanel = serviceTargets.find((target) => target.id === "patch-panel");
|
||||
const endpointPlan = [
|
||||
{
|
||||
phase: "dev-ingress-precondition",
|
||||
targetId: "dev-api-edge",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
method: "GET",
|
||||
path: "/health/live",
|
||||
url: joinUrl(DEV_ENDPOINT, "/health/live"),
|
||||
urlSource: "frozen-dev-endpoint",
|
||||
mutatesDevState: false,
|
||||
calledInDryRun: false
|
||||
},
|
||||
{
|
||||
phase: "dev-ingress-precondition",
|
||||
targetId: "dev-api-edge-json-rpc",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
method: "POST",
|
||||
path: "/json-rpc",
|
||||
url: joinUrl(DEV_ENDPOINT, "/json-rpc"),
|
||||
urlSource: "frozen-dev-endpoint",
|
||||
mutatesDevState: false,
|
||||
calledInDryRun: false,
|
||||
requestShape: "system.health"
|
||||
},
|
||||
{
|
||||
phase: "dev-ingress-precondition",
|
||||
targetId: "dev-frontend",
|
||||
serviceId: "hwlab-cloud-web",
|
||||
method: "GET",
|
||||
path: "/health/live",
|
||||
url: joinUrl(DEV_FRONTEND_ENDPOINT, "/health/live"),
|
||||
urlSource: "frozen-dev-frontend-endpoint",
|
||||
mutatesDevState: false,
|
||||
calledInDryRun: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const target of serviceTargets) {
|
||||
endpointPlan.push(
|
||||
plannedServiceEndpoint(target, "/health/live", {
|
||||
phase: "direct-target-precondition"
|
||||
}),
|
||||
plannedServiceEndpoint(target, target.statusPath, {
|
||||
phase: "direct-target-precondition"
|
||||
})
|
||||
);
|
||||
if (target.wiringPath) {
|
||||
endpointPlan.push(
|
||||
plannedServiceEndpoint(target, target.wiringPath, {
|
||||
phase: "patch-panel-route-precondition"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
endpointPlan.push(
|
||||
plannedServiceEndpoint(box1, "/ports/write", {
|
||||
method: "POST",
|
||||
phase: "live-write-source-do1",
|
||||
mutatesDevState: true
|
||||
}),
|
||||
plannedServiceEndpoint(patchPanel, patchPanel.routePath, {
|
||||
method: "POST",
|
||||
phase: "live-write-patch-panel-route",
|
||||
mutatesDevState: true
|
||||
}),
|
||||
plannedServiceEndpoint(box2, "/status", {
|
||||
phase: "live-read-target-di1"
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
mode: "plan-only",
|
||||
evidenceLevel: "DRY-RUN",
|
||||
route: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
dryRunCallsLiveEndpoints: false,
|
||||
liveWriteWillRun: false,
|
||||
liveWriteRequiresHumanApproval: true,
|
||||
liveCommand: liveSmokeCommand,
|
||||
legacyLiveCommand: legacyLiveSmokeCommand,
|
||||
liveWritePreconditions: [
|
||||
"operator has authorized a bounded DEV M3 live smoke window",
|
||||
"command includes --live, --confirm-dev, and --expect-non-prod",
|
||||
"PROD, real hardware, secret reads, service restarts, and force pushes remain forbidden",
|
||||
"DEV ingress identifies HWLAB DEV on the frozen 16666/16667 boundary",
|
||||
"two distinct hwlab-box-simu targets report res_boxsimu_1 and res_boxsimu_2",
|
||||
"two distinct hwlab-gateway-simu targets report distinct DEV gateway sessions",
|
||||
"one hwlab-patch-panel reports active wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1",
|
||||
"cross-device propagation is owned by hwlab-patch-panel; box loopback, SOURCE, LOCAL, fixture, and DRY-RUN output cannot be marked DEV-LIVE"
|
||||
],
|
||||
endpointPlan,
|
||||
evidenceFields: [
|
||||
"operationId",
|
||||
"traceId",
|
||||
"auditId",
|
||||
"evidenceId",
|
||||
"sourceResourceId=res_boxsimu_1",
|
||||
"sourcePort=DO1",
|
||||
"routeOwner=hwlab-patch-panel",
|
||||
"routeResponse.propagatedBy=hwlab-patch-panel",
|
||||
"targetResourceId=res_boxsimu_2",
|
||||
"targetPort=DI1",
|
||||
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
|
||||
],
|
||||
refusalPolicy: [
|
||||
"no arguments defaults to refusal",
|
||||
"--dry-run/--plan cannot be combined with --live/--allow-live",
|
||||
"live mode refuses without --confirm-dev and --expect-non-prod",
|
||||
"a dry-run, source, local, fixture, or read-only blocker report leaves liveOperation.status as not_run or blocked, never pass"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function isHwlabDevIdentity(probe) {
|
||||
const body = probe.json;
|
||||
if (!probe.ok || !body || typeof body !== "object" || Array.isArray(body)) {
|
||||
@@ -1075,6 +1248,7 @@ async function runLiveM3Targets(targets) {
|
||||
probes.push({ id: "patch-panel", kind: "signals.route", probe: route });
|
||||
assert.ok(route.ok, "patch-panel signal route failed");
|
||||
assert.equal(route.json?.accepted, true, "patch-panel signal route accepted");
|
||||
assert.equal(route.json?.propagatedBy, "hwlab-patch-panel", "patch-panel signal route owner");
|
||||
assert.equal(route.json?.deliveryCount, 1, "patch-panel signal route delivery count");
|
||||
|
||||
const after = await probeJson(joinUrl(box2Target.baseUrl, "/status"));
|
||||
@@ -1122,7 +1296,9 @@ function baseReport({ commitId, observedAt }) {
|
||||
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
|
||||
"node --check scripts/validate-dev-m3-cardinality.mjs",
|
||||
requiredManifestCardinalityCommand,
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production",
|
||||
dryRunPlanCommand,
|
||||
liveSmokeCommand,
|
||||
legacyLiveSmokeCommand,
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
@@ -1133,13 +1309,18 @@ function baseReport({ commitId, observedAt }) {
|
||||
environment: ENVIRONMENT_DEV,
|
||||
requiredBoxSimulators: 2,
|
||||
requiredGatewaySimulators: 2,
|
||||
requiredPatchPanels: 1,
|
||||
requiredRoute: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
realHardwareAllowed: false,
|
||||
prodAllowed: false
|
||||
},
|
||||
safetyGates: {
|
||||
liveFlagRequired: true,
|
||||
confirmDevRequired: true,
|
||||
expectNonProdRequired: true,
|
||||
confirmedNonProductionRequired: true,
|
||||
dryRunPlanSupported: true,
|
||||
dryRunCallsLiveEndpoints: false,
|
||||
prodForbidden: true,
|
||||
realHardwareForbidden: true,
|
||||
secretReadForbidden: true,
|
||||
@@ -1193,6 +1374,94 @@ function baseReport({ commitId, observedAt }) {
|
||||
};
|
||||
}
|
||||
|
||||
async function runDryRunPlan({ values }) {
|
||||
await ensureContractFiles();
|
||||
|
||||
const reportPath = resolveReportPath(values.get("output"));
|
||||
const observedAt = isoNow();
|
||||
const report = baseReport({
|
||||
commitId: currentCommit(),
|
||||
observedAt
|
||||
});
|
||||
const plan = createLiveOperationPlan();
|
||||
|
||||
report.dryRunPlan = plan;
|
||||
report.readOnlySupplementalEvidence = await collectReadOnlySupplementalEvidence();
|
||||
report.liveChecks.push(
|
||||
{
|
||||
id: "dry-run-live-write-plan",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode enumerated live write prerequisites and endpoints without calling DEV endpoints or mutating DEV state.",
|
||||
evidence: [
|
||||
`endpointCount=${plan.endpointPlan.length}`,
|
||||
`mutatingEndpointCount=${plan.endpointPlan.filter((endpoint) => endpoint.mutatesDevState).length}`,
|
||||
"dryRunCallsLiveEndpoints=false"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "two-box-simu-online",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode requires two distinct live DEV box-simu identities before any write; no DEV target was probed.",
|
||||
evidence: ["required=res_boxsimu_1,res_boxsimu_2"]
|
||||
},
|
||||
{
|
||||
id: "two-gateway-simu-online",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode requires two distinct live DEV gateway-simu identities before any write; no DEV target was probed.",
|
||||
evidence: ["required=gwsimu_1,gwsimu_2"]
|
||||
},
|
||||
{
|
||||
id: "patch-panel-healthy",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode requires a callable DEV hwlab-patch-panel before any write; no DEV target was probed.",
|
||||
evidence: ["required=hwlab-patch-panel"]
|
||||
},
|
||||
{
|
||||
id: "wiring-do1-di1-applied",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode requires patch-panel-owned res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 wiring before any write.",
|
||||
evidence: [`route=${plan.route}`]
|
||||
},
|
||||
{
|
||||
id: "direct-call-do-write-di-read",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode did not send /ports/write or /signals/route; this output is DRY-RUN only and cannot be labeled DEV-LIVE.",
|
||||
evidence: ["No live DEV write was sent."]
|
||||
},
|
||||
{
|
||||
id: "audit-evidence-traceable",
|
||||
status: "not_run",
|
||||
summary: "Plan-only mode listed the required operation, trace, audit, and evidence fields but did not create them.",
|
||||
evidence: plan.evidenceFields
|
||||
}
|
||||
);
|
||||
report.liveOperation = {
|
||||
status: "not_run",
|
||||
operationId: "not_observed",
|
||||
traceId: "not_observed",
|
||||
auditId: "not_observed",
|
||||
evidenceId: "not_observed",
|
||||
summary: "Dry-run plan only; no DEV-LIVE operation was attempted or claimed."
|
||||
};
|
||||
report.blockers.push({
|
||||
type: "safety_blocker",
|
||||
scope: "m3-live-write-authorization",
|
||||
status: "open",
|
||||
classification: "dry_run_plan_only",
|
||||
summary: "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets."
|
||||
});
|
||||
report.summary = {
|
||||
status: "blocked",
|
||||
classification: "dry_run_plan_only",
|
||||
observedAt,
|
||||
result: "DRY-RUN plan emitted live write prerequisites, endpoint plan, and required evidence fields; it did not call DEV endpoints or produce DEV-LIVE evidence."
|
||||
};
|
||||
|
||||
await writeReport(report, reportPath);
|
||||
console.log(`[dev-m3-smoke] mode=dry-run status=blocked report=${relativePath(reportPath)}`);
|
||||
console.log("[dev-m3-smoke] dry-run: no DEV endpoint calls and no live write were sent");
|
||||
}
|
||||
|
||||
function addNotRunM3Checks(report, reason) {
|
||||
for (const id of [
|
||||
"two-box-simu-online",
|
||||
@@ -1223,8 +1492,14 @@ async function writeReport(report, reportPath) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { flags, values } = parseArgs(process.argv.slice(2));
|
||||
requireSafetyGates(flags);
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const safety = requireSafetyGates(args);
|
||||
if (safety.mode === "dry-run") {
|
||||
await runDryRunPlan(args);
|
||||
return;
|
||||
}
|
||||
|
||||
const { values } = args;
|
||||
await ensureContractFiles();
|
||||
|
||||
const reportPath = resolveReportPath(values.get("output"));
|
||||
|
||||
@@ -522,7 +522,10 @@ function collectM3Evidence(reports) {
|
||||
`auditId=${m3.liveOperation?.auditId ?? "not_observed"}`,
|
||||
`evidenceId=${m3.liveOperation?.evidenceId ?? "not_observed"}`
|
||||
],
|
||||
commands: ["node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"],
|
||||
commands: [
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
|
||||
],
|
||||
summary: liveSummary
|
||||
})
|
||||
];
|
||||
|
||||
@@ -85,8 +85,11 @@ const requiredDevM3ValidationCommands = [
|
||||
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
|
||||
"node --check scripts/validate-dev-m3-cardinality.mjs",
|
||||
"node scripts/validate-dev-m3-cardinality.mjs",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
|
||||
];
|
||||
const legacyDevM3LiveCommand =
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
|
||||
const requiredDevM3Docs = [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m3-hardware-loop.md"
|
||||
@@ -1334,6 +1337,10 @@ async function validateDevM3Report(report, label) {
|
||||
`${label}.validationCommands missing ${requiredCommand}`
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
report.validationCommands.includes(legacyDevM3LiveCommand),
|
||||
`${label}.validationCommands missing legacy compatibility command ${legacyDevM3LiveCommand}`
|
||||
);
|
||||
|
||||
assertObject(report.runtimeTarget, `${label}.runtimeTarget`);
|
||||
assert.equal(report.runtimeTarget.endpoint, "http://74.48.78.17:16667", `${label}.runtimeTarget.endpoint`);
|
||||
@@ -1348,6 +1355,16 @@ async function validateDevM3Report(report, label) {
|
||||
assert.equal(report.runtimeTarget.environment, "dev", `${label}.runtimeTarget.environment`);
|
||||
assert.equal(report.runtimeTarget.requiredBoxSimulators, 2, `${label}.runtimeTarget.requiredBoxSimulators`);
|
||||
assert.equal(report.runtimeTarget.requiredGatewaySimulators, 2, `${label}.runtimeTarget.requiredGatewaySimulators`);
|
||||
if (Object.hasOwn(report.runtimeTarget, "requiredPatchPanels")) {
|
||||
assert.equal(report.runtimeTarget.requiredPatchPanels, 1, `${label}.runtimeTarget.requiredPatchPanels`);
|
||||
}
|
||||
if (Object.hasOwn(report.runtimeTarget, "requiredRoute")) {
|
||||
assert.equal(
|
||||
report.runtimeTarget.requiredRoute,
|
||||
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
`${label}.runtimeTarget.requiredRoute`
|
||||
);
|
||||
}
|
||||
assert.equal(report.runtimeTarget.realHardwareAllowed, false, `${label}.runtimeTarget.realHardwareAllowed`);
|
||||
assert.equal(report.runtimeTarget.prodAllowed, false, `${label}.runtimeTarget.prodAllowed`);
|
||||
|
||||
@@ -1355,7 +1372,9 @@ async function validateDevM3Report(report, label) {
|
||||
for (const field of [
|
||||
"liveFlagRequired",
|
||||
"confirmDevRequired",
|
||||
"expectNonProdRequired",
|
||||
"confirmedNonProductionRequired",
|
||||
"dryRunPlanSupported",
|
||||
"prodForbidden",
|
||||
"realHardwareForbidden",
|
||||
"secretReadForbidden",
|
||||
@@ -1364,6 +1383,9 @@ async function validateDevM3Report(report, label) {
|
||||
]) {
|
||||
assert.equal(report.safetyGates[field], true, `${label}.safetyGates.${field}`);
|
||||
}
|
||||
if (Object.hasOwn(report.safetyGates, "dryRunCallsLiveEndpoints")) {
|
||||
assert.equal(report.safetyGates.dryRunCallsLiveEndpoints, false, `${label}.safetyGates.dryRunCallsLiveEndpoints`);
|
||||
}
|
||||
|
||||
assertArray(report.liveChecks, `${label}.liveChecks`);
|
||||
assert.ok(report.liveChecks.length >= 8, `${label}.liveChecks must include DEV M3 checks`);
|
||||
@@ -1449,7 +1471,14 @@ async function validateDevM3Report(report, label) {
|
||||
["pass", "gap", "source-ready", "manifest-ready"].includes(evidence.status),
|
||||
`${evidenceLabel}.status must be pass, gap, source-ready, or manifest-ready`
|
||||
);
|
||||
assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`);
|
||||
if (Array.isArray(evidence.source)) {
|
||||
assertStringArray(evidence.source, `${evidenceLabel}.source`, { minLength: 1 });
|
||||
for (const [sourceIndex, sourcePath] of evidence.source.entries()) {
|
||||
assertRepoRelativePath(sourcePath, `${evidenceLabel}.source[${sourceIndex}]`);
|
||||
}
|
||||
} else {
|
||||
assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`);
|
||||
}
|
||||
assertString(evidence.summary, `${evidenceLabel}.summary`);
|
||||
assertObject(evidence.evidence, `${evidenceLabel}.evidence`);
|
||||
assertString(evidence.requiredFollowUp, `${evidenceLabel}.requiredFollowUp`);
|
||||
@@ -1462,6 +1491,42 @@ async function validateDevM3Report(report, label) {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(report, "dryRunPlan")) {
|
||||
const plan = report.dryRunPlan;
|
||||
assertObject(plan, `${label}.dryRunPlan`);
|
||||
assert.equal(plan.mode, "plan-only", `${label}.dryRunPlan.mode`);
|
||||
assert.equal(plan.evidenceLevel, "DRY-RUN", `${label}.dryRunPlan.evidenceLevel`);
|
||||
assert.equal(plan.route, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1", `${label}.dryRunPlan.route`);
|
||||
assert.equal(plan.dryRunCallsLiveEndpoints, false, `${label}.dryRunPlan.dryRunCallsLiveEndpoints`);
|
||||
assert.equal(plan.liveWriteWillRun, false, `${label}.dryRunPlan.liveWriteWillRun`);
|
||||
assertStringArray(plan.liveWritePreconditions, `${label}.dryRunPlan.liveWritePreconditions`, { minLength: 4 });
|
||||
assertStringArray(plan.evidenceFields, `${label}.dryRunPlan.evidenceFields`, { minLength: 4 });
|
||||
assertArray(plan.endpointPlan, `${label}.dryRunPlan.endpointPlan`);
|
||||
assert.ok(plan.endpointPlan.length >= 8, `${label}.dryRunPlan.endpointPlan must enumerate precondition and live endpoints`);
|
||||
assert.ok(
|
||||
plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-patch-panel" && endpoint.path === "/signals/route" && endpoint.mutatesDevState === true),
|
||||
`${label}.dryRunPlan.endpointPlan must include the patch-panel route write endpoint`
|
||||
);
|
||||
assert.ok(
|
||||
plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-box-simu" && endpoint.path === "/ports/write" && endpoint.mutatesDevState === true),
|
||||
`${label}.dryRunPlan.endpointPlan must include the source box DO1 write endpoint`
|
||||
);
|
||||
assert.ok(
|
||||
plan.endpointPlan.every((endpoint) => endpoint.calledInDryRun === false),
|
||||
`${label}.dryRunPlan.endpointPlan endpoints must not be called in dry-run`
|
||||
);
|
||||
for (const requiredField of [
|
||||
"operationId",
|
||||
"traceId",
|
||||
"auditId",
|
||||
"evidenceId",
|
||||
"routeResponse.propagatedBy=hwlab-patch-panel",
|
||||
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
|
||||
]) {
|
||||
assert.ok(plan.evidenceFields.includes(requiredField), `${label}.dryRunPlan.evidenceFields missing ${requiredField}`);
|
||||
}
|
||||
}
|
||||
|
||||
assertObject(report.liveOperation, `${label}.liveOperation`);
|
||||
assertStatus(report.liveOperation.status, `${label}.liveOperation.status`);
|
||||
for (const field of ["operationId", "traceId", "auditId", "evidenceId", "summary"]) {
|
||||
|
||||
@@ -41,6 +41,9 @@ async function main() {
|
||||
assertIncludes(doc, "deploy/artifact-catalog.dev.json", "m3 runbook");
|
||||
assertIncludes(doc, "pikasTech/HWLAB#63", "m3 runbook");
|
||||
assertIncludes(doc, "Do not promote SOURCE / LOCAL / DRY-RUN / fixture output to `DEV-LIVE`.", "m3 runbook");
|
||||
assertIncludes(doc, "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run", "m3 runbook");
|
||||
assertIncludes(doc, "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod", "m3 runbook");
|
||||
assertIncludes(doc, "liveOperation.status: \"not_run\"", "m3 runbook");
|
||||
assertNotIncludes(boundary, "http://74.48.78.17:6666/", "m3 current boundary");
|
||||
assertNotIncludes(boundary, "http://74.48.78.17:6667/", "m3 current boundary");
|
||||
assertIncludes(boundary, "http://74.48.78.17:16666/", "m3 current boundary");
|
||||
|
||||
Reference in New Issue
Block a user