test: add dev m3 blocked evidence detail
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"]) {
|
||||
|
||||
Reference in New Issue
Block a user