f3ce52bb84
Refs #99 #78 #102
230 lines
8.1 KiB
JavaScript
230 lines
8.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
M3_TRUSTED_CHAIN,
|
|
createHardwareEvidencePanelModel,
|
|
defaultHardwareEvidencePanelData,
|
|
hardwareEvidencePanelSourceLegend,
|
|
renderHardwareEvidencePanel
|
|
} from "../workbench-hardware-panel.mjs";
|
|
|
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const panelPath = path.resolve(rootDir, "workbench-hardware-panel.mjs");
|
|
const panelSource = fs.readFileSync(panelPath, "utf8");
|
|
|
|
const REQUIRED_SOURCE_KINDS = Object.freeze(["SOURCE", "DRY-RUN", "DEV-LIVE", "BLOCKED"]);
|
|
const FORBIDDEN_MUTATION_PATTERNS = Object.freeze([
|
|
/hardware\.operation\.request/u,
|
|
/callRpc\(\s*["']hardware\./u,
|
|
/hardware\.invoke\.shell/u,
|
|
/audit\.event\.write/u,
|
|
/evidence\.record\.write/u,
|
|
/\/wiring\/apply/u,
|
|
/\/wiring\/reload/u,
|
|
/fetch\(/u,
|
|
/XMLHttpRequest/u,
|
|
/sendBeacon/u
|
|
]);
|
|
|
|
export function runWorkbenchHardwarePanelContract() {
|
|
assert.equal(typeof renderHardwareEvidencePanel, "function", "renderer export must exist");
|
|
assert.equal(typeof createHardwareEvidencePanelModel, "function", "model export must exist");
|
|
assert.deepEqual(M3_TRUSTED_CHAIN, {
|
|
fromResourceId: "res_boxsimu_1",
|
|
fromPort: "DO1",
|
|
patchPanelServiceId: "hwlab-patch-panel",
|
|
toResourceId: "res_boxsimu_2",
|
|
toPort: "DI1"
|
|
});
|
|
|
|
const sourceKinds = hardwareEvidencePanelSourceLegend.map((item) => item.sourceKind);
|
|
assert.deepEqual(sourceKinds, REQUIRED_SOURCE_KINDS, "panel must expose SOURCE/DRY-RUN/DEV-LIVE/BLOCKED tags");
|
|
for (const sourceKind of REQUIRED_SOURCE_KINDS) {
|
|
assert.match(panelSource, new RegExp(`["']${sourceKind}["']`, "u"), `${sourceKind} label must be rendered by source`);
|
|
}
|
|
|
|
const blockedModel = createHardwareEvidencePanelModel(defaultHardwareEvidencePanelData);
|
|
assert.equal(blockedModel.trustedLoop.status, "blocked", "default model must stay blocked without DEV-LIVE evidence");
|
|
assert.equal(blockedModel.trustedLoop.tone, "degraded", "default model must render degraded, not green");
|
|
assert.equal(blockedModel.sourceKind, "BLOCKED", "missing DEV-LIVE evidence must force BLOCKED source kind");
|
|
assert.equal(blockedModel.chain.fromResourceId, "res_boxsimu_1");
|
|
assert.equal(blockedModel.chain.fromPort, "DO1");
|
|
assert.equal(blockedModel.chain.patchPanelServiceId, "hwlab-patch-panel");
|
|
assert.equal(blockedModel.chain.toResourceId, "res_boxsimu_2");
|
|
assert.equal(blockedModel.chain.toPort, "DI1");
|
|
assert.match(blockedModel.chain.detail, /BLOCKED/u);
|
|
assert.match(blockedModel.chain.detail, /waiting for DEV-LIVE M3 loop/u);
|
|
|
|
assert.ok(
|
|
blockedModel.statuses.some((item) => item.kind === "BOX-SIMU" && item.id === "res_boxsimu_1"),
|
|
"BOX-SIMU source status must be present"
|
|
);
|
|
assert.ok(
|
|
blockedModel.statuses.some((item) => item.kind === "Gateway-SIMU"),
|
|
"Gateway-SIMU status must be present"
|
|
);
|
|
assert.ok(
|
|
blockedModel.statuses.some((item) => item.kind === "Patch Panel" && item.id === "hwlab-patch-panel"),
|
|
"Patch Panel status must be present"
|
|
);
|
|
|
|
assert.ok(blockedModel.records.operations.length >= 1, "operation summary must render");
|
|
assert.ok(blockedModel.records.audit.length >= 1, "audit summary must render");
|
|
assert.ok(blockedModel.records.evidence.length >= 1, "evidence summary must render");
|
|
assert.ok(
|
|
blockedModel.records.evidence.some((record) => record.sourceKind === "DRY-RUN"),
|
|
"fixture evidence must stay DRY-RUN labeled"
|
|
);
|
|
|
|
assert.ok(blockedModel.controls.length >= 1, "placeholder controls must exist");
|
|
for (const control of blockedModel.controls) {
|
|
assert.equal(control.disabled, true, `${control.id} must be disabled`);
|
|
assert.match(control.reason, /not wired \/ readonly \/ waiting for DEV-LIVE M3 loop/u);
|
|
}
|
|
|
|
const misleadingGreenModel = createHardwareEvidencePanelModel({
|
|
statuses: [
|
|
{
|
|
id: "res_boxsimu_1",
|
|
label: "BOX-SIMU 1",
|
|
kind: "BOX-SIMU",
|
|
status: "healthy",
|
|
sourceKind: "SOURCE"
|
|
}
|
|
],
|
|
chain: {
|
|
...M3_TRUSTED_CHAIN,
|
|
status: "healthy",
|
|
sourceKind: "SOURCE"
|
|
},
|
|
records: {
|
|
operations: [],
|
|
audit: [],
|
|
evidence: []
|
|
},
|
|
controls: []
|
|
});
|
|
assert.equal(misleadingGreenModel.trustedLoop.status, "blocked", "green-looking source data cannot promote M3");
|
|
assert.equal(misleadingGreenModel.chain.status, "blocked", "chain must be blocked without DEV-LIVE evidence");
|
|
assert.notEqual(misleadingGreenModel.statuses[0].status, "healthy", "healthy source-only status must be downgraded");
|
|
|
|
const trustedModel = createHardwareEvidencePanelModel({
|
|
chain: {
|
|
...M3_TRUSTED_CHAIN,
|
|
status: "verified",
|
|
sourceKind: "DEV-LIVE",
|
|
detail: "DEV-LIVE M3 loop observed and attached."
|
|
},
|
|
records: {
|
|
operations: [
|
|
{
|
|
id: "op_m3_dev_live_readonly_summary",
|
|
status: "succeeded",
|
|
sourceKind: "DEV-LIVE",
|
|
summary: "Existing trusted operation summary only."
|
|
}
|
|
],
|
|
audit: [
|
|
{
|
|
id: "aud_m3_dev_live_readonly_summary",
|
|
status: "recorded",
|
|
sourceKind: "DEV-LIVE",
|
|
summary: "Existing trusted audit summary only."
|
|
}
|
|
],
|
|
evidence: [
|
|
{
|
|
id: "evi_m3_dev_live_loop",
|
|
status: "verified",
|
|
sourceKind: "DEV-LIVE",
|
|
summary: "Trusted M3 loop evidence is already recorded."
|
|
}
|
|
]
|
|
},
|
|
controls: [
|
|
{
|
|
id: "dev-live-safe-placeholder",
|
|
label: "Readonly",
|
|
disabled: false,
|
|
reason: ""
|
|
}
|
|
]
|
|
});
|
|
assert.equal(trustedModel.trustedLoop.status, "verified", "DEV-LIVE evidence plus exact chain can promote M3");
|
|
assert.equal(trustedModel.sourceKind, "DEV-LIVE");
|
|
assert.equal(trustedModel.controls[0].disabled, true, "controls must remain disabled even with DEV-LIVE evidence");
|
|
assert.match(trustedModel.controls[0].reason, /not wired \/ readonly \/ waiting for DEV-LIVE M3 loop/u);
|
|
|
|
const dom = createFakeDom();
|
|
const rendered = renderHardwareEvidencePanel(dom.container, defaultHardwareEvidencePanelData);
|
|
assert.equal(rendered.model.sourceKind, "BLOCKED", "renderer must use guarded model");
|
|
assert.equal(dom.container.children.length, 1, "renderer must replace the container with one panel root");
|
|
assert.equal(dom.container.children[0].dataset.sourceKind, "BLOCKED", "rendered panel must expose sourceKind");
|
|
const buttons = dom.container.querySelectorAll("button");
|
|
assert.equal(buttons.length, blockedModel.controls.length, "renderer must include all placeholder controls");
|
|
for (const button of buttons) {
|
|
assert.equal(button.disabled, true, "rendered placeholder controls must be disabled");
|
|
}
|
|
|
|
for (const pattern of FORBIDDEN_MUTATION_PATTERNS) {
|
|
assert.doesNotMatch(panelSource, pattern, `panel must not include mutation or network path ${pattern}`);
|
|
}
|
|
}
|
|
|
|
function createFakeDom() {
|
|
const document = {
|
|
createElement(tagName) {
|
|
return new FakeElement(tagName, document);
|
|
}
|
|
};
|
|
return {
|
|
document,
|
|
container: new FakeElement("div", document)
|
|
};
|
|
}
|
|
|
|
class FakeElement {
|
|
constructor(tagName, ownerDocument) {
|
|
this.tagName = tagName.toUpperCase();
|
|
this.ownerDocument = ownerDocument;
|
|
this.children = [];
|
|
this.dataset = {};
|
|
this.attributes = {};
|
|
this.className = "";
|
|
this.disabled = false;
|
|
this.textContent = "";
|
|
this.type = "";
|
|
}
|
|
|
|
append(...children) {
|
|
this.children.push(...children);
|
|
}
|
|
|
|
replaceChildren(...children) {
|
|
this.children = children;
|
|
}
|
|
|
|
setAttribute(name, value) {
|
|
this.attributes[name] = String(value);
|
|
}
|
|
|
|
querySelectorAll(tagName) {
|
|
const normalizedTagName = tagName.toUpperCase();
|
|
const matches = [];
|
|
for (const child of this.children) {
|
|
if (child.tagName === normalizedTagName) {
|
|
matches.push(child);
|
|
}
|
|
matches.push(...child.querySelectorAll(tagName));
|
|
}
|
|
return matches;
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
runWorkbenchHardwarePanelContract();
|
|
console.log("Workbench hardware evidence panel contract passed");
|
|
}
|