test: add topology constraint validator

This commit is contained in:
HWLAB Code Queue
2026-05-21 15:31:11 +00:00
parent 619fbc830e
commit 277bdb0d44
6 changed files with 1485 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Topology Constraint Validator
`scripts/validate-topology-constraints.mjs` is a local contract validator for
the MVP topology rules that were prose-only after M0. It reads fixtures from
`fixtures/mvp/topology-constraints/` and does not call DEV, PROD, real
hardware, databases, secrets, or service endpoints.
Run it locally:
```sh
node scripts/validate-topology-constraints.mjs
```
## Contract
- The fixture must describe a DEV topology with `hwlab-box-simu`,
`hwlab-gateway-simu`, and one active `hwlab-patch-panel` status object.
- Cross-device propagation must remain `patch-panel-only`. Box simulator local
loopback is not a valid substitute for cross-device sync.
- Wiring endpoints must reference known box resources and declared ports.
- Active wiring configs must not contain resource-level directed cycles.
- Active config exclusivity is enforced per project and resource group. The
resource group is `wiringConfig.constraints.resourceGroupId` when present;
otherwise it is the sorted set of resources touched by the config.
- A resource group may have only one active wiring config unless the active
patch panel reports `metadata.activeConfigOverride`.
- A patch-panel override is valid only when it is reported by an active
`hwlab-patch-panel`, preserves `patch-panel-only`, lists exactly the active
config ids for the resource group, gives a non-empty reason, and selects the
same `wiringConfigId` as the patch panel status object.
- The patch panel's `activeConnections` must match the selected wiring config.
## Fixtures
| Fixture | Expected result | Purpose |
| --- | --- | --- |
| `valid-topology.json` | valid | One active acyclic config routed by `hwlab-patch-panel`. |
| `invalid-cycle.json` | `wiring_cycle` | Detects a directed resource cycle in active wiring. |
| `invalid-multiple-active-configs.json` | `active_config_exclusivity` | Rejects multiple active configs in the same resource group without an override. |
| `invalid-patch-panel-override.json` | `patch_panel_override_invalid` | Rejects a patch-panel override whose selected config disagrees with patch panel state. |
Each fixture stores `expectedValidation` beside the topology. The validator
passes only when valid fixtures produce no violations and invalid fixtures
produce the expected violation code set. This makes the validator a pre-DEV
contract for the future real M3 flow while keeping the current M3 hardware loop
smoke focused on local patch-panel behavior.
@@ -0,0 +1,202 @@
{
"caseId": "invalid-cycle",
"expectedValidation": {
"valid": false,
"violationCodes": ["wiring_cycle"]
},
"topology": {
"topologyId": "top_constraints_cycle",
"projectId": "prj_topology_constraints",
"environment": "dev",
"services": {
"boxes": [
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"live": true,
"resource": {
"resourceId": "res_constraints_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"boxId": "boxsim_alpha",
"resourceType": "simulator_endpoint",
"name": "boxsim_alpha simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
},
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"live": true,
"resource": {
"resourceId": "res_constraints_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"boxId": "boxsim_beta",
"resourceType": "simulator_endpoint",
"name": "boxsim_beta simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
}
],
"gateways": [
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_alpha",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"endpoint": "http://127.0.0.1:7301",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_alpha"]
},
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_beta",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"endpoint": "http://127.0.0.1:7302",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_beta"]
}
],
"patchPanel": {
"patchPanelStatusId": "pps_constraints_cycle",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"wiringConfigId": "wir_constraints_cycle",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_constraints_alpha",
"fromPort": "DO1",
"toResourceId": "res_constraints_beta",
"toPort": "DI1"
},
{
"fromResourceId": "res_constraints_beta",
"fromPort": "DO1",
"toResourceId": "res_constraints_alpha",
"toPort": "DI1"
}
],
"observedAt": "2026-05-21T00:00:00.000Z",
"metadata": {
"propagation": "patch-panel-only",
"connectionCount": 2
}
}
},
"wiringConfigs": [
{
"wiringConfigId": "wir_constraints_cycle",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints cycle wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_alpha",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_beta",
"port": "DI1"
},
"mode": "exclusive"
},
{
"from": {
"resourceId": "res_constraints_beta",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_alpha",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"constraints": {
"patchPanelIsOnlyCrossDeviceChannel": true,
"boxSimuLocalLoopbackForCrossDeviceSync": false
}
}
}
@@ -0,0 +1,212 @@
{
"caseId": "invalid-multiple-active-configs",
"expectedValidation": {
"valid": false,
"violationCodes": ["active_config_exclusivity"]
},
"topology": {
"topologyId": "top_constraints_multiple_active",
"projectId": "prj_topology_constraints",
"environment": "dev",
"services": {
"boxes": [
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"live": true,
"resource": {
"resourceId": "res_constraints_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"boxId": "boxsim_alpha",
"resourceType": "simulator_endpoint",
"name": "boxsim_alpha simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
},
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"live": true,
"resource": {
"resourceId": "res_constraints_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"boxId": "boxsim_beta",
"resourceType": "simulator_endpoint",
"name": "boxsim_beta simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
}
],
"gateways": [
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_alpha",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"endpoint": "http://127.0.0.1:7301",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_alpha"]
},
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_beta",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"endpoint": "http://127.0.0.1:7302",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_beta"]
}
],
"patchPanel": {
"patchPanelStatusId": "pps_constraints_multiple_active",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"wiringConfigId": "wir_constraints_active_primary",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_constraints_alpha",
"fromPort": "DO1",
"toResourceId": "res_constraints_beta",
"toPort": "DI1"
}
],
"observedAt": "2026-05-21T00:00:00.000Z",
"metadata": {
"propagation": "patch-panel-only",
"connectionCount": 1
}
}
},
"wiringConfigs": [
{
"wiringConfigId": "wir_constraints_active_primary",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints active primary wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_alpha",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_beta",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
{
"wiringConfigId": "wir_constraints_active_secondary",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints active secondary wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_beta",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_alpha",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"constraints": {
"patchPanelIsOnlyCrossDeviceChannel": true,
"boxSimuLocalLoopbackForCrossDeviceSync": false
}
}
}
@@ -0,0 +1,221 @@
{
"caseId": "invalid-patch-panel-override",
"expectedValidation": {
"valid": false,
"violationCodes": ["patch_panel_override_invalid"]
},
"topology": {
"topologyId": "top_constraints_bad_override",
"projectId": "prj_topology_constraints",
"environment": "dev",
"services": {
"boxes": [
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"live": true,
"resource": {
"resourceId": "res_constraints_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"boxId": "boxsim_alpha",
"resourceType": "simulator_endpoint",
"name": "boxsim_alpha simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
},
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"live": true,
"resource": {
"resourceId": "res_constraints_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"boxId": "boxsim_beta",
"resourceType": "simulator_endpoint",
"name": "boxsim_beta simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
}
],
"gateways": [
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_alpha",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"endpoint": "http://127.0.0.1:7301",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_alpha"]
},
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_beta",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"endpoint": "http://127.0.0.1:7302",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_beta"]
}
],
"patchPanel": {
"patchPanelStatusId": "pps_constraints_bad_override",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"wiringConfigId": "wir_constraints_override_secondary",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_constraints_beta",
"fromPort": "DO1",
"toResourceId": "res_constraints_alpha",
"toPort": "DI1"
}
],
"observedAt": "2026-05-21T00:00:00.000Z",
"metadata": {
"propagation": "patch-panel-only",
"connectionCount": 1,
"activeConfigOverride": {
"resourceGroupId": "rg_constraints_alpha_beta",
"activeWiringConfigIds": [
"wir_constraints_override_primary",
"wir_constraints_override_secondary"
],
"selectedWiringConfigId": "wir_constraints_override_primary",
"reason": "Fixture intentionally disagrees with patch panel selected wiringConfigId."
}
}
}
},
"wiringConfigs": [
{
"wiringConfigId": "wir_constraints_override_primary",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints override primary wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_alpha",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_beta",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
{
"wiringConfigId": "wir_constraints_override_secondary",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints override secondary wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_beta",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_alpha",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"constraints": {
"patchPanelIsOnlyCrossDeviceChannel": true,
"boxSimuLocalLoopbackForCrossDeviceSync": false
}
}
}
@@ -0,0 +1,185 @@
{
"caseId": "valid-topology",
"expectedValidation": {
"valid": true,
"violationCodes": []
},
"topology": {
"topologyId": "top_constraints_valid",
"projectId": "prj_topology_constraints",
"environment": "dev",
"services": {
"boxes": [
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"live": true,
"resource": {
"resourceId": "res_constraints_alpha",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"boxId": "boxsim_alpha",
"resourceType": "simulator_endpoint",
"name": "boxsim_alpha simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
},
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"live": true,
"resource": {
"resourceId": "res_constraints_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_beta",
"boxId": "boxsim_beta",
"resourceType": "simulator_endpoint",
"name": "boxsim_beta simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["DO1", "DI1"],
"propagation": "patch-panel-only"
}
},
"ports": {
"DO1": {
"port": "DO1",
"direction": "output",
"value": false,
"source": "local"
},
"DI1": {
"port": "DI1",
"direction": "input",
"value": false,
"source": "local"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
}
}
],
"gateways": [
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_alpha",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_alpha",
"endpoint": "http://127.0.0.1:7301",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_alpha"]
},
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"projectId": "prj_topology_constraints",
"live": true,
"session": {
"gatewaySessionId": "gws_constraints_beta",
"projectId": "prj_topology_constraints",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_beta",
"endpoint": "http://127.0.0.1:7302",
"status": "connected",
"environment": "dev"
},
"boxes": ["res_constraints_beta"]
}
],
"patchPanel": {
"patchPanelStatusId": "pps_constraints_valid",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"wiringConfigId": "wir_constraints_alpha_beta",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_constraints_alpha",
"fromPort": "DO1",
"toResourceId": "res_constraints_beta",
"toPort": "DI1"
}
],
"observedAt": "2026-05-21T00:00:00.000Z",
"metadata": {
"propagation": "patch-panel-only",
"connectionCount": 1
}
}
},
"wiringConfigs": [
{
"wiringConfigId": "wir_constraints_alpha_beta",
"projectId": "prj_topology_constraints",
"gatewaySessionId": "gws_constraints_alpha",
"name": "Topology constraints valid alpha to beta wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_constraints_alpha",
"port": "DO1"
},
"to": {
"resourceId": "res_constraints_beta",
"port": "DI1"
},
"mode": "exclusive"
}
],
"constraints": {
"resourceGroupId": "rg_constraints_alpha_beta",
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
}
],
"constraints": {
"patchPanelIsOnlyCrossDeviceChannel": true,
"boxSimuLocalLoopbackForCrossDeviceSync": false
}
}
}
+619
View File
@@ -0,0 +1,619 @@
#!/usr/bin/env node
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultFixtureDir = path.join(repoRoot, "fixtures/mvp/topology-constraints");
const PATCH_PANEL_SERVICE_ID = "hwlab-patch-panel";
const PATCH_PANEL_ONLY = "patch-panel-only";
const ACTIVE = "active";
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasOwn(value, key) {
return Object.prototype.hasOwnProperty.call(value, key);
}
function addViolation(violations, code, message, jsonPath = "$") {
violations.push({ code, message, path: jsonPath });
}
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
function sameStringSet(left, right) {
const leftSorted = uniqueSorted(left);
const rightSorted = uniqueSorted(right);
return (
leftSorted.length === rightSorted.length &&
leftSorted.every((value, index) => value === rightSorted[index])
);
}
async function collectJsonFiles(target) {
const targetStat = await stat(target);
if (targetStat.isFile()) {
return target.endsWith(".json") ? [target] : [];
}
const entries = await readdir(target, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path.join(target, entry.name);
if (entry.isDirectory()) {
files.push(...(await collectJsonFiles(entryPath)));
} else if (entry.isFile() && entry.name.endsWith(".json")) {
files.push(entryPath);
}
}
return files.sort();
}
function getFixtureTargets() {
const args = process.argv.slice(2);
if (args.length === 0) {
return [defaultFixtureDir];
}
return args.map((arg) => path.resolve(process.cwd(), arg));
}
function getCaseId(filePath, fixture) {
return fixture.caseId ?? path.basename(filePath, ".json");
}
function getTopology(fixture) {
return fixture.topology ?? fixture;
}
function getExpectedValidation(fixture) {
return (
fixture.expectedValidation ?? {
valid: true,
violationCodes: []
}
);
}
function getWiringConfigs(topology, violations) {
if (Array.isArray(topology.wiringConfigs)) {
return topology.wiringConfigs;
}
if (isObject(topology.wiringConfig)) {
return [topology.wiringConfig];
}
addViolation(
violations,
"topology_shape",
"topology requires wiringConfigs[] or wiringConfig",
"$.topology.wiringConfigs"
);
return [];
}
function indexBoxes(topology, violations) {
const boxes = topology.services?.boxes;
if (!Array.isArray(boxes) || boxes.length === 0) {
addViolation(violations, "topology_shape", "topology requires at least one box simulator", "$.topology.services.boxes");
return new Map();
}
const resources = new Map();
boxes.forEach((box, index) => {
const jsonPath = `$.topology.services.boxes[${index}]`;
const resource = box.resource;
const resourceId = resource?.resourceId;
if (box.serviceId !== "hwlab-box-simu") {
addViolation(violations, "topology_shape", "box serviceId must be hwlab-box-simu", `${jsonPath}.serviceId`);
}
if (box.projectId !== topology.projectId || resource?.projectId !== topology.projectId) {
addViolation(violations, "topology_shape", "box projectId must match topology projectId", jsonPath);
}
if (!resourceId) {
addViolation(violations, "topology_shape", "box resource.resourceId is required", `${jsonPath}.resource.resourceId`);
return;
}
if (resources.has(resourceId)) {
addViolation(violations, "topology_shape", `duplicate resourceId ${resourceId}`, `${jsonPath}.resource.resourceId`);
}
if (box.constraints?.crossDevicePropagation !== PATCH_PANEL_ONLY) {
addViolation(
violations,
"patch_panel_only",
"box cross-device propagation must be patch-panel-only",
`${jsonPath}.constraints.crossDevicePropagation`
);
}
if (box.constraints?.localLoopbackEnabled !== false) {
addViolation(
violations,
"patch_panel_only",
"box local loopback cannot substitute for cross-device propagation",
`${jsonPath}.constraints.localLoopbackEnabled`
);
}
if (resource.metadata?.propagation !== PATCH_PANEL_ONLY) {
addViolation(
violations,
"patch_panel_only",
"box resource metadata propagation must be patch-panel-only",
`${jsonPath}.resource.metadata.propagation`
);
}
resources.set(resourceId, {
box,
jsonPath,
ports: isObject(box.ports) ? box.ports : {},
declaredPorts: new Set(asArray(resource.metadata?.ports))
});
});
return resources;
}
function validateGateways(topology, resources, violations) {
const gateways = topology.services?.gateways;
if (!Array.isArray(gateways) || gateways.length === 0) {
addViolation(violations, "topology_shape", "topology requires gateway simulators", "$.topology.services.gateways");
return new Set();
}
const sessions = new Set();
gateways.forEach((gateway, index) => {
const jsonPath = `$.topology.services.gateways[${index}]`;
if (gateway.serviceId !== "hwlab-gateway-simu") {
addViolation(violations, "topology_shape", "gateway serviceId must be hwlab-gateway-simu", `${jsonPath}.serviceId`);
}
if (gateway.projectId !== topology.projectId || gateway.session?.projectId !== topology.projectId) {
addViolation(violations, "topology_shape", "gateway projectId must match topology projectId", jsonPath);
}
if (gateway.session?.status !== "connected") {
addViolation(violations, "topology_shape", "gateway simulator session must be connected", `${jsonPath}.session.status`);
}
if (gateway.session?.gatewaySessionId) {
sessions.add(gateway.session.gatewaySessionId);
}
asArray(gateway.boxes).forEach((resourceId, boxIndex) => {
if (!resources.has(resourceId)) {
addViolation(
violations,
"endpoint_reference",
`gateway references unknown box resource ${resourceId}`,
`${jsonPath}.boxes[${boxIndex}]`
);
}
});
});
return sessions;
}
function validatePatchPanel(topology, sessions, violations) {
const patchPanel = topology.services?.patchPanel;
if (!isObject(patchPanel)) {
addViolation(violations, "topology_shape", "topology requires one patch panel status object", "$.topology.services.patchPanel");
return {};
}
if (patchPanel.serviceId !== PATCH_PANEL_SERVICE_ID) {
addViolation(
violations,
"patch_panel_override_invalid",
"patch panel override must be reported by hwlab-patch-panel",
"$.topology.services.patchPanel.serviceId"
);
}
if (patchPanel.state !== ACTIVE) {
addViolation(violations, "topology_shape", "patch panel status must be active", "$.topology.services.patchPanel.state");
}
if (patchPanel.projectId !== topology.projectId) {
addViolation(violations, "topology_shape", "patch panel projectId must match topology projectId", "$.topology.services.patchPanel.projectId");
}
if (patchPanel.metadata?.propagation !== PATCH_PANEL_ONLY) {
addViolation(
violations,
"patch_panel_only",
"patch panel propagation metadata must be patch-panel-only",
"$.topology.services.patchPanel.metadata.propagation"
);
}
if (patchPanel.gatewaySessionId && sessions.size > 0 && !sessions.has(patchPanel.gatewaySessionId)) {
addViolation(
violations,
"endpoint_reference",
"patch panel gatewaySessionId must belong to a known gateway simulator",
"$.topology.services.patchPanel.gatewaySessionId"
);
}
return patchPanel;
}
function portExists(resourceEntry, port) {
return hasOwn(resourceEntry.ports, port) || resourceEntry.declaredPorts.has(port);
}
function validateEndpoint(endpoint, resources, violations, jsonPath) {
if (!isObject(endpoint)) {
addViolation(violations, "topology_shape", "connection endpoint must be an object", jsonPath);
return;
}
const resource = resources.get(endpoint.resourceId);
if (!resource) {
addViolation(violations, "endpoint_reference", `unknown resource ${endpoint.resourceId}`, `${jsonPath}.resourceId`);
return;
}
if (!portExists(resource, endpoint.port)) {
addViolation(
violations,
"endpoint_reference",
`unknown port ${endpoint.resourceId}:${endpoint.port}`,
`${jsonPath}.port`
);
}
}
function validateWiringConfig(config, index, topology, resources, sessions, violations) {
const jsonPath = `$.topology.wiringConfigs[${index}]`;
if (config.projectId !== topology.projectId) {
addViolation(violations, "topology_shape", "wiring config projectId must match topology projectId", `${jsonPath}.projectId`);
}
if (config.gatewaySessionId && sessions.size > 0 && !sessions.has(config.gatewaySessionId)) {
addViolation(
violations,
"endpoint_reference",
"wiring config gatewaySessionId must belong to a known gateway simulator",
`${jsonPath}.gatewaySessionId`
);
}
if (!Array.isArray(config.connections) || config.connections.length === 0) {
addViolation(violations, "topology_shape", "wiring config requires at least one connection", `${jsonPath}.connections`);
return;
}
if (config.constraints?.propagation !== PATCH_PANEL_ONLY) {
addViolation(
violations,
"patch_panel_only",
"wiring config propagation must be patch-panel-only",
`${jsonPath}.constraints.propagation`
);
}
if (config.constraints?.localLoopbackSubstituteAllowed !== false) {
addViolation(
violations,
"patch_panel_only",
"wiring config cannot allow local loopback as a substitute",
`${jsonPath}.constraints.localLoopbackSubstituteAllowed`
);
}
config.connections.forEach((connection, connectionIndex) => {
const connectionPath = `${jsonPath}.connections[${connectionIndex}]`;
validateEndpoint(connection.from, resources, violations, `${connectionPath}.from`);
validateEndpoint(connection.to, resources, violations, `${connectionPath}.to`);
});
}
function getActiveConnectionKey(connection) {
return [
connection.fromResourceId,
connection.fromPort,
connection.toResourceId,
connection.toPort
].join("->");
}
function connectionToPatchPanelConnection(connection) {
return {
fromResourceId: connection.from.resourceId,
fromPort: connection.from.port,
toResourceId: connection.to.resourceId,
toPort: connection.to.port
};
}
function validatePatchPanelMatchesSelectedConfig(patchPanel, wiringConfigs, violations) {
if (!patchPanel.wiringConfigId) {
addViolation(
violations,
"patch_panel_config_mismatch",
"active patch panel status must name the selected wiringConfigId",
"$.topology.services.patchPanel.wiringConfigId"
);
return;
}
const selectedConfig = wiringConfigs.find((config) => config.wiringConfigId === patchPanel.wiringConfigId);
if (!selectedConfig) {
addViolation(
violations,
"patch_panel_config_mismatch",
`patch panel references unknown wiring config ${patchPanel.wiringConfigId}`,
"$.topology.services.patchPanel.wiringConfigId"
);
return;
}
const expectedConnections = selectedConfig.connections.map(connectionToPatchPanelConnection).map(getActiveConnectionKey);
const actualConnections = asArray(patchPanel.activeConnections).map(getActiveConnectionKey);
if (!sameStringSet(expectedConnections, actualConnections)) {
addViolation(
violations,
"patch_panel_config_mismatch",
"patch panel activeConnections must match the selected wiring config",
"$.topology.services.patchPanel.activeConnections"
);
}
}
function detectDirectedCycle(connections) {
const graph = new Map();
for (const connection of connections) {
const from = connection.from?.resourceId;
const to = connection.to?.resourceId;
if (!from || !to) {
continue;
}
if (!graph.has(from)) {
graph.set(from, new Set());
}
if (!graph.has(to)) {
graph.set(to, new Set());
}
graph.get(from).add(to);
}
const visiting = new Set();
const visited = new Set();
function visit(node) {
if (visiting.has(node)) {
return true;
}
if (visited.has(node)) {
return false;
}
visiting.add(node);
for (const next of graph.get(node) ?? []) {
if (visit(next)) {
return true;
}
}
visiting.delete(node);
visited.add(node);
return false;
}
return [...graph.keys()].some((node) => visit(node));
}
function validateCycles(wiringConfigs, violations) {
wiringConfigs.forEach((config, index) => {
if (config.status !== ACTIVE) {
return;
}
if (detectDirectedCycle(config.connections ?? [])) {
addViolation(
violations,
"wiring_cycle",
`active wiring config ${config.wiringConfigId} contains a directed resource cycle`,
`$.topology.wiringConfigs[${index}].connections`
);
}
});
}
function getResourceGroupId(config) {
if (typeof config.constraints?.resourceGroupId === "string" && config.constraints.resourceGroupId.length > 0) {
return config.constraints.resourceGroupId;
}
const resourceIds = [];
for (const connection of config.connections ?? []) {
if (connection.from?.resourceId) {
resourceIds.push(connection.from.resourceId);
}
if (connection.to?.resourceId) {
resourceIds.push(connection.to.resourceId);
}
}
return uniqueSorted(resourceIds).join("+");
}
function validateOverrideForActiveGroup(override, groupId, activeConfigs, patchPanel) {
if (!isObject(override)) {
return "patch panel override must be an object";
}
const activeIds = activeConfigs.map((config) => config.wiringConfigId);
const overrideIds = asArray(override.activeWiringConfigIds);
if (override.resourceGroupId !== groupId) {
return `override resourceGroupId must be ${groupId}`;
}
if (!sameStringSet(activeIds, overrideIds)) {
return "override activeWiringConfigIds must exactly list the active configs in the resource group";
}
if (!activeIds.includes(override.selectedWiringConfigId)) {
return "override selectedWiringConfigId must be one of the active configs";
}
if (override.selectedWiringConfigId !== patchPanel.wiringConfigId) {
return "override selectedWiringConfigId must match patch panel wiringConfigId";
}
if (typeof override.reason !== "string" || override.reason.trim().length === 0) {
return "override reason is required";
}
if (patchPanel.serviceId !== PATCH_PANEL_SERVICE_ID || patchPanel.state !== ACTIVE) {
return "override must be reported by an active hwlab-patch-panel status object";
}
if (patchPanel.metadata?.propagation !== PATCH_PANEL_ONLY) {
return "override must preserve patch-panel-only propagation";
}
return null;
}
function validateActiveConfigExclusivity(wiringConfigs, patchPanel, violations) {
const groups = new Map();
wiringConfigs.forEach((config) => {
if (config.status !== ACTIVE) {
return;
}
const groupId = getResourceGroupId(config);
const key = `${config.projectId}:${groupId}`;
if (!groups.has(key)) {
groups.set(key, { groupId, configs: [] });
}
groups.get(key).configs.push(config);
});
for (const { groupId, configs } of groups.values()) {
if (configs.length <= 1) {
continue;
}
const override = patchPanel.metadata?.activeConfigOverride;
if (!override) {
addViolation(
violations,
"active_config_exclusivity",
`resource group ${groupId} has ${configs.length} active wiring configs without a patch-panel override`,
"$.topology.wiringConfigs"
);
continue;
}
const overrideError = validateOverrideForActiveGroup(override, groupId, configs, patchPanel);
if (overrideError) {
addViolation(
violations,
"patch_panel_override_invalid",
overrideError,
"$.topology.services.patchPanel.metadata.activeConfigOverride"
);
}
}
}
function validatePatchPanelOnly(topology, violations) {
if (topology.environment !== "dev") {
addViolation(violations, "topology_shape", "topology validator fixtures must be DEV topologies", "$.topology.environment");
}
if (topology.constraints?.patchPanelIsOnlyCrossDeviceChannel !== true) {
addViolation(
violations,
"patch_panel_only",
"topology must declare patchPanelIsOnlyCrossDeviceChannel",
"$.topology.constraints.patchPanelIsOnlyCrossDeviceChannel"
);
}
if (topology.constraints?.boxSimuLocalLoopbackForCrossDeviceSync !== false) {
addViolation(
violations,
"patch_panel_only",
"topology must reject box simulator local loopback as cross-device sync",
"$.topology.constraints.boxSimuLocalLoopbackForCrossDeviceSync"
);
}
}
function validateTopology(topology) {
const violations = [];
if (!isObject(topology)) {
addViolation(violations, "topology_shape", "fixture topology must be an object");
return violations;
}
validatePatchPanelOnly(topology, violations);
const resources = indexBoxes(topology, violations);
const sessions = validateGateways(topology, resources, violations);
const patchPanel = validatePatchPanel(topology, sessions, violations);
const wiringConfigs = getWiringConfigs(topology, violations);
wiringConfigs.forEach((config, index) => {
validateWiringConfig(config, index, topology, resources, sessions, violations);
});
validatePatchPanelMatchesSelectedConfig(patchPanel, wiringConfigs, violations);
validateCycles(wiringConfigs, violations);
validateActiveConfigExclusivity(wiringConfigs, patchPanel, violations);
return violations;
}
function compareValidation(caseId, expected, violations) {
const actualCodes = uniqueSorted(violations.map((violation) => violation.code));
const expectedCodes = uniqueSorted(expected.violationCodes ?? []);
if (expected.valid === true && actualCodes.length === 0) {
return [];
}
if (expected.valid === false && sameStringSet(actualCodes, expectedCodes)) {
return [];
}
const details = violations
.map((violation) => ` - ${violation.code} at ${violation.path}: ${violation.message}`)
.join("\n");
const expectedSummary = expected.valid
? "valid"
: `invalid with [${expectedCodes.join(", ")}]`;
const actualSummary = actualCodes.length === 0 ? "valid" : `invalid with [${actualCodes.join(", ")}]`;
return [
`${caseId}: expected ${expectedSummary}, got ${actualSummary}`,
details || " - no violations"
];
}
async function loadFixture(filePath) {
const raw = await readFile(filePath, "utf8");
return JSON.parse(raw);
}
const files = (
await Promise.all(getFixtureTargets().map((target) => collectJsonFiles(target)))
).flat();
if (files.length === 0) {
console.error("No topology constraint fixtures found");
process.exit(1);
}
const failures = [];
for (const filePath of files) {
const fixture = await loadFixture(filePath);
const caseId = getCaseId(filePath, fixture);
const expected = getExpectedValidation(fixture);
const violations = validateTopology(getTopology(fixture));
const comparisonFailures = compareValidation(caseId, expected, violations);
if (comparisonFailures.length > 0) {
failures.push(...comparisonFailures);
continue;
}
const label = expected.valid ? "valid" : `expected ${uniqueSorted(expected.violationCodes ?? []).join(", ")}`;
console.log(`ok ${caseId}: ${label}`);
}
if (failures.length > 0) {
console.error(failures.join("\n"));
process.exit(1);
}
console.log(`Topology constraint validation passed (${files.length} fixtures)`);