Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/check.mjs
T
2026-05-23 04:47:07 +00:00

518 lines
26 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 {
runDevCloudWorkbenchLocalAgentFixtureSmoke,
runDevCloudWorkbenchMobileSmoke,
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 distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-contract.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 requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
"接线/operation",
"audit 记录",
"evidence 证据",
"trace/messageId",
"audit.event.query + SOURCE 回退",
"evidence.record.query + SOURCE 回退",
"conversationId、traceId 和 messageId",
"provider/model/trace/conversation",
"失败原因=",
"runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。",
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
"当前显示 SOURCE 回退,不能冒充 DEV-LIVE"
]);
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract");
const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states");
const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke();
const mobileOuterScrollSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-outer-scroll-lock");
const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-help-markdown-route");
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
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(outerScrollSmokeCheck?.status, "pass", "desktop workbench shell must lock outer page scrolling");
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct");
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
assert.equal(mobileWorkbenchSmoke.devLive, false);
assert.equal(mobileOuterScrollSmokeCheck?.status, "pass", "mobile smoke must prove outer page scroll remains locked");
assert.equal(mobileHelpSmokeCheck?.status, "pass", "mobile help route must remain non-default Markdown content");
assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFixtureSmoke.blockers, null, 2));
assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(localAgentFixtureSmoke.devLive, false);
if (mobileWorkbenchSmoke.status === "pass") {
assert.equal(mobileWorkbenchSmoke.viewport.width, 390);
assert.equal(mobileWorkbenchSmoke.viewport.height, 844);
assert.equal(mobileWorkbenchSmoke.checks.every((check) => check.status === "pass"), true);
}
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, /id="hardware-source-status"/);
assert.match(app, /function renderHardwareStatus/);
assert.match(app, /function hardwareGroup/);
assert.match(app, /function hardwareRow/);
assert.match(app, /function trustedM3Link/);
assert.match(app, /function hasTrustedM3LiveEvidence/);
assert.match(app, /function hasTrustedPatchPanelLiveReport/);
assert.match(app, /function linkStatusLabel/);
assert.match(app, /function normalizePort/);
assert.match(app, /LIVE_M3_ID_FIELDS/);
assert.match(app, /patchPanelLiveReport/);
assert.match(app, /runtimeCountsSourceKind = "SOURCE"/);
assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u);
assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u);
assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/);
assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/);
assert.match(app, /function renderRecords/);
assert.match(app, /function codeAgentRecordCards/);
assert.match(app, /function operationRecordCards/);
assert.match(app, /function auditRecordCards/);
assert.match(app, /function evidenceRecordCards/);
assert.match(app, /function auditCard/);
assert.match(app, /function evidenceCard/);
assert.match(app, /function liveRecordTone/);
assert.match(app, /function liveRecordDurabilityBlocker/);
assert.match(app, /function isDurableRuntimeReady/);
assert.match(app, /runtimeSummary\?\.durable !== true/);
assert.match(app, /summary\.liveRuntimeEvidence !== true/);
assert.match(app, /auditCard\(event, liveRecordTone\(event, live\), liveRecordDurabilityBlocker\(live\)\)/);
assert.match(app, /evidenceCard\(record, liveRecordTone\(record, live\), liveRecordDurabilityBlocker\(live\)\)/);
assert.match(app, /function recordGroup/);
assert.match(app, /function recordField/);
assert.match(app, /function sourceKindLabel/);
assert.match(app, /function liveFailureCard/);
assert.match(app, /state\.liveSurface/);
assert.match(app, /renderRecords\(state\.liveSurface\)/);
assert.match(app, /traceId: error\.traceId \|\| traceId/);
assert.match(app, /error\.traceId = traceId/);
for (const hardwareTerm of [
"Gateway/Box 在线",
"BOX-SIMU 端口",
"Patch Panel 连线",
"DO1 -> patch-panel -> DI1",
"AI/AO/FREQ",
"future capability",
"SOURCE",
"DEV-LIVE",
"BLOCKED",
"runtime counts;不是 M3 DEV-LIVE 证据",
"counts 不是 patch-panel live report",
"缺少 patch-panel live report 与 operation/trace/audit/evidence IDs",
"Web 不提供硬件写 RPC;不会新增伪硬件写操作。"
]) {
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
}
assert.match(app, /dataset\.sourceKind/);
assert.match(styles, /\.record-group\s*{/);
assert.match(styles, /\.record-group-title\s*{/);
assert.match(styles, /\.hardware-section\s*{/);
assert.match(styles, /\.hardware-row\s*{/);
assert.match(html, /Gate \/ 诊断 \/ 验收/);
assert.match(app, /new Set\(\["\/gate", "\/diagnostics\/gate"\]\)/);
assert.match(html, /href="\/styles\.css"/);
assert.match(html, /src="\/app\.mjs"/);
for (const workbenchElement of [
"activity-rail",
"explorer",
"resource-explorer",
"resource-tree",
"conversation-list",
"agent-chat-status",
"task-list",
"right-sidebar",
"command-form",
"command-send",
"hardware-list",
"control-list",
"wiring-body",
"records-list",
"diagnostics-list"
]) {
assert.match(html, new RegExp(workbenchElement));
}
assert.match(html, /aria-controls="resource-explorer"/);
assert.match(html, /aria-expanded="true"/);
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(functionBody(app, "routeFromLocation"), /replace\(\/\^#\\\/\?\//);
assert.match(functionBody(app, "routeFromLocation"), /helpPathnames\(\)\.has/);
assert.match(functionBody(app, "routeFromLocation"), /return "help"/);
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(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s);
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100dvh;[^}]*max-height:\s*100dvh;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/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/);
for (const trustedRecordTerm of requiredTrustedRecordTerms) {
assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`);
}
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /link\.fromResourceId/u);
assert.match(buildScript, /buildCloudWebDist/);
assert.match(distContractScript, /"help\.md"/);
assert.match(distContractScript, /"third_party\/marked\/marked\.esm\.js"/);
assert.match(distContractScript, /"third_party\/marked\/LICENSE"/);
assert.match(distContractScript, /gate\/index\.html/);
assert.match(distContractScript, /diagnostics\/gate\/index\.html/);
assert.match(distContractScript, /help\/index\.html/);
assert.match(markedLicense, /MarkedJS/);
assert.match(markedLicense, /Permission is hereby granted, free of charge/);
assert.match(html, /live-status/);
assert.match(app, /function workbenchApiSurfaceStatus/);
assert.match(app, /function workbenchSurfaceReadiness/);
assert.match(app, /function hasDurableRuntimeQueryBlock/);
assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /surface\.durableQueryBlocked/);
assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /surface\.degraded/);
assert.match(app, /function readinessBooleanValues/);
assert.match(functionBody(app, "readinessBooleanValues"), /payload\.ready/);
assert.match(functionBody(app, "readinessBooleanValues"), /payload\.readiness\?\.ready/);
assert.match(functionBody(app, "workbenchSurfaceReadiness"), /readyValues\.includes\(false\)/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /runtime_durable_adapter_query_blocked/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.runtime\?\.blocker/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.readiness\?\.durability\?\.blocker/);
assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /blockedLayer === "durability_query"/);
assert.match(app, /可信记录受阻 \/ 只读模式/);
assert.match(app, /API 降级 \/ 只读模式/);
assert.match(app, /同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/);
assert.match(app, /同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
assert.match(html, /method-list/);
assert.match(html, /quick-actions/);
assert.match(html, /编写任务/);
assert.match(html, /查看接线/);
assert.match(html, /查看状态/);
assert.match(html, /placeholder="输入要发送给 Code Agent 的消息;不会直接触发硬件变更。"/);
assert.match(html, />发送<\/button>/);
assert.doesNotMatch(html, />添加草稿<\/button>/);
assert.doesNotMatch(html, /主流程[\s\S]*添加草稿/u);
assert.match(app, /sendAgentMessage/);
assert.match(app, /fetchJson\("\/v1\/agent\/chat"/);
assert.match(app, /fetchJson\("\/v1\/m3\/io"/);
assert.match(html, /id="m3-control-form"/);
assert.match(html, /id="m3-control-status"/);
assert.match(html, />写入 DO1<\/button>/);
assert.match(html, />读取 DI1<\/button>/);
assert.match(app, /function runM3IoAction/);
assert.match(app, /function renderM3ControlStatus/);
assert.match(app, /frontendBypass=false/);
assert.match(app, /按钮只调用 \/v1\/m3\/io/);
assert.match(app, /Web 不直连 gateway\/box-simu/);
assert.match(styles, /\.m3-control-form\s*{/);
assert.match(app, /Code Agent 调用失败/);
assert.match(app, /normalizeBlockedAgentResult/);
assert.match(app, /isBlockedAgentResponse/);
assert.match(app, /isHttpNon2xxStatus/);
assert.match(app, /providerStatus/);
assert.match(app, /codeAgentAvailability/);
assert.match(app, /codeAgentAvailabilityFrom/);
assert.match(app, /latestCompletedAgentMessage/);
assert.match(app, /BLOCKED 凭证缺口/);
assert.match(app, /provider_unavailable/);
assert.match(app, /OPENAI_API_KEY/);
assert.match(app, /hwlab-code-agent-provider\/openai-api-key/);
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
assert.match(app, /TRUSTED_CODE_AGENT_PROVIDERS/);
assert.match(app, /UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/);
assert.match(app, /isRealCompletedChatResult/);
assert.match(app, /isRealCompletedChatMessage/);
assert.match(app, /hasRealCodeAgentEvidence/);
assert.match(app, /isTrustedCodeAgentProvider\(value\?\.provider\)/);
assert.match(app, /Boolean\(value\?\.model\)/);
assert.match(app, /Boolean\(value\?\.backend\)/);
assert.match(app, /Boolean\(value\?\.traceId\)/);
assert.match(app, /Boolean\(value\?\.conversationId \|\| value\?\.sessionId\)/);
assert.match(app, /providerTrace:\s*result\.providerTrace/);
assert.match(app, /providerTraceSummary\(message\.providerTrace\)/);
assert.match(app, /recordField\("conversation", message\.conversationId\)/);
assert.match(app, /recordField\("provider", message\.provider\)/);
assert.match(app, /Code Agent 完成证据不足/);
assert.match(app, /本次不会标记为真实完成/);
assert.match(app, /untrusted_completion/);
assert.match(app, /function classifyCodeAgentCompletion/);
assert.match(app, /function isSourceFixtureChatResult/);
assert.match(app, /function isSourceFixtureCompletion/);
assert.match(app, /function isSourceFixtureCompletedChatMessage/);
assert.match(app, /Code Agent SOURCE 回复/);
assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/);
assert.match(app, /function messageEvidencePanel/);
assert.match(app, /function messageEvidenceSummary/);
assert.match(app, /function boundedEvidenceField/);
assert.match(app, /sourceKind:\s*completion\.sourceKind/);
assert.match(app, /status === "completed" \? "dev-live"/);
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
assert.doesNotMatch(app, /tone:\s*state\.conversationId\s*\?\s*"dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /sk-[A-Za-z0-9._-]{8,}/u);
assert.match(styles, /\.message-card\.status-completed\s*{/);
assert.match(styles, /\.message-card\.status-source\s*{/);
assert.match(styles, /\.message-user\s*{/);
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
assert.match(styles, /\.message-evidence\s*{/);
assert.match(styles, /\.message-evidence-chip\s*{/);
assert.match(app, /state\.conversationId/);
assert.match(app, /conversationId/);
assert.match(app, /messageId/);
assert.match(app, /createdAt/);
assert.match(app, /updatedAt/);
assert.match(app, /error\?\.message/);
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, /function syncMobileExplorer/);
assert.match(app, /window\.matchMedia\("\(max-width: 860px\)"\)/);
assert.match(app, /setExplorerCollapsed\(true\)/);
assert.match(app, /function collapseExplorerAfterMobileAction/);
assert.match(functionBody(app, "setExplorerCollapsed"), /aria-expanded/);
assert.match(styles, /\.explorer-collapsed \.explorer\s*{[^}]*pointer-events:\s*none;/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer\s*{[\s\S]*?grid-row:\s*1 \/ 3;[\s\S]*?z-index:\s*4;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/);
assert.match(styles, /@media \(max-width: 520px\)[\s\S]*?\.command-bar\s*{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto auto;/);
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, /url\.pathname === "\/v1\/agent\/chat"/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/m3\/io"/);
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(artifactPublisher, /url\.pathname\.replace\(\/\\\/\+\$\/,\s*""\) \|\| "\/"/);
assert.match(artifactPublisher, /routePath === "\/gate"/);
assert.match(artifactPublisher, /routePath === "\/diagnostics\/gate"/);
assert.match(artifactPublisher, /routePath === "\/help"/);
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(app, /https?:\/\/[^"']*(?:gateway-simu|box-simu|patch-panel)[^"']*/iu);
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.deepEqual(gateSummary.topology.gateways.map((gateway) => gateway.gatewayId), ["gwsimu_1", "gwsimu_2"]);
assert.deepEqual(gateSummary.topology.boxResources.map((resource) => resource.resourceId), ["res_boxsimu_1", "res_boxsimu_2"]);
assert.deepEqual(gateSummary.topology.patchPanel.activeConnections, [
{
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
}
]);
assert.doesNotMatch(
JSON.stringify(gateSummary.topology),
/res_m5-control-relay|res_m5-target-board|out1|reset/u,
"16666 default topology must not render M5/reset wiring"
);
assert.equal(gateSummary.m5DryRunTopology.patchPanel.patchPanelStatusId, "pps_m5-0001");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.operations.length, 2);
assert.ok(gateSummary.operations.every((operation) => operation.dryRun === true));
assert.ok(gateSummary.operations.every((operation) => operation.requestedAt && operation.updatedAt));
assert.equal(gateSummary.auditEvents.length, 7);
assert.ok(gateSummary.auditEvents.every((event) => event.dryRun === true));
assert.ok(gateSummary.auditEvents.every((event) => event.traceId && event.occurredAt));
assert.equal(gateSummary.evidenceRecords.length, 2);
assert.ok(gateSummary.evidenceRecords.every((record) => record.traceId && record.createdAt && record.dryRun === true));
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);
}
function functionBody(source, functionName) {
const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u"));
if (!match) return "";
let depth = 0;
for (let index = match.index + match[0].length - 1; index < source.length; index += 1) {
const char = source[index];
if (char === "{") depth += 1;
if (char === "}") {
depth -= 1;
if (depth === 0) return source.slice(match.index, index + 1);
}
}
return "";
}