241 lines
10 KiB
JavaScript
241 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
|
|
import { runDevCloudWorkbenchStaticSmoke } from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
|
import { gateSummary } from "../gate-summary.mjs";
|
|
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
|
|
import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-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",
|
|
"runtime.mjs",
|
|
"workbench-hardware-panel.mjs",
|
|
"help.md",
|
|
"third_party/marked/marked.esm.js",
|
|
"third_party/marked/LICENSE"
|
|
];
|
|
|
|
for (const file of requiredFiles) {
|
|
const filePath = path.resolve(rootDir, file);
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`missing web asset: ${file}`);
|
|
}
|
|
}
|
|
|
|
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
|
const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8");
|
|
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
|
|
const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8");
|
|
const buildScript = fs.readFileSync(path.resolve(rootDir, "scripts/build.mjs"), "utf8");
|
|
const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8");
|
|
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
|
|
const frontendSource = `${html}\n${app}\n${artifactPublisher}`;
|
|
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
|
|
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
|
|
|
|
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
|
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
|
|
assert.equal(workbenchSmoke.devLive, false);
|
|
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
|
|
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
|
|
|
|
assert.match(html, /HWLAB 云工作台/);
|
|
for (const chineseWorkbenchText of [
|
|
"硬件资源",
|
|
"Agent 对话",
|
|
"工作清单",
|
|
"可信记录",
|
|
"内部复核",
|
|
"使用说明",
|
|
"允许的只读 RPC"
|
|
]) {
|
|
assert.match(`${html}\n${app}`, new RegExp(chineseWorkbenchText));
|
|
}
|
|
for (const legacyEnglishTitle of [
|
|
/Agent Conversation \/ Trace Workspace/,
|
|
/Resource Tree/,
|
|
/Current Blockers/,
|
|
/Control Drafts/,
|
|
/Trusted Records/,
|
|
/Same-Origin Read Probe/,
|
|
/Allowed Read RPC/,
|
|
/Open Blockers/,
|
|
/Light Validation/,
|
|
/DEV Health Contract/,
|
|
/Secondary Entry/,
|
|
/Runtime Surface/
|
|
]) {
|
|
assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle);
|
|
}
|
|
assert.match(html, /硬件状态:BOX-SIMU \/ Gateway-SIMU \/ hwlab-patch-panel/);
|
|
assert.match(html, /Gate \/ 诊断 \/ 验收/);
|
|
for (const workbenchElement of [
|
|
"activity-rail",
|
|
"explorer",
|
|
"resource-tree",
|
|
"conversation-list",
|
|
"task-list",
|
|
"right-sidebar",
|
|
"command-form",
|
|
"hardware-list",
|
|
"control-list",
|
|
"wiring-body",
|
|
"records-list",
|
|
"diagnostics-list"
|
|
]) {
|
|
assert.match(html, new RegExp(workbenchElement));
|
|
}
|
|
for (const viewId of ["workspace", "gate"]) {
|
|
assert.match(html, new RegExp(`data-view="${viewId}"`));
|
|
}
|
|
assert.match(html, /data-route="help"/);
|
|
assert.match(html, /data-view="help"/);
|
|
assert.match(html, /<section class="view help-view"[\s\S]*?hidden/);
|
|
assert.match(html, /id="help-content"/);
|
|
assert.match(html, /data-help-route-policy="non-default-internal-help"/);
|
|
assert.match(app, /from "\.\/third_party\/marked\/marked\.esm\.js"/);
|
|
assert.match(app, /marked\.parse\(/);
|
|
assert.doesNotMatch(app, /function\s+parseMarkdown|markedRegex|markdownRegex/);
|
|
assert.match(app, /fetch\("\.\/help\.md"/);
|
|
assert.match(app, /帮助内容加载失败/);
|
|
assert.match(app, /return "workspace";/);
|
|
assert.match(styles, /\.help-view\s*{[^}]*overflow:\s*hidden;/s);
|
|
assert.match(styles, /\.help-panel\s*{[^}]*overflow:\s*hidden;/s);
|
|
assert.match(styles, /\.help-content\s*{[^}]*overflow:\s*auto;/s);
|
|
assert.match(helpMarkdown, /^# 云工作台内部使用说明/m);
|
|
for (const helpTerm of [
|
|
"左侧资源与功能导航",
|
|
"Agent 对话与工作清单",
|
|
"完整中文标签",
|
|
"内部复核",
|
|
"BOX-SIMU",
|
|
"Gateway-SIMU",
|
|
"hwlab-patch-panel",
|
|
"控制",
|
|
"接线",
|
|
"可信记录",
|
|
"诊断",
|
|
"Gate",
|
|
"SOURCE",
|
|
"LOCAL",
|
|
"DRY-RUN",
|
|
"DEV-LIVE",
|
|
"BLOCKED",
|
|
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
|
"same-origin `/v1`",
|
|
"只读 `/json-rpc`",
|
|
"`/health/live`",
|
|
"`:16666`",
|
|
"`:16667`"
|
|
]) {
|
|
assert.match(helpMarkdown, new RegExp(escapeRegExp(helpTerm)), `missing help term: ${helpTerm}`);
|
|
}
|
|
const workspaceHtml = sectionSource(html, "workspace");
|
|
const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`;
|
|
for (const backendTerm of [/Gate/u, /诊断/u, /验收/u, /BLOCKED/u, /M0-M5/u, /执行轨迹/u]) {
|
|
assert.doesNotMatch(visibleShellHtml, backendTerm, `default workbench must not expose backend term ${backendTerm}`);
|
|
}
|
|
for (const fullNavLabel of ["工作台", "内部复核", "使用说明", "资源树"]) {
|
|
assert.match(html, new RegExp(`>${escapeRegExp(fullNavLabel)}<`), `missing full nav label ${fullNavLabel}`);
|
|
}
|
|
for (const oneCharNavLabel of [">台<", ">证<", ">诊<", ">帮<"]) {
|
|
assert.doesNotMatch(html, new RegExp(escapeRegExp(oneCharNavLabel)), `legacy one-character nav label must be removed: ${oneCharNavLabel}`);
|
|
}
|
|
assert.doesNotMatch(html, /data-side-tab-jump="records"/u, "trusted records must not remain a left-side standalone shortcut");
|
|
for (const wiringHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) {
|
|
assert.match(html, new RegExp(`<th>${escapeRegExp(wiringHeader)}<\\/th>`), `missing wiring column ${wiringHeader}`);
|
|
}
|
|
assert.match(html, /id="unblock-list"/);
|
|
assert.match(app, /DB live readiness/);
|
|
assert.match(app, /res_boxsimu_1:DO1 -> res_boxsimu_2:DI1/);
|
|
assert.match(app, /patch-panel DO1 -> DI1/);
|
|
assert.match(buildScript, /"help\.md"/);
|
|
assert.match(buildScript, /"third_party\/marked\/marked\.esm\.js"/);
|
|
assert.match(buildScript, /"third_party\/marked\/LICENSE"/);
|
|
assert.match(markedLicense, /MarkedJS/);
|
|
assert.match(markedLicense, /Permission is hereby granted, free of charge/);
|
|
assert.match(html, /live-status/);
|
|
assert.match(html, /method-list/);
|
|
assert.match(html, /quick-actions/);
|
|
assert.match(html, /编写任务/);
|
|
assert.match(html, /查看接线/);
|
|
assert.match(html, /查看状态/);
|
|
assert.doesNotMatch(html, /M3 Diagnostics Console/);
|
|
assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;/s);
|
|
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
|
|
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s);
|
|
assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
|
|
assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
|
|
assert.match(app, /fetchJson\("\/v1"\)/);
|
|
assert.match(app, /fetchJson\("\/health\/live"\)/);
|
|
assert.match(app, /callRpc\("system\.health"\)/);
|
|
assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/);
|
|
assert.match(app, /callRpc\("audit\.event\.query"/);
|
|
assert.match(app, /callRpc\("evidence\.record\.query"/);
|
|
assert.match(artifactPublisher, /HWLAB_API_BASE_URL/);
|
|
assert.match(artifactPublisher, /readOnlyRpcMethods/);
|
|
assert.match(artifactPublisher, /requestUpstream/);
|
|
assert.match(artifactPublisher, /from "node:http"/);
|
|
assert.doesNotMatch(artifactPublisher, /await fetch\(target/);
|
|
assert.match(artifactPublisher, /"system\.health"/);
|
|
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
|
|
assert.match(artifactPublisher, /"audit\.event\.query"/);
|
|
assert.match(artifactPublisher, /"evidence\.record\.query"/);
|
|
assert.match(artifactPublisher, /readonly_rpc_required/);
|
|
assert.match(artifactPublisher, /cloud_api_proxy_failed/);
|
|
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);
|
|
assert.equal(gateSummary.endpoint, sourceSummary.endpoint);
|
|
assert.equal(gateSummary.gateStatus, sourceSummary.gateStatus);
|
|
assert.equal(gateSummary.milestones.length, 6);
|
|
assert.deepEqual(
|
|
gateSummary.milestones.map((item) => item.id),
|
|
["M0", "M1", "M2", "M3", "M4", "M5"]
|
|
);
|
|
assert.equal(gateSummary.artifactCount, 13);
|
|
assert.equal(gateSummary.healthCount, 14);
|
|
assert.equal(gateSummary.gatewaySessionCount, 2);
|
|
assert.equal(gateSummary.boxResourceCount, 2);
|
|
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);
|
|
assert.equal(gateSummary.safety.allowNetwork, false);
|
|
|
|
runCloudWebM3ReadonlyContract();
|
|
runWorkbenchHardwarePanelContract();
|
|
|
|
console.log("hwlab-cloud-web check ok: workbench shell, hardware evidence panel, and read-only diagnostics contract are present");
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function sectionSource(source, viewId) {
|
|
const start = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
|
|
if (start === -1) return "";
|
|
const next = source.slice(start + 1).search(/<section\b[^>]*\bdata-view=["'][^"']+["'][^>]*>/u);
|
|
return next === -1 ? source.slice(start) : source.slice(start, start + 1 + next);
|
|
}
|
|
|
|
function beforeSection(source, viewId) {
|
|
const index = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
|
|
return index === -1 ? source : source.slice(0, index);
|
|
}
|