test: add dev m3 blocked evidence detail

This commit is contained in:
HWLAB Code Queue
2026-05-21 16:44:13 +00:00
parent 9f56de2062
commit 5e74278e74
4 changed files with 140 additions and 7 deletions
+10
View File
@@ -74,3 +74,13 @@ 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.
## Read-Only Continuation Evidence
When DEV ingress is blocked, continuation work may still record read-only
evidence. The smoke report includes a static comparison of
`deploy/k8s/base/workloads.yaml` against the M3 requirement for two
`hwlab-box-simu` instances and two `hwlab-gateway-simu` instances. This is not
live runtime proof. If that static comparison or the live runtime still lacks
the required simulator cardinality, the fix is a separate authorized DEV deploy
or runtime task, not a mutation inside the M3 evidence collection task.
+42 -6
View File
@@ -4,7 +4,7 @@
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#38",
"taskId": "dev-m3-hardware-loop",
"commitId": "1e8d009e9531",
"commitId": "a79037243324",
"acceptanceLevel": "dev_m3_hardware_loop",
"devOnly": true,
"prodDisabled": true,
@@ -66,10 +66,10 @@
"blockerClass": "network_blocker",
"summary": "DEV ingress at http://74.48.78.17:6667 did not return reachable HWLAB DEV health.",
"evidence": [
"{\"url\":\"http://74.48.78.17:6667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:38:29.319Z\",\"observedAt\":\"2026-05-21T16:38:33.884Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:38:33.885Z\",\"observedAt\":\"2026-05-21T16:38:36.674Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:38:36.674Z\",\"observedAt\":\"2026-05-21T16:38:39.674Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:38:39.674Z\",\"observedAt\":\"2026-05-21T16:38:39.835Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}"
"{\"url\":\"http://74.48.78.17:6667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:45:27.143Z\",\"observedAt\":\"2026-05-21T16:45:27.314Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:45:27.314Z\",\"observedAt\":\"2026-05-21T16:45:27.504Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:45:27.504Z\",\"observedAt\":\"2026-05-21T16:45:27.669Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}",
"{\"url\":\"http://74.48.78.17:6667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T16:45:27.669Z\",\"observedAt\":\"2026-05-21T16:45:27.847Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}"
]
},
{
@@ -121,6 +121,42 @@
]
}
],
"readOnlySupplementalEvidence": [
{
"id": "deploy-skeleton-m3-cardinality",
"status": "gap",
"blockerClass": "runtime_blocker",
"source": "deploy/k8s/base/workloads.yaml",
"summary": "Read-only static comparison found the checked-in DEV deploy skeleton declares one box simulator replica and one gateway simulator replica; proving two of each in live DEV requires a separate deploy/runtime task or authorized runtime observation.",
"evidence": {
"required": {
"hwlab-box-simu": 2,
"hwlab-gateway-simu": 2,
"hwlab-patch-panel": 1,
"wiring": "box-simu-1 DO1 -> box-simu-2 DI1"
},
"observed": {
"hwlab-box-simu": {
"replicas": 1,
"env": {
"HWLAB_BOX_ID": "boxsim_alpha"
}
},
"hwlab-gateway-simu": {
"replicas": 1,
"env": {
"HWLAB_BOX_RESOURCES": "res_boxsim_alpha"
}
},
"hwlab-patch-panel": {
"replicas": 1,
"env": {}
}
}
},
"requiredFollowUp": "Create a separate authorized DEV deploy/runtime task to provision or expose two box-simu instances, two gateway-simu instances, M3 wiring, and audit/evidence endpoints; do not perform that mutation in this read-only continuation."
}
],
"localRuntimeObservations": [
{
"id": "docker-hwlab-containers",
@@ -153,7 +189,7 @@
],
"summary": {
"status": "blocked",
"observedAt": "2026-05-21T16:38:29.315Z",
"observedAt": "2026-05-21T16:45:27.138Z",
"result": "No live DEV M3 evidence was produced; fixture-backed local evidence was not used as a substitute."
}
}
+68 -1
View File
@@ -2,7 +2,7 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { access, mkdir, writeFile } from "node:fs/promises";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import path from "node:path";
@@ -14,6 +14,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
const defaultReportPath = "reports/dev-gate/dev-m3-hardware-loop.json";
const namespace = "hwlab-dev";
const issue = "pikasTech/HWLAB#38";
const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
const serviceTargets = Object.freeze([
{
@@ -204,6 +205,70 @@ function collectLocalRuntimeObservations() {
return observations;
}
async function collectReadOnlySupplementalEvidence() {
const raw = await readFile(path.join(repoRoot, deployWorkloadsPath), "utf8");
const workloads = JSON.parse(raw);
const targets = ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"];
const observed = workloads.items
.filter((item) => targets.includes(item.metadata?.name))
.map((item) => {
const container = item.spec?.template?.spec?.containers?.[0] ?? {};
const env = Object.fromEntries((container.env ?? []).map((entry) => [entry.name, entry.value]));
return {
name: item.metadata.name,
kind: item.kind,
namespace: item.metadata.namespace,
replicas: item.spec?.replicas ?? null,
image: container.image,
env
};
});
const box = observed.find((item) => item.name === "hwlab-box-simu");
const gateway = observed.find((item) => item.name === "hwlab-gateway-simu");
const patchPanel = observed.find((item) => item.name === "hwlab-patch-panel");
const boxReadyForM3 = box?.replicas === 2 && String(box.env.HWLAB_BOX_ID ?? "").includes(",");
const gatewayReadyForM3 =
gateway?.replicas === 2 && String(gateway.env.HWLAB_BOX_RESOURCES ?? "").includes(",");
return [
{
id: "deploy-skeleton-m3-cardinality",
status: boxReadyForM3 && gatewayReadyForM3 ? "pass" : "gap",
blockerClass: boxReadyForM3 && gatewayReadyForM3 ? null : "runtime_blocker",
source: deployWorkloadsPath,
summary: boxReadyForM3 && gatewayReadyForM3
? "Checked-in DEV deploy skeleton declares two simulator identities for M3."
: "Read-only static comparison found the checked-in DEV deploy skeleton declares one box simulator replica and one gateway simulator replica; proving two of each in live DEV requires a separate deploy/runtime task or authorized runtime observation.",
evidence: {
required: {
"hwlab-box-simu": 2,
"hwlab-gateway-simu": 2,
"hwlab-patch-panel": 1,
wiring: "box-simu-1 DO1 -> box-simu-2 DI1"
},
observed: {
"hwlab-box-simu": {
replicas: box?.replicas ?? null,
env: box?.env ?? null
},
"hwlab-gateway-simu": {
replicas: gateway?.replicas ?? null,
env: gateway?.env ?? null
},
"hwlab-patch-panel": {
replicas: patchPanel?.replicas ?? null,
env: patchPanel?.env ?? null
}
}
},
requiredFollowUp: boxReadyForM3 && gatewayReadyForM3
? "None from static deploy cardinality."
: "Create a separate authorized DEV deploy/runtime task to provision or expose two box-simu instances, two gateway-simu instances, M3 wiring, and audit/evidence endpoints; do not perform that mutation in this read-only continuation."
}
];
}
async function probeJson(url, options = {}) {
const method = options.method ?? "GET";
const startedAt = isoNow();
@@ -533,6 +598,7 @@ function baseReport({ commitId, observedAt }) {
evidence: [DEV_ENDPOINT]
}
],
readOnlySupplementalEvidence: [],
localRuntimeObservations: [],
liveOperation: {
status: "not_run",
@@ -591,6 +657,7 @@ async function main() {
commitId: currentCommit(),
observedAt
});
report.readOnlySupplementalEvidence = await collectReadOnlySupplementalEvidence();
const ingress = await observeDevIngress();
report.localRuntimeObservations = collectLocalRuntimeObservations();
+20
View File
@@ -339,6 +339,26 @@ async function validateDevM3Report(report, label) {
}
}
if (Object.hasOwn(report, "readOnlySupplementalEvidence")) {
assertArray(report.readOnlySupplementalEvidence, `${label}.readOnlySupplementalEvidence`);
for (const [index, evidence] of report.readOnlySupplementalEvidence.entries()) {
const evidenceLabel = `${label}.readOnlySupplementalEvidence[${index}]`;
assertObject(evidence, evidenceLabel);
for (const field of ["id", "status", "source", "summary", "evidence", "requiredFollowUp"]) {
assert.ok(Object.hasOwn(evidence, field), `${evidenceLabel} missing ${field}`);
}
assertString(evidence.id, `${evidenceLabel}.id`);
assert.ok(["pass", "gap"].includes(evidence.status), `${evidenceLabel}.status must be pass or gap`);
assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`);
assertString(evidence.summary, `${evidenceLabel}.summary`);
assertObject(evidence.evidence, `${evidenceLabel}.evidence`);
assertString(evidence.requiredFollowUp, `${evidenceLabel}.requiredFollowUp`);
if (Object.hasOwn(evidence, "blockerClass") && evidence.blockerClass !== null) {
assert.ok(blockerTypes.has(evidence.blockerClass), `${evidenceLabel}.blockerClass must be known`);
}
}
}
assertObject(report.liveOperation, `${label}.liveOperation`);
assertStatus(report.liveOperation.status, `${label}.liveOperation.status`);
for (const field of ["operationId", "traceId", "auditId", "evidenceId", "summary"]) {