test: add Cloud Web M3 read-only diagnostics contract

This commit is contained in:
HWLAB Code Queue
2026-05-22 07:17:33 +00:00
parent 8e89409dda
commit f525c52258
15 changed files with 185 additions and 32 deletions
+6
View File
@@ -83,11 +83,17 @@ The external MVP acceptance surface is DEV-only and report-driven:
```sh
npm run l6:smoke
npm run web:m3-readonly
npm run web:check
npm run cli:dry-run
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
`npm run web:m3-readonly` is the Cloud Web #59 guardrail: it checks that the
browser entry remains on public `:16666`, API/edge diagnostics remain on public
`:16667`, the page only calls read-only diagnostics RPCs, and M3 stays blocked
unless the real `DO1 -> hwlab-patch-panel -> DI1` DEV-LIVE evidence exists.
The CLI command defaults to a JSON dry-run plan sourced from
`reports/dev-gate/dev-mvp-gate-report.json` and
`fixtures/mvp/m5-e2e/dry-run-plan.json`. A live DEV attempt requires
+3 -2
View File
@@ -232,6 +232,7 @@
"serviceId": "hwlab-cloud-web",
"probe": {
"commands": [
"npm run web:m3-readonly",
"npm run web:check",
"npm run web:build"
]
@@ -243,8 +244,8 @@
"deployEnv",
"healthTimestamp"
],
"success": "Static assets exist, build succeeds, and runtime uses the fixed DEV endpoint.",
"failure": "Endpoint drift, direct hardware control path, or missing asset."
"success": "Static assets exist, build succeeds, runtime uses public frontend :16666 and API/edge :16667, and Cloud Web exposes only read-only M3 diagnostics for topology, route, health, blockers, and evidence summary.",
"failure": "Endpoint drift, direct hardware write/control path, edge-only diagnostic promoted to M3 DEV-LIVE, or missing asset."
},
{
"component": "Agent manager",
+2 -2
View File
@@ -72,7 +72,7 @@ provide a current health timestamp. Existing skeleton services may expose
| frp tunnel | `hwlab-tunnel-client` | Tunnel session/status observation | Tunnel for DEV route is established to D601 router and identifies commit/image | Tunnel down, wrong target, ambiguous service identity |
| D601 router | `hwlab-router` | `hwlab-dev/hwlab-router` health/status | Router is live, environment is `dev`, route target is cloud API/web or runtime service | Namespace mismatch, route missing, stale health timestamp |
| Cloud API | `hwlab-cloud-api` | `GET /health`, `GET /live`, optional JSON-RPC `/rpc` probe | Returns service ID, `dev`, healthy/live status, redacted DB env readiness, and accepts a harmless protocol probe | HTTP error, unknown service ID, wrong environment, invalid JSON-RPC envelope, missing DB env readiness |
| Cloud Web | `hwlab-cloud-web` | `GET http://74.48.78.17:16666/` plus static asset check and configured API endpoint | Public browser entry serves the web bundle on `:16666`; API calls target `http://74.48.78.17:16667` or the internal k3s API service, and do not call hardware directly | Endpoint drift, public web served from `:6667`, direct hardware control path, missing asset |
| Cloud Web | `hwlab-cloud-web` | `GET http://74.48.78.17:16666/` plus `npm run web:m3-readonly` / static asset check | Public browser entry serves the web bundle on `:16666`; API/edge diagnostics use `http://74.48.78.17:16667` or same-origin/internal k3s API service; the visible surface is M3 diagnostics for topology, route, health, blockers, and evidence summary only | Endpoint drift, public web served from `:6667`, direct write/control endpoint text or calls, edge-only evidence presented as M3 `DEV-LIVE`, missing asset |
| Agent manager | `hwlab-agent-mgr` | Agent scheduling health/status | Reports live manager, commit/image, and DEV environment; no worker leak after smoke | Scheduler unavailable, wrong environment, unbounded worker session |
| Agent worker | `hwlab-agent-worker` | Worker health/status during dry-run or scoped smoke | Worker session is scoped to DEV project and emits trace/audit identifiers | Missing session identity, unsafe mutation, cleanup failure |
| Agent skills | `hwlab-agent-skills` | Skill bundle version/status | Skill artifact is traceable to commit/build source and compatible with worker | Unversioned skill bundle, missing build source |
@@ -97,7 +97,7 @@ listed above.
| 4 | Observe master edge proxy route. | Edge route maps DEV endpoint to `frp` and records artifact identity. | Missing route, stale route, missing artifact identity. | `network_blocker` |
| 5 | Observe `frp` tunnel. | Tunnel links master edge to D601 router for DEV. | Tunnel down or target mismatch. | `network_blocker` |
| 6 | Observe D601 `hwlab-dev/hwlab-router`. | Router is live and forwards only to HWLAB DEV services. | Namespace mismatch or route bypass. | `runtime_blocker` |
| 7 | Check cloud API/web surface. | Cloud API health is valid on public `:16667`; web assets are served on public `:16666` and point API traffic at `:16667` or the internal k3s API service. | Bad health, endpoint drift, direct hardware control from web. | `runtime_blocker` |
| 7 | Check cloud API/web surface. | Cloud API health is valid on public `:16667`; web assets are served on public `:16666`; Cloud Web exposes only M3 diagnostics for route, health, topology, blockers, and evidence summary; `npm run web:m3-readonly` passes. | Bad health, endpoint drift, direct hardware write/control surface from web, or edge-only evidence promoted to M3 `DEV-LIVE`. | `runtime_blocker` |
| 8 | Check gateway, simulator, box simulator, and patch-panel contracts. | Health/status JSON is parseable and topology keeps routing under patch-panel ownership; for M3 live, a traced `DO1 -> patch-panel -> DI1` operation exists. | Missing health, invalid topology, bypass path, or any attempt to promote support/fixture evidence to M3 live. | `runtime_blocker` |
| 9 | Check agent manager, worker, and skills contracts. | Agent artifacts are traceable; dry-run/scoped smoke emits session, trace, audit, and cleanup evidence. | Missing traceability, unsafe mutation, cleanup leak. | `agent_blocker` |
| 10 | Verify evidence and artifact observability. | Every accepted artifact has service ID, commit, image/tag/digest or reason, build source, env, and health timestamp. | Missing required observability field. | `observability_blocker` |
+11
View File
@@ -19,6 +19,17 @@ The L6 external acceptance CLI wraps the same report and dry-run plan:
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
Cloud Web #59 adds a separate read-only diagnostics contract:
```sh
node web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs
```
This contract is source-only. It verifies the public frontend/API endpoint
split, the read-only JSON-RPC calls used by Cloud Web, the visible topology,
route, health, and M3 evidence summary, and the rule that edge-only
observations cannot be displayed as M3 `DEV-LIVE`.
This command defaults to a JSON dry-run plan. `--dry-run` is accepted for
backward compatibility, but it is no longer required for safety. The output
must identify the M0-M5 report source, artifact and DEV health contract counts,
+6
View File
@@ -128,11 +128,17 @@ Safe source and dry-run checks:
```sh
npm run l6:smoke
npm run web:m3-readonly
npm run web:check
npm run cli:dry-run
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
```
For Cloud Web #59, `npm run web:m3-readonly` is the review contract. It keeps
the browser endpoint at public `:16666`, API/edge diagnostics at public
`:16667`, allows only read-only topology/route/health/evidence diagnostics,
and fails if edge-only observations are presented as M3 `DEV-LIVE`.
The default CLI MVP command is a JSON dry-run plan even when `--dry-run` is not
provided. It must include the DEV endpoint, artifact/health counts, two
gateway sessions, two box resources, the patch-panel status, agent and worker
+1
View File
@@ -20,6 +20,7 @@
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
"l6:smoke": "node scripts/l6-cli-web-smoke.mjs",
"web:summary": "node scripts/export-web-gate-summary.mjs",
"web:m3-readonly": "node web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs",
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs",
"artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
+3 -1
View File
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
import { gateSummary } from "../web/hwlab-cloud-web/gate-summary.mjs";
import { runCloudWebM3ReadonlyContract } from "../web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -80,5 +81,6 @@ assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.evidenceRecords.length, 2);
runCloudWebM3ReadonlyContract();
console.log("L6 CLI/Web smoke passed: CLI and Web read the M0-M5 gate report");
console.log("L6 CLI/Web smoke passed: CLI and Web read the M0-M5 gate report and Cloud Web stays read-only for M3 diagnostics");
+16 -7
View File
@@ -88,10 +88,11 @@ function renderStaticConsole() {
function renderLivePending() {
appendMetricGrid(el.overviewMetrics, [
["API origin", "same-origin /v1 + /json-rpc"],
["Frontend", runtime.endpoints.frontend, "warn"],
["Configured DEV", runtime.endpoints.dev, "warn"],
["Health", "probing", "warn"],
["Runtime", "probing", "warn"],
["DB", "probing", "warn"],
["M3", "probing", "warn"],
["Gate", gateSummary.gateStatus, gateSummary.gateStatus]
]);
replaceChildren(el.methodGrid, emptyNotice("Waiting for live cloud-api method list."));
@@ -231,10 +232,11 @@ function renderLiveConsole(live) {
appendMetricGrid(el.overviewMetrics, [
["API origin", "same-origin /v1 + /json-rpc"],
["Frontend", runtime.endpoints.frontend, "warn"],
["Configured DEV", runtime.endpoints.dev, "warn"],
["Health", live.health.ok ? live.health.data.status : "degraded", live.health.ok ? live.health.data.status : "degraded"],
["Runtime", runtimeSummary?.status ?? "unavailable", runtimeSummary?.status ?? "degraded"],
["DB", live.health.ok ? live.health.data.db?.status : "unavailable", live.health.ok ? live.health.data.db?.status : "degraded"],
["M3", m3Status(), m3Status()],
["Observed", reachable ? shortTime(live.observedAt) : "not available", reachable ? "pass" : "degraded"]
]);
@@ -254,11 +256,12 @@ function renderMethods(methods, live) {
}
const source = live.restIndex.ok && Array.isArray(live.restIndex.data.methods) ? "GET /v1" : "cloud.adapter.describe";
el.methodDetail.textContent = `Live method list from ${source}; ${methods.length} methods returned.`;
el.methodDetail.textContent = `Live method list from ${source}; showing read-only diagnostics methods from ${methods.length} returned methods.`;
const fragment = document.createDocumentFragment();
for (const method of methods) {
if (!rpcReadMethods.includes(method)) continue;
const item = document.createElement("span");
item.className = `method-pill ${rpcReadMethods.includes(method) ? "read" : "write"}`;
item.className = "method-pill read";
item.textContent = method;
fragment.append(item);
}
@@ -445,11 +448,12 @@ function renderEvidenceContract() {
function renderGateDiagnostics() {
appendMetricGrid(el.metaGrid, [
["Environment", gateSummary.environment],
["Frontend", runtime.endpoints.frontend],
["Endpoint", gateSummary.endpoint],
["Namespace", gateSummary.namespace],
["Report commit", gateSummary.reportCommitId],
["Gate report", gateSummary.generatedFrom.gateReport],
["M5 plan", gateSummary.generatedFrom.m5Plan]
["Route", runtime.serviceRoute.join(" -> ")],
["Gate report", gateSummary.generatedFrom.gateReport]
]);
el.milestoneGrid.replaceChildren();
@@ -493,14 +497,19 @@ function renderGateDiagnostics() {
el.commandList.replaceChildren();
for (const command of [
"node web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs",
"node web/hwlab-cloud-web/scripts/check.mjs",
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production",
"node scripts/m5-mvp-e2e-dry-run.mjs"
]) {
appendText(el.commandList, "li", command, "mono");
}
}
function m3Status() {
return gateSummary.milestones.find((milestone) => milestone.id === "M3")?.status ?? "unknown";
}
function runtimeSummaryFrom(live) {
return live.health.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null;
}
+11 -11
View File
@@ -12,8 +12,8 @@
<header class="masthead">
<div class="identity">
<p class="eyebrow">HWLAB Cloud Web / DEV</p>
<h1>DEV Control Console</h1>
<p class="lede">Same-origin runtime observation for cloud-api health, methods, topology contracts, operations, agent sessions, and evidence.</p>
<h1>M3 Diagnostics Console</h1>
<p class="lede">Read-only runtime observation for cloud-api health, M3 topology contracts, route, patch-panel blockers, and evidence summaries.</p>
</div>
<div class="live-card" aria-live="polite">
<span class="label">Live API</span>
@@ -25,7 +25,7 @@
<nav class="module-nav" aria-label="HWLAB console modules">
<a href="#overview" data-route="overview">Overview</a>
<a href="#topology" data-route="topology">Topology / Patch Panel</a>
<a href="#hardware" data-route="hardware">Hardware Operations</a>
<a href="#hardware" data-route="hardware">M3 Evidence</a>
<a href="#agents" data-route="agents">Agent Sessions</a>
<a href="#evidence" data-route="evidence">Evidence / Audit</a>
<a href="#gate" data-route="gate">Gate / Diagnostics</a>
@@ -35,9 +35,9 @@
<div class="section-head">
<div>
<p class="eyebrow">Overview</p>
<h2 id="overview-title">Runtime Surface</h2>
<h2 id="overview-title">Read-Only Surface</h2>
</div>
<p>Live rows are populated only from same-origin API responses. Checked-in gate records are labeled separately.</p>
<p>Live rows are populated only from same-origin read APIs. Checked-in gate records are labeled separately.</p>
</div>
<div class="metrics six" id="overview-metrics"></div>
<div class="split">
@@ -101,15 +101,15 @@
<section class="view" id="hardware" data-view="hardware" aria-labelledby="hardware-title" hidden>
<div class="section-head">
<div>
<p class="eyebrow">Hardware Operations</p>
<h2 id="hardware-title">Operation Ledger</h2>
<p class="eyebrow">M3 Evidence</p>
<h2 id="hardware-title">M3 Evidence Ledger</h2>
</div>
<p>The console does not issue mutating hardware requests. It shows live operation counts plus checked-in operation evidence.</p>
</div>
<div class="metrics four" id="hardware-live-metrics"></div>
<section class="panel">
<div class="panel-head">
<h3>Checked-In Operations</h3>
<h3>Checked-In Evidence</h3>
<p>Fixture-bound M5 dry-run operations; not live dispatch proof.</p>
</div>
<div class="table-wrap">
@@ -231,7 +231,7 @@
<p class="eyebrow">Gate / Diagnostics</p>
<h2 id="gate-title">DEV MVP Gate Report</h2>
</div>
<p>Original Gate data remains available here for diagnostics and acceptance review.</p>
<p>Gate data remains available here for read-only diagnostics and acceptance review.</p>
</div>
<section class="panel compact">
<div class="panel-head">
@@ -277,8 +277,8 @@
</section>
<section class="panel compact">
<div class="panel-head">
<h3>Operator Commands</h3>
<p>Safe CLI entry points for manual acceptance.</p>
<h3>Validation Commands</h3>
<p>Read-only local checks for acceptance review.</p>
</div>
<ol class="commands" id="command-list"></ol>
</section>
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
"check": "node scripts/check.mjs",
"build": "node scripts/build.mjs"
}
+3
View File
@@ -1,5 +1,8 @@
export const runtime = {
endpoints: {
frontend: "http://74.48.78.17:16666",
api: "http://74.48.78.17:16667",
edge: "http://74.48.78.17:16667",
dev: "http://74.48.78.17:16667"
},
serviceRoute: [
+1 -1
View File
@@ -5,7 +5,7 @@ const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".
const distDir = path.resolve(rootDir, "dist");
fs.mkdirSync(distDir, { recursive: true });
for (const file of ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"]) {
for (const file of ["index.html", "styles.css", "app.mjs", "gate-summary.mjs", "runtime.mjs"]) {
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
}
+14 -4
View File
@@ -4,10 +4,11 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
import { gateSummary } from "../gate-summary.mjs";
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(rootDir, "../..");
const requiredFiles = ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"];
const requiredFiles = ["index.html", "styles.css", "app.mjs", "gate-summary.mjs", "runtime.mjs"];
for (const file of requiredFiles) {
const filePath = path.resolve(rootDir, file);
@@ -21,11 +22,12 @@ const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
const frontendSource = `${html}\n${app}`;
assert.match(html, /HWLAB DEV Console/);
assert.match(html, /DEV Control Console/);
assert.match(html, /M3 Diagnostics Console/);
assert.match(html, /Read-only runtime observation/);
for (const moduleName of [
"Overview",
"Topology / Patch Panel",
"Hardware Operations",
"M3 Evidence",
"Agent Sessions",
"Evidence / Audit",
"Gate / Diagnostics"
@@ -47,7 +49,13 @@ assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/);
assert.match(app, /callRpc\("audit\.event\.query"/);
assert.match(app, /callRpc\("evidence\.record\.query"/);
assert.match(app, /degraded/);
assert.match(app, /runtime\.serviceRoute\.join\(" -> "\)/);
assert.doesNotMatch(app, /callRpc\("hardware\./);
assert.doesNotMatch(app, /hardware\.operation\.request/);
assert.doesNotMatch(app, /hardware\.invoke\.shell/);
assert.doesNotMatch(app, /audit\.event\.write/);
assert.doesNotMatch(app, /evidence\.record\.write/);
assert.doesNotMatch(frontendSource, /--live --confirm-dev --confirmed-non-production/);
assert.doesNotMatch(frontendSource, /74\.48\.78\.17:666[67]\b|:666[67]\b/);
const sourceSummary = loadMvpGateSummary(repoRoot);
@@ -68,4 +76,6 @@ assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.evidenceRecords.length, 2);
assert.equal(gateSummary.safety.allowNetwork, false);
console.log("hwlab-cloud-web check ok: console modules and gate summary are present");
runCloudWebM3ReadonlyContract();
console.log("hwlab-cloud-web check ok: read-only M3 diagnostics contract is present");
@@ -0,0 +1,107 @@
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.ok(Array.isArray(gateSummary.topology.patchPanel.activeConnections), "patch-panel links must be visible");
assert.ok(gateSummary.topology.gateways.length >= 1, "topology must expose gateway records");
assert.ok(gateSummary.topology.boxResources.length >= 1, "topology must expose box resource records");
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");
}
-4
View File
@@ -279,10 +279,6 @@ h3 {
border-color: rgba(125, 207, 196, 0.45);
}
.method-pill.write {
border-color: rgba(232, 191, 80, 0.36);
}
.milestones {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));