test: add Cloud Web M3 read-only diagnostics contract
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user