From f525c522580d854922acedaeadbefa4c6cf81e79 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Fri, 22 May 2026 07:17:33 +0000 Subject: [PATCH] test: add Cloud Web M3 read-only diagnostics contract --- README.md | 6 + docs/dev-acceptance-checklist.json | 5 +- docs/dev-acceptance-matrix.md | 4 +- docs/m5-mvp-e2e.md | 11 ++ docs/operator-runbook.md | 6 + package.json | 1 + scripts/l6-cli-web-smoke.mjs | 4 +- web/hwlab-cloud-web/app.mjs | 23 ++-- web/hwlab-cloud-web/index.html | 22 ++-- web/hwlab-cloud-web/package.json | 1 + web/hwlab-cloud-web/runtime.mjs | 3 + web/hwlab-cloud-web/scripts/build.mjs | 2 +- web/hwlab-cloud-web/scripts/check.mjs | 18 ++- .../scripts/m3-readonly-contract.mjs | 107 ++++++++++++++++++ web/hwlab-cloud-web/styles.css | 4 - 15 files changed, 185 insertions(+), 32 deletions(-) create mode 100644 web/hwlab-cloud-web/scripts/m3-readonly-contract.mjs diff --git a/README.md b/README.md index ae7275f1..c374f572 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/dev-acceptance-checklist.json b/docs/dev-acceptance-checklist.json index f262b100..bb2204a3 100644 --- a/docs/dev-acceptance-checklist.json +++ b/docs/dev-acceptance-checklist.json @@ -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", diff --git a/docs/dev-acceptance-matrix.md b/docs/dev-acceptance-matrix.md index 762e2880..c67e7c89 100644 --- a/docs/dev-acceptance-matrix.md +++ b/docs/dev-acceptance-matrix.md @@ -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` | diff --git a/docs/m5-mvp-e2e.md b/docs/m5-mvp-e2e.md index 23ffbe01..caadbe9c 100644 --- a/docs/m5-mvp-e2e.md +++ b/docs/m5-mvp-e2e.md @@ -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, diff --git a/docs/operator-runbook.md b/docs/operator-runbook.md index 86c05261..60fa3678 100644 --- a/docs/operator-runbook.md +++ b/docs/operator-runbook.md @@ -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 diff --git a/package.json b/package.json index 26c4966d..ef43e2c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/l6-cli-web-smoke.mjs b/scripts/l6-cli-web-smoke.mjs index 2cda0dc4..ede7ce53 100644 --- a/scripts/l6-cli-web-smoke.mjs +++ b/scripts/l6-cli-web-smoke.mjs @@ -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"); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index c77aef44..fa368a65 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -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; } diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 6bcf31c4..0f32e236 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -12,8 +12,8 @@

HWLAB Cloud Web / DEV

-

DEV Control Console

-

Same-origin runtime observation for cloud-api health, methods, topology contracts, operations, agent sessions, and evidence.

+

M3 Diagnostics Console

+

Read-only runtime observation for cloud-api health, M3 topology contracts, route, patch-panel blockers, and evidence summaries.

Live API @@ -25,7 +25,7 @@