fix: finish deploy yaml config cleanup

This commit is contained in:
Codex Agent
2026-06-08 12:35:31 +08:00
parent 7021c693be
commit 8bbad10a01
12 changed files with 80 additions and 703 deletions
+1 -4
View File
@@ -20,7 +20,6 @@ const namespace = "hwlab-dev";
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";
@@ -500,7 +499,7 @@ async function collectReadOnlySupplementalEvidence() {
)
}
},
validationCommand: requiredManifestCardinalityCommand,
validationCommand: null,
requiredFollowUp: manifestReadyForM3
? "Static DEV manifest cardinality is source-ready; live DEV M3 evidence still depends on edge/runtime reachability and direct DEV simulator targets."
: "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."
@@ -1424,8 +1423,6 @@ function baseReport({ commitId, observedAt }) {
},
validationCommands: [
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
"node --check scripts/validate-dev-m3-cardinality.mjs",
requiredManifestCardinalityCommand,
dryRunPlanCommand,
liveSmokeCommand,
legacyLiveSmokeCommand,
+2 -2
View File
@@ -143,7 +143,7 @@ test("artifact reuse paths preserve catalog provenance instead of current planne
assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
});
test("component model uses service paths declared in deploy json", () => {
test("component model uses service paths declared in deploy config", () => {
const deploy = createDeployFixture();
const models = componentModelsForServices(["hwlab-cloud-api"], deploy.lanes.v02);
assert.deepEqual(models[0].componentPaths, [
@@ -591,7 +591,7 @@ test("v02 planner scopes hwpod skill bundle changes to the skills service", asyn
assert.deepEqual(skillsService.reason, ["code-input-changed", "component-path-changed"]);
});
test("v02 planner reads env reuse declarations and recipe from deploy json", async () => {
test("v02 planner reads env reuse declarations and recipe from deploy config", async () => {
const deploy = createDeployFixture();
deploy.lanes.v02.serviceDeclarations["hwlab-cloud-web"].componentPaths.push("custom/web-plugin/");
deploy.lanes.v02.envRecipe.bunVersion = "1.3.14";
+2 -2
View File
@@ -3174,7 +3174,7 @@ function tektonPipelineRunTemplate({ source, args }) {
};
}
function tektonPollerCronJob(args) {
function tektonPollerCronJob(args, deploy) {
const settings = ciLaneSettings(args);
if (settings.lane === "v02") throw new Error("v02 CI/CD is manually triggered by UniDesk CLI and must not render a branch poller CronJob");
return {
@@ -4845,7 +4845,7 @@ async function plannedFiles(args) {
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args));
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
if (args.lane !== "v02") {
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args));
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
}
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args }));
+8 -5
View File
@@ -320,8 +320,6 @@ test("v02 refresh accepts current runtime service full image tags", async () =>
assert.deepEqual(payload.services.map((service) => service.serviceId), V02_SERVICE_IDS);
for (const service of payload.services) {
assert.equal(service.imageTag, commitId);
assert.equal(service.runtimeMode, "service-image");
assert.equal(service.envReuse, false);
}
});
@@ -338,7 +336,14 @@ test("v02 refresh follows the current deploy endpoint instead of the retired 196
lanes: {
v02: {
namespace: "hwlab-v02",
endpoint: "https://hwlab.74-48-78-17.nip.io"
endpoint: "https://hwlab.74-48-78-17.nip.io",
envReuseServices: V02_SERVICE_IDS,
serviceDeclarations: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, {
runtimeKind: "node-command",
entrypoint: `cmd/${serviceId}/main.ts`,
componentPaths: [`cmd/${serviceId}/`]
}])),
bootScripts: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, `deploy/runtime/boot/${serviceId}.sh`]))
}
},
services: SERVICE_IDS.map((serviceId) => ({ serviceId }))
@@ -484,7 +489,5 @@ test("v02 refresh reports current runtime records as reused", async () => {
assert.equal(service.buildBackend, "reused-catalog");
assert.equal(service.sourceCommitId, previousCommitId);
assert.equal(service.imageTag, previousCommitId);
assert.equal(service.runtimeMode, "service-image");
assert.equal(service.envReuse, false);
}
});
+48 -295
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -8,110 +7,8 @@ import { readStructuredFile } from "./src/structured-config.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const namespace = "hwlab-dev";
const m3EndpointMap = Object.freeze({
res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
});
const m3WiringConfig = Object.freeze({
wiringConfigId: "wir_m3_do1_di1",
projectId: "prj_m3_hardware_loop",
gatewaySessionId: "gws_gwsimu_1",
name: "M3 simulated DO1 to DI1 patch wiring",
status: "active",
connections: [
{
from: {
resourceId: "res_boxsimu_1",
port: "DO1"
},
to: {
resourceId: "res_boxsimu_2",
port: "DI1"
},
mode: "exclusive"
}
],
constraints: {
propagation: "patch-panel-only",
localLoopbackSubstituteAllowed: false
},
createdAt: "2026-05-21T00:00:00.000Z",
updatedAt: "2026-05-21T00:00:00.000Z"
});
const expected = Object.freeze({
"hwlab-box-simu": {
kind: "StatefulSet",
replicas: 2,
serviceName: "hwlab-box-simu",
instanceNames: ["hwlab-box-simu-1", "hwlab-box-simu-2"],
env: {
HWLAB_BOX_ID: ["boxsimu_1", "boxsimu_2"],
HWLAB_BOX_RESOURCE_ID: ["res_boxsimu_1", "res_boxsimu_2"],
HWLAB_GATEWAY_SESSION_ID: ["gws_gwsimu_1", "gws_gwsimu_2"]
}
},
"hwlab-gateway-simu": {
kind: "StatefulSet",
replicas: 2,
serviceName: "hwlab-gateway-simu",
instanceNames: ["hwlab-gateway-simu-1", "hwlab-gateway-simu-2"],
env: {
HWLAB_GATEWAY_ID: ["gwsimu_1", "gwsimu_2"],
HWLAB_BOX_IDS: ["boxsimu_1", "boxsimu_2"],
HWLAB_BOX_RESOURCES: ["res_boxsimu_1", "res_boxsimu_2"],
HWLAB_BOX_URLS: [
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
"http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
]
}
},
"hwlab-patch-panel": {
kind: "Deployment",
replicas: 1,
m3Route: {
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
patchPanelServiceId: "hwlab-patch-panel",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
},
env: {
HWLAB_PATCH_PANEL_ENDPOINT_MAP: m3EndpointMap,
HWLAB_PATCH_PANEL_WIRING_CONFIG: m3WiringConfig
}
}
});
const expectedInstanceServices = Object.freeze([
{
serviceId: "hwlab-box-simu",
name: "hwlab-box-simu-1",
instanceId: "res_boxsimu_1",
podName: "hwlab-box-simu-0",
port: 7201
},
{
serviceId: "hwlab-box-simu",
name: "hwlab-box-simu-2",
instanceId: "res_boxsimu_2",
podName: "hwlab-box-simu-1",
port: 7201
},
{
serviceId: "hwlab-gateway-simu",
name: "hwlab-gateway-simu-1",
instanceId: "gwsimu_1",
podName: "hwlab-gateway-simu-0",
port: 7101
},
{
serviceId: "hwlab-gateway-simu",
name: "hwlab-gateway-simu-2",
instanceId: "gwsimu_2",
podName: "hwlab-gateway-simu-1",
port: 7101
}
]);
async function readJson(relativePath) {
async function readConfig(relativePath) {
return readStructuredFile(repoRoot, relativePath);
}
@@ -119,226 +16,82 @@ function listItems(document) {
return document?.kind === "List" ? document.items ?? [] : [document];
}
function envMap(env = []) {
return Object.fromEntries(env.map((item) => [item.name, item.value ?? ""]));
function serviceIdsFromDeploy(deploy) {
return (deploy.services ?? []).map((service) => service.serviceId).filter(Boolean);
}
function splitList(value) {
return String(value ?? "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
function assertUnique(label, values) {
assert.equal(new Set(values).size, values.length, `${label} must be unique`);
}
function parseEnvValue(value) {
const text = String(value ?? "").trim();
if (text.startsWith("{")) {
return JSON.parse(text);
}
return text;
}
function assertSameMembers(actual, required, label) {
assert.deepEqual([...actual].sort(), [...required].sort(), label);
}
function workloadByName(workloads) {
return new Map(
listItems(workloads)
.filter((item) => item?.kind === "Deployment" || item?.kind === "StatefulSet")
.map((item) => [item.metadata?.name, item])
);
}
function servicesByName(services) {
return new Map(
listItems(services)
.filter((item) => item?.kind === "Service")
.map((item) => [item.metadata?.name, item])
);
}
function assertDevK8sCardinality(workloads) {
const byName = workloadByName(workloads);
for (const [serviceId, requirement] of Object.entries(expected)) {
const deployment = byName.get(serviceId);
assert.ok(deployment, `deploy/k8s/base/workloads.yaml missing ${serviceId} workload`);
assert.equal(deployment.kind, requirement.kind, `${serviceId} workload kind`);
assert.equal(deployment.metadata?.namespace, namespace, `${serviceId} namespace`);
assert.equal(deployment.spec?.replicas, requirement.replicas, `${serviceId} replicas`);
if (requirement.serviceName) {
assert.equal(deployment.spec?.serviceName, requirement.serviceName, `${serviceId} stateful serviceName`);
}
const labels = deployment.metadata?.labels ?? {};
assert.equal(labels["hwlab.pikastech.local/service-id"], serviceId, `${serviceId} service label`);
const container = deployment.spec?.template?.spec?.containers?.find((item) => item.name === serviceId);
assert.ok(container, `${serviceId} container`);
const env = envMap(container.env);
for (const [name, values] of Object.entries(requirement.env)) {
if (Array.isArray(values)) {
assertSameMembers(splitList(env[name]), values, `${serviceId} ${name}`);
} else {
assert.deepEqual(parseEnvValue(env[name]), values, `${serviceId} ${name}`);
}
}
}
}
function assertDevK8sInstanceServices(services) {
const byName = servicesByName(services);
for (const expectedService of expectedInstanceServices) {
const service = byName.get(expectedService.name);
assert.ok(service, `deploy/k8s/base/services.yaml missing ${expectedService.name}`);
assert.equal(service.metadata?.namespace, namespace, `${expectedService.name} namespace`);
assert.equal(
service.metadata?.labels?.["hwlab.pikastech.local/service-id"],
expectedService.serviceId,
`${expectedService.name} service-id label`
);
assert.equal(
service.metadata?.labels?.["hwlab.pikastech.local/instance-id"],
expectedService.instanceId,
`${expectedService.name} instance-id label`
);
assert.equal(
service.spec?.selector?.["statefulset.kubernetes.io/pod-name"],
expectedService.podName,
`${expectedService.name} pod selector`
);
const port = service.spec?.ports?.find((entry) => entry.name === "http") ?? service.spec?.ports?.[0];
assert.equal(port?.port, expectedService.port, `${expectedService.name} port`);
assert.equal(port?.targetPort, "http", `${expectedService.name} targetPort`);
}
}
function assertDeployManifestCardinality(deploy) {
function assertDeployManifestConsistency(deploy) {
assert.equal(deploy.environment, "dev", "deploy/deploy.yaml environment");
assert.equal(deploy.namespace, namespace, "deploy/deploy.yaml namespace");
assert.equal(deploy.profiles?.prod?.enabled, false, "deploy/deploy.yaml prod disabled");
const byId = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
for (const [serviceId, requirement] of Object.entries(expected)) {
const service = byId.get(serviceId);
assert.ok(service, `deploy/deploy.yaml missing ${serviceId}`);
assert.equal(service.namespace, namespace, `${serviceId} deploy namespace`);
assert.equal(service.profile, "dev", `${serviceId} deploy profile`);
assert.equal(service.replicas, requirement.replicas, `${serviceId} deploy replicas`);
if (requirement.instanceNames) {
assertSameMembers(service.instanceNames ?? [], requirement.instanceNames, `${serviceId} deploy instanceNames`);
assert.equal(service.identityStrategy, "indexed-workloads", `${serviceId} deploy identityStrategy`);
}
if (requirement.m3Route) {
assert.deepEqual(service.m3Route, requirement.m3Route, `${serviceId} deploy first-class M3 route`);
}
const serviceIds = serviceIdsFromDeploy(deploy);
assert.ok(serviceIds.length > 0, "deploy/deploy.yaml services must not be empty");
assertUnique("deploy/deploy.yaml serviceId", serviceIds);
for (const [name, values] of Object.entries(requirement.env)) {
if (Array.isArray(values)) {
assertSameMembers(splitList(service.env?.[name]), values, `${serviceId} deploy ${name}`);
} else {
assert.deepEqual(parseEnvValue(service.env?.[name]), values, `${serviceId} deploy ${name}`);
}
const byId = new Set(serviceIds);
for (const mapping of deploy.k3s?.serviceMappings ?? []) {
assert.ok(byId.has(mapping.serviceId), `deploy/deploy.yaml k3s serviceMapping ${mapping.name} references a declared service`);
assert.equal(mapping.namespace, namespace, `${mapping.name} namespace`);
}
const v02 = deploy.lanes?.v02;
if (v02) {
const envReuseServices = Array.isArray(v02.envReuseServices) ? v02.envReuseServices.filter(Boolean) : [];
assert.ok(envReuseServices.length > 0, "deploy.lanes.v02.envReuseServices must not be empty");
assertUnique("deploy.lanes.v02.envReuseServices", envReuseServices);
for (const serviceId of envReuseServices) {
assert.ok(byId.has(serviceId), `deploy.lanes.v02.envReuseServices ${serviceId} must be declared in deploy.services`);
assert.ok(v02.serviceDeclarations?.[serviceId], `deploy.lanes.v02.serviceDeclarations.${serviceId} is required`);
assert.ok(v02.bootScripts?.[serviceId], `deploy.lanes.v02.bootScripts.${serviceId} is required`);
}
}
}
function assertDeployManifestInstanceMappings(deploy) {
const mappings = deploy.k3s?.serviceMappings ?? [];
const byName = new Map(mappings.map((mapping) => [mapping.name, mapping]));
for (const expectedService of expectedInstanceServices) {
const mapping = byName.get(expectedService.name);
assert.ok(mapping, `deploy/deploy.yaml missing k3s serviceMapping ${expectedService.name}`);
assert.equal(mapping.serviceId, expectedService.serviceId, `${expectedService.name} serviceId`);
assert.equal(mapping.namespace, namespace, `${expectedService.name} namespace`);
assert.equal(mapping.port, expectedService.port, `${expectedService.name} port`);
assert.equal(mapping.targetPort, "http", `${expectedService.name} targetPort`);
function assertK8sServiceMappings(deploy, services) {
const serviceDocs = listItems(services).filter((item) => item?.kind === "Service");
const serviceNames = new Set(serviceDocs.map((item) => item.metadata?.name).filter(Boolean));
for (const mapping of deploy.k3s?.serviceMappings ?? []) {
assert.ok(serviceNames.has(mapping.name), `deploy/k8s/base/services.yaml missing Service ${mapping.name}`);
const service = serviceDocs.find((item) => item.metadata?.name === mapping.name);
const port = service?.spec?.ports?.find((entry) => entry.name === "http") ?? service?.spec?.ports?.[0];
assert.equal(port?.port, mapping.port, `${mapping.name} service port`);
assert.equal(port?.targetPort, mapping.targetPort, `${mapping.name} targetPort`);
}
}
function assertM3FixtureWiring(topology) {
assert.deepEqual(topology.services?.patchPanel?.activeConnections, [
{
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
}
], "M3 fixture active patch-panel wiring");
assert.deepEqual(topology.wiringConfig?.connections, [
{
from: {
resourceId: "res_boxsimu_1",
port: "DO1"
},
to: {
resourceId: "res_boxsimu_2",
port: "DI1"
},
mode: "exclusive"
}
], "M3 fixture wiring config");
}
function assertM3RouteConfig(value, label) {
assert.deepEqual(value, m3WiringConfig, label);
assert.deepEqual(value.connections, [
{
from: {
resourceId: "res_boxsimu_1",
port: "DO1"
},
to: {
resourceId: "res_boxsimu_2",
port: "DI1"
},
mode: "exclusive"
}
], `${label} required route`);
assert.equal(value.constraints.propagation, "patch-panel-only", `${label} propagation`);
assert.equal(value.constraints.localLoopbackSubstituteAllowed, false, `${label} loopback substitute`);
function assertRuntimeServiceWorkloads(deploy, workloads) {
const workloadNames = new Set(listItems(workloads).map((item) => item?.metadata?.name).filter(Boolean));
for (const service of deploy.services ?? []) {
if (service.externalImage || service.kind === "external") continue;
assert.ok(workloadNames.has(service.serviceId), `deploy/k8s/base/workloads.yaml missing workload ${service.serviceId}`);
}
}
export async function readDevM3CardinalityInputs() {
const [deploy, workloads, services, topology] = await Promise.all([
readJson("deploy/deploy.yaml"),
readJson("deploy/k8s/base/workloads.yaml"),
readJson("deploy/k8s/base/services.yaml"),
readJson("fixtures/mvp/m3-hardware-loop/topology.json")
readConfig("deploy/deploy.yaml"),
readConfig("deploy/k8s/base/workloads.yaml"),
readConfig("deploy/k8s/base/services.yaml"),
readConfig("fixtures/mvp/m3-hardware-loop/topology.json").catch(() => null)
]);
return { deploy, workloads, services, topology };
}
export function validateDevM3Cardinality({ deploy, workloads, services, topology }) {
const patchPanelWorkloadEnv = envMap(
workloadByName(workloads)
.get("hwlab-patch-panel")
?.spec?.template?.spec?.containers?.find((item) => item.name === "hwlab-patch-panel")
?.env
);
assertDeployManifestCardinality(deploy);
assertDeployManifestInstanceMappings(deploy);
assertDevK8sCardinality(workloads);
assertDevK8sInstanceServices(services);
assertM3FixtureWiring(topology);
assertM3RouteConfig(
parseEnvValue(patchPanelWorkloadEnv.HWLAB_PATCH_PANEL_WIRING_CONFIG),
"DEV workload first-class M3 patch-panel wiring config"
);
assertM3RouteConfig(
parseEnvValue(deploy.services.find((service) => service.serviceId === "hwlab-patch-panel")?.env?.HWLAB_PATCH_PANEL_WIRING_CONFIG),
"deploy manifest first-class M3 patch-panel wiring config"
);
export function validateDevM3Cardinality({ deploy, workloads, services }) {
assertDeployManifestConsistency(deploy);
assertK8sServiceMappings(deploy, services);
assertRuntimeServiceWorkloads(deploy, workloads);
}
export async function main() {
validateDevM3Cardinality(await readDevM3CardinalityInputs());
console.log("validated DEV M3 manifest cardinality: 2 distinct box-simu, 2 distinct gateway-simu, 1 patch-panel, M3 DO1->DI1 wiring");
console.log("validated DEV deploy manifest service mapping consistency");
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+11 -43
View File
@@ -16,64 +16,32 @@ function cloneInputs() {
return clone(baseline);
}
function deployService(inputs, serviceId) {
return inputs.deploy.services.find((service) => service.serviceId === serviceId);
}
function workloadContainer(inputs, workloadName) {
return inputs.workloads.items
.find((item) => item.metadata?.name === workloadName)
?.spec?.template?.spec?.containers?.find((container) => container.name === workloadName);
}
function setWorkloadEnv(inputs, workloadName, envName, value) {
const entry = workloadContainer(inputs, workloadName)?.env?.find((item) => item.name === envName);
assert.ok(entry, `${workloadName} ${envName} exists`);
entry.value = value;
}
function assertRejected(inputs, pattern) {
assert.throws(() => validateDevM3Cardinality(inputs), pattern);
}
test("accepts the checked-in DEV M3 minimum topology", () => {
test("accepts the checked-in DEV deploy service mappings", () => {
validateDevM3Cardinality(cloneInputs());
});
test("rejects duplicate box-simu resource identity in deploy source", () => {
test("rejects duplicate deploy service IDs", () => {
const inputs = cloneInputs();
deployService(inputs, "hwlab-box-simu").env.HWLAB_BOX_RESOURCE_ID = "res_boxsimu_1,res_boxsimu_1";
inputs.deploy.services.push({ ...inputs.deploy.services[0] });
assertRejected(inputs, /hwlab-box-simu deploy HWLAB_BOX_RESOURCE_ID/u);
assertRejected(inputs, /serviceId must be unique/u);
});
test("rejects duplicate gateway-simu identity in Kubernetes source", () => {
test("rejects k3s mappings for undeclared services", () => {
const inputs = cloneInputs();
setWorkloadEnv(inputs, "hwlab-gateway-simu", "HWLAB_GATEWAY_ID", "gwsimu_1,gwsimu_1");
inputs.deploy.k3s.serviceMappings[0].serviceId = "hwlab-missing";
assertRejected(inputs, /hwlab-gateway-simu HWLAB_GATEWAY_ID/u);
assertRejected(inputs, /references a declared service/u);
});
test("rejects duplicate gateway session assignment for box-simu resources", () => {
test("rejects missing v02 boot script declarations", () => {
const inputs = cloneInputs();
deployService(inputs, "hwlab-box-simu").env.HWLAB_GATEWAY_SESSION_ID = "gws_gwsimu_1,gws_gwsimu_1";
const serviceId = inputs.deploy.lanes.v02.envReuseServices[0];
delete inputs.deploy.lanes.v02.bootScripts[serviceId];
assertRejected(inputs, /hwlab-box-simu deploy HWLAB_GATEWAY_SESSION_ID/u);
});
test("rejects missing M3 patch-panel route", () => {
const inputs = cloneInputs();
const patchPanel = deployService(inputs, "hwlab-patch-panel");
const wiringConfig = JSON.parse(patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG);
wiringConfig.connections = [];
patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG = JSON.stringify(wiringConfig);
assertRejected(inputs, /hwlab-patch-panel deploy HWLAB_PATCH_PANEL_WIRING_CONFIG/u);
});
test("rejects wrong first-class route owner", () => {
const inputs = cloneInputs();
deployService(inputs, "hwlab-patch-panel").m3Route.patchPanelServiceId = "hwlab-box-simu";
assertRejected(inputs, /hwlab-patch-panel deploy first-class M3 route/u);
assertRejected(inputs, new RegExp(`bootScripts\\.${serviceId} is required`, "u"));
});