import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { gateSummary } from "../gate-summary.mjs"; import { runtime } from "../runtime.mjs"; const PUBLIC_FRONTEND_ENDPOINT = "http://74.48.78.17:16666"; const PUBLIC_API_EDGE_ENDPOINT = "http://74.48.78.17:16667"; const READ_ONLY_RPC_METHODS = Object.freeze([ "system.health", "cloud.adapter.describe", "audit.event.query", "evidence.record.query" ]); const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = path.resolve(rootDir, "../.."); function readText(relativePath) { return fs.readFileSync(path.resolve(repoRoot, relativePath), "utf8"); } function readJson(relativePath) { return JSON.parse(readText(relativePath)); } function uniqueSorted(values) { return [...new Set(values)].sort(); } function rpcCallsFrom(source) { return [...source.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]); } export function runCloudWebM3ReadonlyContract() { const html = readText("web/hwlab-cloud-web/index.html"); const app = readText("web/hwlab-cloud-web/app.mjs"); const styles = readText("web/hwlab-cloud-web/styles.css"); const runtimeSource = readText("web/hwlab-cloud-web/runtime.mjs"); const frontendSource = `${html}\n${app}\n${styles}\n${runtimeSource}`; const acceptance = readJson("docs/dev-acceptance-checklist.json"); assert.equal(acceptance.publicEndpoints.frontend, PUBLIC_FRONTEND_ENDPOINT, "public frontend must stay on :16666"); assert.equal(acceptance.publicEndpoints.api, PUBLIC_API_EDGE_ENDPOINT, "public API must stay on :16667"); assert.equal(acceptance.publicEndpoints.edge, PUBLIC_API_EDGE_ENDPOINT, "public edge must stay on :16667"); assert.equal(runtime.endpoints.frontend, PUBLIC_FRONTEND_ENDPOINT, "web runtime frontend endpoint"); assert.equal(runtime.endpoints.api, PUBLIC_API_EDGE_ENDPOINT, "web runtime API endpoint"); assert.equal(runtime.endpoints.edge, PUBLIC_API_EDGE_ENDPOINT, "web runtime edge endpoint"); assert.equal(runtime.endpoints.dev, PUBLIC_API_EDGE_ENDPOINT, "legacy runtime dev endpoint alias"); assert.equal(gateSummary.endpoint, PUBLIC_API_EDGE_ENDPOINT, "gate summary endpoint"); assert.deepEqual( uniqueSorted(rpcCallsFrom(app)), [...READ_ONLY_RPC_METHODS].sort(), "Cloud Web must call only read-only JSON-RPC diagnostics methods" ); assert.match(app, /fetchJson\("\/v1"\)/u, "Cloud Web must read the public REST index"); assert.match(app, /fetchJson\("\/json-rpc"/u, "Cloud Web must use same-origin JSON-RPC for diagnostics"); assert.match(app, /runtime\.serviceRoute\.join\(" -> "\)/u, "Cloud Web must render the observed route contract"); for (const [pattern, label] of [ [/DEV Control Console/iu, "generic control console label"], [/Operator Commands/iu, "operator command entry point label"], [/safe CLI entry points/iu, "manual command launcher copy"], [/--live --confirm-dev --confirmed-non-production/iu, "live mutation command"], [/hardware\.operation\.request/iu, "direct hardware operation RPC"], [/hardware\.invoke\.shell/iu, "direct hardware shell RPC"], [/audit\.event\.write/iu, "audit write RPC"], [/evidence\.record\.write/iu, "evidence write RPC"], [/\/v1\/rpc\//iu, "direct REST RPC bridge"], [/\/wiring\/apply/iu, "patch-panel apply endpoint"], [/\/wiring\/reload/iu, "patch-panel reload endpoint"], [/method-pill[\s\S]{0,80}write/iu, "write method pill"] ]) { assert.doesNotMatch(frontendSource, pattern, `Cloud Web frontend must not expose ${label}`); } const m3Milestone = gateSummary.milestones.find((item) => item.id === "M3"); assert.equal(m3Milestone?.status, "blocked", "Cloud Web must not present edge-only M3 as DEV-LIVE"); assert.match(m3Milestone.summary, /blocked by hardware loop topology/u, "M3 summary must explain the live blocker"); assert.ok( gateSummary.blockers.some((blocker) => blocker.scope === "m3-hardware-loop-runtime"), "M3 hardware-loop runtime blocker must remain visible" ); assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel", "topology must route through patch-panel"); assert.deepEqual(gateSummary.topology.gateways.map((gateway) => gateway.gatewayId), ["gwsimu_1", "gwsimu_2"], "M3 workbench topology must expose two distinct gateway-simu identities"); assert.deepEqual(gateSummary.topology.boxResources.map((resource) => resource.resourceId), ["res_boxsimu_1", "res_boxsimu_2"], "M3 workbench topology must expose two distinct box-simu identities"); assert.deepEqual(gateSummary.topology.patchPanel.activeConnections, [ { fromResourceId: "res_boxsimu_1", fromPort: "DO1", toResourceId: "res_boxsimu_2", toPort: "DI1" } ], "M3 workbench patch-panel links must show only res_boxsimu_1:DO1 -> res_boxsimu_2:DI1"); assert.doesNotMatch( JSON.stringify(gateSummary.topology), /res_m5-control-relay|res_m5-target-board|out1|reset/u, "M5/reset dry-run wiring must not appear in the default M3 workbench topology" ); assert.ok(gateSummary.health.length >= 1, "health diagnostics must remain visible"); assert.ok(gateSummary.evidenceRecords.every((record) => record.dryRun === true), "checked-in evidence must stay dry-run labeled"); assert.ok( acceptance.m3EvidenceClassification.nonP0.includes("edge-only-diagnostic"), "edge-only diagnostics must stay non-P0 support evidence" ); assert.match( acceptance.m3EvidenceClassification.promotionRule, /Do not classify M3 support or non-P0 evidence as DEV-LIVE/u, "M3 promotion rule must forbid edge-only DEV-LIVE promotion" ); } if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { runCloudWebM3ReadonlyContract(); console.log("Cloud Web M3 read-only diagnostics contract passed"); }