327 lines
11 KiB
JavaScript
327 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
candidateTargetsFromKubernetes,
|
|
classifyDirectTargetBlockerScope,
|
|
createLiveOperationPlan,
|
|
hasRequiredM3Connection,
|
|
kubernetesServiceIdentity,
|
|
parseArgs,
|
|
requireSafetyGates
|
|
} from "./dev-m3-hardware-loop-smoke.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const testOutputDir = "tmp/dev-m3-hardware-loop-smoke-test";
|
|
|
|
function runSmoke(args) {
|
|
return execFileSync("node", ["scripts/dev-m3-hardware-loop-smoke.mjs", ...args], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"]
|
|
});
|
|
}
|
|
|
|
test("default M3 smoke is source/read-only and never claims DEV-LIVE", async () => {
|
|
await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
|
|
const output = path.join(testOutputDir, "default-source-readonly.json");
|
|
try {
|
|
const stdout = runSmoke(["--output", output]);
|
|
assert.match(stdout, /mode=dry-run/u);
|
|
const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));
|
|
|
|
assert.equal(report.dryRunPlan.mode, "plan-only");
|
|
assert.equal(report.dryRunPlan.dryRunCallsLiveEndpoints, false);
|
|
assert.equal(report.dryRunPlan.liveWriteWillRun, false);
|
|
assert.equal(report.liveOperation.status, "not_run");
|
|
assert.equal(report.liveOperation.operationId, "not_observed");
|
|
assert.equal(report.summary.status, "blocked");
|
|
assert.match(report.summary.result, /did not call DEV endpoints/u);
|
|
assert.equal(
|
|
report.liveChecks.find((check) => check.id === "direct-call-do-write-di-read")?.status,
|
|
"not_run"
|
|
);
|
|
} finally {
|
|
await rm(path.join(repoRoot, output), { force: true });
|
|
}
|
|
});
|
|
|
|
test("--dry-run aliases the same source/read-only no-write path", async () => {
|
|
await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
|
|
const output = path.join(testOutputDir, "dry-run-source-readonly.json");
|
|
try {
|
|
runSmoke(["--dry-run", "--output", output]);
|
|
const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));
|
|
|
|
assert.equal(report.dryRunPlan.mode, "plan-only");
|
|
assert.equal(report.dryRunPlan.liveWriteWillRun, false);
|
|
assert.ok(
|
|
report.dryRunPlan.failureClassifications.includes("patch_panel_wiring_missing")
|
|
);
|
|
assert.equal(report.liveOperation.status, "not_run");
|
|
} finally {
|
|
await rm(path.join(repoRoot, output), { force: true });
|
|
}
|
|
});
|
|
|
|
test("M3 report validator rejects DRY-RUN evidence promoted to live pass", async () => {
|
|
await mkdir(path.join(repoRoot, testOutputDir), { recursive: true });
|
|
const output = path.join(testOutputDir, "dry-run-promoted-live.json");
|
|
const promoted = path.join(testOutputDir, "dry-run-promoted-live-tampered.json");
|
|
try {
|
|
runSmoke(["--dry-run", "--output", output]);
|
|
const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8"));
|
|
report.liveOperation = {
|
|
status: "pass",
|
|
operationId: "op_fake_dry_run",
|
|
traceId: "trc_fake_dry_run",
|
|
auditId: "aud_fake_dry_run",
|
|
evidenceId: "evd_fake_dry_run",
|
|
summary: "Incorrectly promoted DRY-RUN output to DEV-LIVE."
|
|
};
|
|
await writeFile(path.join(repoRoot, promoted), JSON.stringify(report, null, 2));
|
|
|
|
assert.throws(
|
|
() =>
|
|
execFileSync("node", ["scripts/validate-dev-gate-report.mjs", promoted], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"]
|
|
}),
|
|
(error) => {
|
|
assert.match(
|
|
`${error.message}\n${error.stderr ?? ""}`,
|
|
/liveOperation\.status cannot be pass when only a DRY-RUN plan is present/u
|
|
);
|
|
return true;
|
|
}
|
|
);
|
|
} finally {
|
|
await rm(path.join(repoRoot, output), { force: true });
|
|
await rm(path.join(repoRoot, promoted), { force: true });
|
|
}
|
|
});
|
|
|
|
test("M3 safety gates keep incomplete live commands source/read-only", () => {
|
|
assert.deepEqual(requireSafetyGates(parseArgs([])), {
|
|
mode: "dry-run",
|
|
liveWriteAllowed: false
|
|
});
|
|
assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev"])), {
|
|
mode: "dry-run",
|
|
liveWriteAllowed: false
|
|
});
|
|
assert.deepEqual(requireSafetyGates(parseArgs(["--dry-run", "--live", "--confirm-dev", "--expect-non-prod"])), {
|
|
mode: "dry-run",
|
|
liveWriteAllowed: false
|
|
});
|
|
assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--expect-non-prod"])), {
|
|
mode: "live",
|
|
liveWriteAllowed: true
|
|
});
|
|
assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"])), {
|
|
mode: "live",
|
|
liveWriteAllowed: true
|
|
});
|
|
});
|
|
|
|
test("M3 blocker scopes classify missing identities and patch-panel route gaps", () => {
|
|
assert.equal(classifyDirectTargetBlockerScope("m3-service-discovery"), "target_missing");
|
|
assert.equal(classifyDirectTargetBlockerScope("m3-direct-target-missing"), "target_missing");
|
|
assert.equal(classifyDirectTargetBlockerScope("m3-box-simu-identity"), "identity_not_distinct");
|
|
assert.equal(classifyDirectTargetBlockerScope("m3-gateway-simu-identity"), "identity_not_distinct");
|
|
assert.equal(classifyDirectTargetBlockerScope("m3-patch-panel-wiring"), "patch_panel_wiring_missing");
|
|
|
|
const plan = createLiveOperationPlan();
|
|
assert.equal(plan.evidenceLevel, "DRY-RUN");
|
|
assert.equal(plan.liveWriteWillRun, false);
|
|
assert.ok(plan.failureClassifications.includes("identity_not_distinct"));
|
|
assert.ok(plan.failureClassifications.includes("patch_panel_wiring_missing"));
|
|
assert.ok(plan.failureClassifications.includes("patch_panel_route_owner_invalid"));
|
|
assert.match(plan.refusalPolicy.join("\n"), /source\/read-only/u);
|
|
});
|
|
|
|
test("patch-panel route preflight rejects missing res_boxsimu_1 DO1 to res_boxsimu_2 DI1 wiring or wrong owner", () => {
|
|
const wrongRoute = hasRequiredM3Connection({
|
|
status: {
|
|
serviceId: "hwlab-patch-panel",
|
|
metadata: {
|
|
propagation: "patch-panel-only"
|
|
},
|
|
activeConnections: [
|
|
{
|
|
fromResourceId: "res_boxsimu_1",
|
|
fromPort: "DO1",
|
|
toResourceId: "res_boxsimu_1",
|
|
toPort: "DI1"
|
|
}
|
|
]
|
|
},
|
|
wiring: {
|
|
constraints: {
|
|
propagation: "patch-panel-only",
|
|
localLoopbackSubstituteAllowed: false
|
|
},
|
|
connections: [
|
|
{
|
|
from: { resourceId: "res_boxsimu_1", port: "DO1" },
|
|
to: { resourceId: "res_boxsimu_1", port: "DI1" },
|
|
mode: "exclusive"
|
|
}
|
|
]
|
|
}
|
|
});
|
|
assert.equal(Boolean(wrongRoute.active), false);
|
|
assert.equal(Boolean(wrongRoute.configured), false);
|
|
assert.equal(Boolean(wrongRoute.owner), true);
|
|
|
|
const requiredRoute = hasRequiredM3Connection({
|
|
status: {
|
|
serviceId: "hwlab-patch-panel",
|
|
metadata: {
|
|
propagation: "patch-panel-only"
|
|
},
|
|
activeConnections: [
|
|
{
|
|
fromResourceId: "res_boxsimu_1",
|
|
fromPort: "DO1",
|
|
toResourceId: "res_boxsimu_2",
|
|
toPort: "DI1"
|
|
}
|
|
]
|
|
},
|
|
wiring: {
|
|
constraints: {
|
|
propagation: "patch-panel-only",
|
|
localLoopbackSubstituteAllowed: false
|
|
},
|
|
connections: [
|
|
{
|
|
from: { resourceId: "res_boxsimu_1", port: "DO1" },
|
|
to: { resourceId: "res_boxsimu_2", port: "DI1" },
|
|
mode: "exclusive"
|
|
}
|
|
]
|
|
}
|
|
});
|
|
assert.equal(Boolean(requiredRoute.active), true);
|
|
assert.equal(Boolean(requiredRoute.configured), true);
|
|
assert.equal(Boolean(requiredRoute.owner), true);
|
|
|
|
const wrongOwner = hasRequiredM3Connection({
|
|
status: {
|
|
serviceId: "hwlab-box-simu",
|
|
metadata: {
|
|
propagation: "box-loopback"
|
|
},
|
|
activeConnections: requiredRoute.active ? [requiredRoute.active] : []
|
|
},
|
|
wiring: {
|
|
constraints: {
|
|
propagation: "patch-panel-only",
|
|
localLoopbackSubstituteAllowed: false
|
|
},
|
|
connections: [
|
|
{
|
|
from: { resourceId: "res_boxsimu_1", port: "DO1" },
|
|
to: { resourceId: "res_boxsimu_2", port: "DI1" },
|
|
mode: "exclusive"
|
|
}
|
|
]
|
|
}
|
|
});
|
|
assert.equal(Boolean(wrongOwner.active), true);
|
|
assert.equal(Boolean(wrongOwner.configured), true);
|
|
assert.equal(Boolean(wrongOwner.owner), false);
|
|
});
|
|
|
|
test("Kubernetes discovery preserves current indexed M3 service identities", () => {
|
|
const endpointSlice = (serviceName, address, port, targetRef, labels = {}) => ({
|
|
metadata: {
|
|
name: `${serviceName}-slice`,
|
|
labels: {
|
|
"kubernetes.io/service-name": serviceName,
|
|
...labels
|
|
}
|
|
},
|
|
ports: [{ name: "http", port }],
|
|
endpoints: [
|
|
{
|
|
addresses: [address],
|
|
conditions: { ready: true },
|
|
targetRef: { name: targetRef }
|
|
}
|
|
]
|
|
});
|
|
|
|
const discovery = {
|
|
status: "observed",
|
|
source: "kubernetes-endpointslices",
|
|
services: Object.fromEntries(
|
|
[
|
|
endpointSlice("hwlab-box-simu-1", "10.0.0.11", 7201, "hwlab-box-simu-0", {
|
|
"hwlab.pikastech.local/instance-id": "res_boxsimu_1"
|
|
}),
|
|
endpointSlice("hwlab-box-simu-2", "10.0.0.12", 7201, "hwlab-box-simu-1", {
|
|
"hwlab.pikastech.local/instance-id": "res_boxsimu_2"
|
|
}),
|
|
endpointSlice("hwlab-gateway-simu-1", "10.0.0.21", 7101, "hwlab-gateway-simu-0", {
|
|
"hwlab.pikastech.local/instance-id": "gwsimu_1"
|
|
}),
|
|
endpointSlice("hwlab-gateway-simu-2", "10.0.0.22", 7101, "hwlab-gateway-simu-1", {
|
|
"hwlab.pikastech.local/instance-id": "gwsimu_2"
|
|
}),
|
|
endpointSlice("hwlab-patch-panel", "10.0.0.31", 7301, "hwlab-patch-panel-abc")
|
|
].map((item) => {
|
|
const identity = kubernetesServiceIdentity(item);
|
|
const port = item.ports[0].port;
|
|
const endpoints = item.endpoints.flatMap((endpoint) =>
|
|
endpoint.addresses.map((address) => ({
|
|
url: `http://${address}:${port}`,
|
|
address,
|
|
port,
|
|
targetRef: endpoint.targetRef.name,
|
|
serviceName: identity.serviceName,
|
|
serviceId: identity.serviceId,
|
|
targetId: identity.targetId,
|
|
instanceId: identity.instanceId
|
|
}))
|
|
);
|
|
return [
|
|
identity.serviceName,
|
|
{
|
|
endpointSlice: item.metadata.name,
|
|
...identity,
|
|
port,
|
|
count: endpoints.length,
|
|
endpoints
|
|
}
|
|
];
|
|
})
|
|
)
|
|
};
|
|
|
|
const candidates = candidateTargetsFromKubernetes(discovery);
|
|
|
|
assert.deepEqual(
|
|
candidates.map((candidate) => [
|
|
candidate.id,
|
|
candidate.serviceName,
|
|
candidate.serviceId,
|
|
candidate.instanceId,
|
|
candidate.baseUrl
|
|
]),
|
|
[
|
|
["box-simu-1", "hwlab-box-simu-1", "hwlab-box-simu", "res_boxsimu_1", "http://10.0.0.11:7201"],
|
|
["box-simu-2", "hwlab-box-simu-2", "hwlab-box-simu", "res_boxsimu_2", "http://10.0.0.12:7201"],
|
|
["gateway-simu-1", "hwlab-gateway-simu-1", "hwlab-gateway-simu", "gwsimu_1", "http://10.0.0.21:7101"],
|
|
["gateway-simu-2", "hwlab-gateway-simu-2", "hwlab-gateway-simu", "gwsimu_2", "http://10.0.0.22:7101"],
|
|
["patch-panel", "hwlab-patch-panel", "hwlab-patch-panel", null, "http://10.0.0.31:7301"]
|
|
]
|
|
);
|
|
});
|