test: harden M3 trusted loop smoke path
This commit is contained in:
@@ -33,6 +33,14 @@ const requiredM3Connection = Object.freeze({
|
||||
const requiredM3GatewayIds = Object.freeze(["gwsimu_1", "gwsimu_2"]);
|
||||
const runnerK3sKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
|
||||
|
||||
const m3FailureClassifications = Object.freeze({
|
||||
targetMissing: "target_missing",
|
||||
identityNotDistinct: "identity_not_distinct",
|
||||
patchPanelWiringMissing: "patch_panel_wiring_missing",
|
||||
operationFailed: "operation_failed",
|
||||
evidenceMissing: "evidence_missing"
|
||||
});
|
||||
|
||||
const serviceTargets = Object.freeze([
|
||||
{
|
||||
id: "box-simu-1",
|
||||
@@ -90,11 +98,31 @@ function parseArgs(argv) {
|
||||
return {
|
||||
flags,
|
||||
values,
|
||||
dryRun: flags.has("--dry-run") || flags.has("--plan"),
|
||||
live: flags.has("--live") || flags.has("--allow-live")
|
||||
dryRun: flags.has("--dry-run") || flags.has("--plan") || flags.has("--source-read-only"),
|
||||
live: flags.has("--live")
|
||||
};
|
||||
}
|
||||
|
||||
function classifyDirectTargetBlockerScope(scope) {
|
||||
if (scope === "m3-patch-panel-wiring") {
|
||||
return m3FailureClassifications.patchPanelWiringMissing;
|
||||
}
|
||||
if (scope === "m3-box-simu-identity" || scope === "m3-gateway-simu-identity") {
|
||||
return m3FailureClassifications.identityNotDistinct;
|
||||
}
|
||||
if (scope === "m3-service-discovery" || scope === "m3-direct-target-missing") {
|
||||
return m3FailureClassifications.targetMissing;
|
||||
}
|
||||
return m3FailureClassifications.operationFailed;
|
||||
}
|
||||
|
||||
function classifyLiveOperationError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return /\b(auditId|evidenceId)\b/u.test(message)
|
||||
? m3FailureClassifications.evidenceMissing
|
||||
: m3FailureClassifications.operationFailed;
|
||||
}
|
||||
|
||||
function requireSafetyGates({ flags, dryRun, live }) {
|
||||
const forbiddenFlags = [
|
||||
"--prod",
|
||||
@@ -112,28 +140,20 @@ function requireSafetyGates({ flags, dryRun, live }) {
|
||||
assert.ok(!flags.has(flag), `${flag} is forbidden for DEV M3 smoke`);
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
const confirmedLive =
|
||||
live &&
|
||||
flags.has("--confirm-dev") &&
|
||||
flags.has("--confirmed-non-production") &&
|
||||
!dryRun &&
|
||||
!flags.has("--allow-live");
|
||||
|
||||
if (dryRun) {
|
||||
if (!confirmedLive) {
|
||||
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("--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
|
||||
@@ -648,10 +668,10 @@ function createLiveOperationPlan() {
|
||||
"targetPort=DI1",
|
||||
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
|
||||
],
|
||||
failureClassifications: Object.values(m3FailureClassifications),
|
||||
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",
|
||||
"commands missing any of --live --confirm-dev --confirmed-non-production stay source/read-only",
|
||||
"a dry-run, source, local, fixture, or read-only blocker report leaves liveOperation.status as not_run or blocked, never pass"
|
||||
]
|
||||
};
|
||||
@@ -963,7 +983,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
|
||||
type: "observability_blocker",
|
||||
scope: "m3-service-discovery",
|
||||
status: "open",
|
||||
classification: "runner_kubeconfig_readonly_gap",
|
||||
classification: classifyDirectTargetBlockerScope("m3-service-discovery"),
|
||||
sourceIssue: "pikasTech/HWLAB#46",
|
||||
summary: `DEV ingress is reachable on frozen :16667, but direct M3 target discovery via ${kubernetesDiscovery.source} is ${kubernetesDiscovery.status}; environment URLs missing ${environmentTargets.missing.join(", ")}; discovery detail=${kubernetesDiscovery.summary}.`
|
||||
});
|
||||
@@ -972,7 +992,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
|
||||
type: "observability_blocker",
|
||||
scope: "m3-direct-target-missing",
|
||||
status: "open",
|
||||
classification: "direct_target_missing",
|
||||
classification: classifyDirectTargetBlockerScope("m3-direct-target-missing"),
|
||||
sourceIssue: "pikasTech/HWLAB#64",
|
||||
summary: `Read-only service discovery did not expose enough callable DEV M3 direct targets: box-simu=${boxCandidates.length}/2, gateway-simu=${gatewayCandidates.length}/2, patch-panel=${patchCandidates.length}/1.`
|
||||
});
|
||||
@@ -983,7 +1003,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-box-simu-identity",
|
||||
status: "open",
|
||||
classification: "direct_target_identity_gap",
|
||||
classification: classifyDirectTargetBlockerScope("m3-box-simu-identity"),
|
||||
sourceIssue: "pikasTech/HWLAB#64",
|
||||
summary: `DEV exposes two box-simu endpoints, but live identities are not the required distinct resources res_boxsimu_1 and res_boxsimu_2; observed resources=${[...distinctBoxResources].join(", ") || "none"}.`
|
||||
});
|
||||
@@ -994,7 +1014,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-gateway-simu-identity",
|
||||
status: "open",
|
||||
classification: "direct_target_identity_gap",
|
||||
classification: classifyDirectTargetBlockerScope("m3-gateway-simu-identity"),
|
||||
sourceIssue: "pikasTech/HWLAB#64",
|
||||
summary: `DEV exposes two gateway-simu endpoints, but live gateway identities are not distinct; observed identities=${[...distinctGatewayIdentities].join(", ") || "none"}.`
|
||||
});
|
||||
@@ -1007,7 +1027,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-patch-panel-wiring",
|
||||
status: "open",
|
||||
classification: "direct_target_m3_wiring_missing",
|
||||
classification: classifyDirectTargetBlockerScope("m3-patch-panel-wiring"),
|
||||
sourceIssue: "pikasTech/HWLAB#64",
|
||||
summary: `DEV patch-panel is callable, but live wiring does not contain ${requiredM3Connection.fromResourceId}:${requiredM3Connection.fromPort} -> ${requiredM3Connection.toResourceId}:${requiredM3Connection.toPort}; active=${observedActive}; configured=${observedConfigured}.`
|
||||
});
|
||||
@@ -1256,8 +1276,9 @@ async function runLiveM3Targets(targets) {
|
||||
assert.ok(after.ok, "box-simu-2 DI1 read failed");
|
||||
assert.equal(after.json?.ports?.DI1?.value, true, "box-simu-2 DI1 must read true");
|
||||
|
||||
const auditId = route.json?.auditId ?? write.json?.auditId ?? after.json?.auditId;
|
||||
const evidenceId = route.json?.evidenceId ?? write.json?.evidenceId ?? after.json?.evidenceId;
|
||||
const deliveryResponse = route.json?.deliveries?.[0]?.response;
|
||||
const auditId = route.json?.auditId ?? deliveryResponse?.auditId ?? write.json?.auditId ?? after.json?.auditId;
|
||||
const evidenceId = route.json?.evidenceId ?? deliveryResponse?.evidenceId ?? write.json?.evidenceId ?? after.json?.evidenceId;
|
||||
assert.equal(typeof auditId, "string", "live M3 result must include auditId");
|
||||
assert.equal(typeof evidenceId, "string", "live M3 result must include evidenceId");
|
||||
|
||||
@@ -1319,6 +1340,7 @@ function baseReport({ commitId, observedAt }) {
|
||||
confirmDevRequired: true,
|
||||
expectNonProdRequired: true,
|
||||
confirmedNonProductionRequired: true,
|
||||
defaultSourceReadOnly: true,
|
||||
dryRunPlanSupported: true,
|
||||
dryRunCallsLiveEndpoints: false,
|
||||
prodForbidden: true,
|
||||
@@ -1447,12 +1469,12 @@ async function runDryRunPlan({ values }) {
|
||||
type: "safety_blocker",
|
||||
scope: "m3-live-write-authorization",
|
||||
status: "open",
|
||||
classification: "dry_run_plan_only",
|
||||
classification: m3FailureClassifications.targetMissing,
|
||||
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",
|
||||
classification: m3FailureClassifications.targetMissing,
|
||||
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."
|
||||
};
|
||||
@@ -1537,12 +1559,12 @@ async function main() {
|
||||
type: "network_blocker",
|
||||
scope: "frp",
|
||||
status: "open",
|
||||
classification: "frp",
|
||||
classification: m3FailureClassifications.targetMissing,
|
||||
summary: "Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached."
|
||||
});
|
||||
report.summary = {
|
||||
status: "blocked",
|
||||
classification: "frp",
|
||||
classification: m3FailureClassifications.targetMissing,
|
||||
observedAt,
|
||||
result: "No live DEV M3 evidence was produced; fixture-backed local evidence was not used as a substitute."
|
||||
};
|
||||
@@ -1602,7 +1624,7 @@ async function main() {
|
||||
report.blockers.push(...directTargets.blockers);
|
||||
report.summary = {
|
||||
status: "blocked",
|
||||
classification: directTargets.blockers[0]?.classification ?? "direct_target_blocked",
|
||||
classification: directTargets.blockers[0]?.classification ?? m3FailureClassifications.targetMissing,
|
||||
observedAt,
|
||||
result: "DEV ingress and direct targets were reachable, but M3 DEV-LIVE was not triggered because required identities or patch-panel wiring were missing."
|
||||
};
|
||||
@@ -1672,17 +1694,18 @@ async function main() {
|
||||
`[dev-m3-smoke] operationId=${live.operationId} auditId=${live.auditId} evidenceId=${live.evidenceId}`
|
||||
);
|
||||
} catch (error) {
|
||||
const classification = classifyLiveOperationError(error);
|
||||
addNotRunM3Checks(report, "A live DEV M3 runtime contract check failed before all checks passed.");
|
||||
report.blockers.push({
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-hardware-loop-runtime",
|
||||
status: "open",
|
||||
classification: "hardware loop",
|
||||
classification,
|
||||
summary: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
report.summary = {
|
||||
status: "blocked",
|
||||
classification: "hardware loop",
|
||||
classification,
|
||||
observedAt,
|
||||
result: "Live DEV M3 runtime was reachable but did not satisfy the M3 hardware-loop contract."
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user