d7e84f359b
Implements #317 session-gated read-only runner phase with structured long-lived Codex stdio blockers.
791 lines
41 KiB
JavaScript
791 lines
41 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,
|
|
runDevCloudWorkbenchLayoutSmoke,
|
|
runDevCloudWorkbenchMobileSmoke,
|
|
runDevCloudWorkbenchStaticSmoke
|
|
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
|
import { gateSummary } from "../gate-summary.mjs";
|
|
import { runtime } from "../runtime.mjs";
|
|
import { runM3ControlPanelGuard } from "./m3-control-panel-guard.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 codeAgentTimeoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-long-timeout-contract");
|
|
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();
|
|
const localAgentNo4500Check = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-no-4500ms-permanent-failure");
|
|
const layoutSmokeReportPath = path.resolve(repoRoot, "tmp/dev-cloud-workbench-layout-web-check.json");
|
|
const layoutSmoke = await runDevCloudWorkbenchLayoutSmoke({ reportPath: layoutSmokeReportPath });
|
|
fs.mkdirSync(path.dirname(layoutSmokeReportPath), { recursive: true });
|
|
layoutSmoke.artifacts = { ...(layoutSmoke.artifacts ?? {}), reportPath: layoutSmokeReportPath };
|
|
fs.writeFileSync(layoutSmokeReportPath, `${JSON.stringify(layoutSmoke, null, 2)}\n`, "utf8");
|
|
|
|
assert.notEqual(
|
|
mobileWorkbenchSmoke.status,
|
|
"skip",
|
|
[
|
|
mobileWorkbenchSmoke.summary,
|
|
"Install repository dependencies and run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` to generate the required mobile outer-scroll evidence."
|
|
].filter(Boolean).join(" ")
|
|
);
|
|
assert.notEqual(
|
|
layoutSmoke.status,
|
|
"skip",
|
|
[
|
|
layoutSmoke.summary,
|
|
"Install repository dependencies and run `node scripts/dev-cloud-workbench-layout-smoke.mjs --static` to generate the required layout evidence."
|
|
].filter(Boolean).join(" ")
|
|
);
|
|
|
|
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
|
assert.equal(workbenchSmoke.mode, "source");
|
|
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(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent chat must not reuse the 4500ms light API timeout");
|
|
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.ok(
|
|
mobileOuterScrollSmokeCheck,
|
|
"mobile smoke must include the mobile-outer-scroll-lock check; run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` with Playwright installed."
|
|
);
|
|
assert.equal(mobileOuterScrollSmokeCheck.status, "pass", "mobile smoke must prove outer page scroll remains locked");
|
|
assert.ok(
|
|
mobileHelpSmokeCheck,
|
|
"mobile smoke must include the mobile-help-markdown-route check; run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` with Playwright installed."
|
|
);
|
|
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);
|
|
assert.equal(localAgentNo4500Check?.status, "pass", "local Code Agent fixture must prove the 4500ms browser timeout path is not permanent");
|
|
assert.equal(
|
|
layoutSmoke.status,
|
|
"pass",
|
|
JSON.stringify(compactLayoutSmokeFailures(layoutSmoke), null, 2)
|
|
);
|
|
assert.equal(layoutSmoke.sourceMode, "source-static");
|
|
assert.equal(layoutSmoke.evidenceLevel, "SOURCE");
|
|
assert.equal(layoutSmoke.devLive, false);
|
|
assert.equal(layoutSmoke.safety.layoutOnly, true);
|
|
assert.equal(layoutSmoke.safety.codeAgentPostSent, false);
|
|
assert.equal(layoutSmoke.safety.hardwareWriteApis, false);
|
|
assert.equal(layoutSmoke.failures.length, 0);
|
|
assert.equal(layoutSmoke.viewports.map((viewport) => `${viewport.width}x${viewport.height}`).join(","), "1366x768,1024x768,390x844");
|
|
assert.equal(
|
|
layoutSmoke.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status,
|
|
"pass",
|
|
"#287 hardware status tabs must remain covered by layout smoke"
|
|
);
|
|
assert.equal(
|
|
layoutSmoke.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status,
|
|
"skip",
|
|
"#288 single-table gate is future coverage; current /gate route remains covered by layout smoke"
|
|
);
|
|
for (const layoutCheckId of [
|
|
"layout-desktop-expanded",
|
|
"layout-desktop-collapsed",
|
|
"layout-desktop-restored",
|
|
"layout-narrow-desktop-expanded",
|
|
"layout-narrow-desktop-collapsed",
|
|
"layout-narrow-desktop-restored",
|
|
"layout-mobile-collapsed",
|
|
"layout-mobile-drawer",
|
|
"layout-gate-desktop",
|
|
"layout-gate-narrow-desktop",
|
|
"layout-gate-mobile"
|
|
]) {
|
|
assert.equal(layoutSmoke.checks.find((check) => check.id === layoutCheckId)?.status, "pass", `${layoutCheckId} must pass in web:check layout gate`);
|
|
}
|
|
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 对话",
|
|
"工作清单",
|
|
"可信记录",
|
|
"内部复核",
|
|
"使用说明"
|
|
]) {
|
|
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, /M3 实况状态/);
|
|
assert.match(html, /硬件状态工作区/);
|
|
assert.match(html, /id="hardware-source-status"/);
|
|
assert.match(app, /function renderHardwareStatus/);
|
|
assert.match(app, /function initHardwareTabs/);
|
|
assert.match(app, /function hardwareKvGroup/);
|
|
assert.match(app, /function sourceFallbackM3Status/);
|
|
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(functionBody(app, "sourceFallbackM3Status"), /unverified_cloud_api_m3_status/);
|
|
assert.match(functionBody(app, "sourceFallbackM3Status"), /SOURCE 拓扑不能冒充实况硬件状态/);
|
|
assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u);
|
|
assert.match(functionBody(app, "renderHardwareStatus"), /Key\/Value groups/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 safeFailureReason/);
|
|
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/);
|
|
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
|
|
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*150000/);
|
|
assert.match(app, /async function sendAgentMessage[\s\S]*?timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
|
|
assert.match(functionBody(app, "fetchJson"), /timeoutMs\s*=\s*API_TIMEOUT_MS/);
|
|
for (const hardwareTerm of [
|
|
"Gateway 在线",
|
|
"BOX 在线",
|
|
"patchPanel connectionActive",
|
|
"runtime durable",
|
|
"DISABLED 后续能力",
|
|
"SOURCE",
|
|
"DEV-LIVE",
|
|
"unverified_cloud_api_m3_status",
|
|
"/v1/m3/status",
|
|
"仍在等待完整可信记录",
|
|
"页面不直连模拟器,也不伪造硬件状态。"
|
|
]) {
|
|
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
|
|
}
|
|
for (const hardwareHtmlTerm of ["Gateway-SIMU", "BOX-SIMU-1", "BOX-SIMU-2", "Patch Panel"]) {
|
|
assert.match(html, new RegExp(escapeRegExp(hardwareHtmlTerm)), `missing hardware HTML term: ${hardwareHtmlTerm}`);
|
|
}
|
|
assert.match(app, /function hardwareEvidenceLabel/);
|
|
assert.match(styles, /\.record-group\s*{/);
|
|
assert.match(styles, /\.record-group-title\s*{/);
|
|
assert.match(styles, /\.hardware-section\s*{/);
|
|
assert.match(styles, /\.hardware-status-tabs\s*{/);
|
|
assert.match(styles, /\.kv-row\s*{/);
|
|
assert.match(styles, /\.hardware-row\s*{/);
|
|
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",
|
|
"gate-review-body"
|
|
]) {
|
|
assert.match(html, new RegExp(workbenchElement));
|
|
}
|
|
assert.match(html, /id="explorer-toggle"/);
|
|
assert.match(html, /aria-controls="resource-explorer"/);
|
|
assert.match(html, /aria-expanded="true"/);
|
|
assert.match(html, /aria-pressed="false"/);
|
|
assert.match(html, /aria-label="收起左侧资源树"/);
|
|
assert.match(html, />收起资源树<\/button>/);
|
|
assert.match(app, /setAttribute\("aria-label", collapsed \? "展开左侧资源树" : "收起左侧资源树"\)/);
|
|
assert.match(app, /textContent = collapsed \? "展开资源树" : "收起资源树"/);
|
|
assert.match(styles, /--explorer-width:\s*292px/);
|
|
assert.match(styles, /--right-width:\s*clamp\(560px,\s*38vw,\s*740px\)/);
|
|
assert.match(styles, /--right-width-expanded:\s*clamp\(560px,\s*45vw,\s*740px\)/);
|
|
assert.match(styles, /\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/s);
|
|
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`",
|
|
"`/v1/diagnostics/gate`",
|
|
"只读 `/json-rpc`",
|
|
"`/health/live`",
|
|
"`:16666`",
|
|
"`:16667`"
|
|
]) {
|
|
assert.match(helpMarkdown, new RegExp(escapeRegExp(helpTerm)), `missing help term: ${helpTerm}`);
|
|
}
|
|
const defaultFirstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
|
|
for (const backendTerm of ["Gate", "诊断", "验收", "BLOCKED", "SOURCE", "DRY-RUN", "DEV-LIVE", "M0-M5", "M3 基本", "trace/evidence", "执行轨迹", "OPENAI_API_KEY", "Secret", "secretRef", "hwlab-code-agent-provider", "m3-route-required", "waiting-for-dev-live"]) {
|
|
assert.equal(textHasForbiddenTerm(defaultFirstViewportText, backendTerm), false, `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");
|
|
const wiringPanel = html.match(/<section\b[^>]*\bid="panel-wiring"[\s\S]*?<\/section>/u)?.[0] ?? "";
|
|
assert.match(wiringPanel, /data-wiring-layout="two-column-long-table"/u, "wiring panel must use the #276 two-column long-table layout");
|
|
assert.equal((wiringPanel.match(/<th\b/gu) ?? []).length, 2, "wiring table must have exactly two device header columns");
|
|
assert.equal((wiringPanel.match(/<td\b/gu) ?? []).length, 2, "wiring table static IO row must have exactly two port cells");
|
|
for (const wiringTerm of ["res_boxsimu_1", "res_boxsimu_2", "DO1", "DI1", "hwlab-patch-panel", "状态", "证据来源", "轨迹/证据"]) {
|
|
assert.match(wiringPanel, new RegExp(escapeRegExp(wiringTerm)), `missing wiring term ${wiringTerm}`);
|
|
}
|
|
for (const legacyWideHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]) {
|
|
assert.doesNotMatch(wiringPanel, new RegExp(`<th>${escapeRegExp(legacyWideHeader)}<\\/th>`), `legacy wide wiring column must not return: ${legacyWideHeader}`);
|
|
}
|
|
const gateHtml = sectionSource(html, "gate");
|
|
assert.match(gateHtml, /<h2 id="gate-title">内部复核<\/h2>/);
|
|
assert.match(gateHtml, /id="gate-source-status"/);
|
|
assert.match(gateHtml, /id="gate-status-filter"/);
|
|
assert.match(gateHtml, /id="gate-search"/);
|
|
assert.match(gateHtml, /id="gate-refresh"/);
|
|
assert.match(gateHtml, /class="gate-table"/);
|
|
assert.equal((gateHtml.match(/class="gate-table"/gu) ?? []).length, 1, "/gate must contain one main table");
|
|
for (const gateHeader of ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"]) {
|
|
assert.match(gateHtml, new RegExp(`<th>${escapeRegExp(gateHeader)}<\\/th>`), `missing gate table column ${gateHeader}`);
|
|
}
|
|
for (const gateStatus of ["通过", "阻塞", "失败", "待验证", "信息"]) {
|
|
assert.match(`${gateHtml}\n${app}`, new RegExp(escapeRegExp(gateStatus)), `missing gate status ${gateStatus}`);
|
|
}
|
|
for (const forbiddenGateMarker of ["workspace-panel", "gate-grid", "gate-split", "milestone-grid", "trace-list", "health-body", "diagnostics-list", "method-list", "unblock-list"]) {
|
|
assert.doesNotMatch(gateHtml, new RegExp(escapeRegExp(forbiddenGateMarker)), `/gate must not contain legacy dashboard marker ${forbiddenGateMarker}`);
|
|
}
|
|
assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/);
|
|
assert.match(app, /function renderGateTable/);
|
|
assert.match(app, /function normalizeGateRow/);
|
|
assert.match(app, /function gateLiveBlockerRow/);
|
|
assert.match(app, /sourceKind:\s*"LIVE-BACKEND"/);
|
|
assert.match(app, /live 后端聚合不可用;页面不会回退到 SOURCE 静态拓扑。/);
|
|
assert.doesNotMatch(app, /function\s+(?:renderTrace|renderUnblockList|renderDiagnostics|renderGateDiagnostics|renderMethodList)\s*\(/);
|
|
assert.match(styles, /\.gate-table\s*{/);
|
|
assert.match(styles, /\.gate-controls\s*{/);
|
|
assert.doesNotMatch(styles, /\.gate-grid\s*{/);
|
|
assert.doesNotMatch(styles, /\.milestone-grid\s*{/);
|
|
assert.doesNotMatch(styles, /\.trace-list\s*{/);
|
|
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"), /runtime_durable_adapter_/);
|
|
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 返回 blocked\/degraded\/failed 或 ready=false;当前保持只读查看和本地任务整理。/);
|
|
assert.match(app, /\/health\/live 可响应,但可信记录持久化尚未就绪:/);
|
|
assert.doesNotMatch(app, /API 降级 \/ 只读模式/);
|
|
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
|
|
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(app, /fetchJson\("\/v1\/m3\/status"\)/);
|
|
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, /按钮只走受控后端/);
|
|
assert.match(app, /页面不直连模拟器,也不伪造硬件状态。/);
|
|
assert.match(styles, /\.m3-control-form\s*{/);
|
|
assert.match(html, /data-hardware-tab="overview"[\s\S]*>总览<\/button>/);
|
|
assert.match(html, /data-hardware-tab="gateways"[\s\S]*>Gateway-SIMU<\/button>/);
|
|
assert.match(html, /data-hardware-tab="box1"[\s\S]*>BOX-SIMU-1<\/button>/);
|
|
assert.match(html, /data-hardware-tab="box2"[\s\S]*>BOX-SIMU-2<\/button>/);
|
|
assert.match(html, /data-hardware-tab="patch"[\s\S]*>Patch Panel<\/button>/);
|
|
assert.match(app, /function initHardwareTabs/);
|
|
assert.match(app, /function hardwareKvGroup/);
|
|
assert.match(app, /sourceFallbackM3Status/);
|
|
assert.match(styles, /\.hardware-status-tabs\s*\{/);
|
|
assert.match(styles, /\.kv-list\s*\{/);
|
|
assert.match(styles, /\.right-sidebar \.table-wrap\s*\{[^}]*overflow-x:\s*hidden;/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, /sanitizeCodeAgentAvailability/);
|
|
assert.match(app, /latestCompletedAgentMessage/);
|
|
assert.match(app, /服务受阻/);
|
|
assert.match(app, /provider_unavailable/);
|
|
assert.match(app, /OPENAI_API_KEY/);
|
|
assert.match(app, /只有真实完成回复才显示为完成/);
|
|
assert.match(app, /不能因为只有会话编号就当成实况完成/);
|
|
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, /value\?\.sessionMode === "controlled-readonly-session-registry"/);
|
|
assert.match(app, /value\?\.implementationType === "controlled-readonly-session-registry"/);
|
|
assert.match(app, /value\?\.capabilityLevel === "read-only-session-tools"/);
|
|
assert.match(app, /value\?\.session\?\.status === "idle"/);
|
|
assert.match(app, /typeof value\?\.session\?\.idleTimeoutMs === "number"/);
|
|
assert.match(app, /Boolean\(value\?\.session\?\.lastTraceId\)/);
|
|
assert.match(app, /value\?\.longLivedSessionGate\?\.status === "blocked"/);
|
|
assert.match(app, /Boolean\(value\?\.sessionReuse\)/);
|
|
assert.match(app, /value\?\.runner\?\.codexStdio === false/);
|
|
assert.match(app, /value\?\.runner\?\.writeCapable === false/);
|
|
assert.match(app, /value\?\.runner\?\.durableSession === false/);
|
|
assert.match(app, /providerTrace:\s*result\.providerTrace/);
|
|
assert.match(app, /session:\s*result\.session/);
|
|
assert.match(app, /sessionMode:\s*result\.sessionMode/);
|
|
assert.match(app, /sessionReuse:\s*result\.sessionReuse/);
|
|
assert.match(app, /implementationType:\s*result\.implementationType/);
|
|
assert.match(app, /runnerLimitations:\s*result\.runnerLimitations/);
|
|
assert.match(app, /codexStdioFeasibility:\s*result\.codexStdioFeasibility/);
|
|
assert.match(app, /longLivedSessionGate:\s*result\.longLivedSessionGate/);
|
|
assert.match(app, /providerTraceSummary\(message\.providerTrace\)/);
|
|
assert.match(app, /recordField\("conversation", message\.conversationId\)/);
|
|
assert.match(app, /recordField\("provider", message\.provider\)/);
|
|
assert.match(app, /recordField\("session", sessionSummary\(message\.session\)\)/);
|
|
assert.match(app, /recordField\("sessionMode", message\.sessionMode\)/);
|
|
assert.match(app, /recordField\("sessionReuse", sessionReuseSummary\(message\.sessionReuse\)\)/);
|
|
assert.match(app, /recordField\("implementation", message\.implementationType\)/);
|
|
assert.match(app, /recordField\("limitations", limitationsSummary\(message\.runnerLimitations\)\)/);
|
|
assert.match(app, /recordField\("codexStdio", codexStdioFeasibilitySummary\(message\.codexStdioFeasibility\)\)/);
|
|
assert.match(app, /recordField\("longLivedGate", longLivedSessionGateSummary\(message\.longLivedSessionGate\)\)/);
|
|
assert.match(app, /function sessionSummary/);
|
|
assert.match(app, /function longLivedSessionGateSummary/);
|
|
assert.match(app, /read-only-session-tools/);
|
|
assert.match(app, /controlled-readonly-session-registry/);
|
|
assert.match(app, /not-codex-stdio/);
|
|
assert.match(app, /not-write-capable/);
|
|
assert.match(app, /not-durable-session/);
|
|
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(styles, /\.rail-button\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/s);
|
|
assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/s);
|
|
assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s);
|
|
assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s);
|
|
assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s);
|
|
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-evidence\s*{[\s\S]*?grid-column:\s*1;/);
|
|
for (const userFacingFunctionName of [
|
|
"codeAgentStatusMessage",
|
|
"codeAgentPromptText",
|
|
"codeAgentControlSummary",
|
|
"codeAgentBlockedSummary",
|
|
"codeAgentAvailabilitySummary",
|
|
"failureMessage",
|
|
"safeFailureReason"
|
|
]) {
|
|
assert.doesNotMatch(functionBody(app, userFacingFunctionName), /OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u, `${userFacingFunctionName} must keep default copy sanitized`);
|
|
}
|
|
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.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
|
|
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*150000/);
|
|
assert.match(app, /CODE_AGENT_TIMEOUT_MS/);
|
|
assert.match(app, /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
|
|
assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/);
|
|
assert.match(app, /失败时会保留输入供重试/);
|
|
assert.match(app, /Code Agent 请求超过/);
|
|
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|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\("\/v1\/diagnostics\/gate"/);
|
|
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, /HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS/);
|
|
assert.match(artifactPublisher, /cloudApiProxyTimeoutMs/);
|
|
assert.match(artifactPublisher, /setTimeout\(\(\) => \{/);
|
|
assert.match(artifactPublisher, /cloud api proxy timed out after/);
|
|
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, /url\.pathname === "\/v1\/m3\/status"/);
|
|
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.deepEqual(runtime.serviceRoute, [
|
|
"browser/CLI/gateway",
|
|
"master hwlab-edge-proxy",
|
|
"frp",
|
|
"D601 hwlab-dev/hwlab-router",
|
|
"cloud-web/cloud-api"
|
|
]);
|
|
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();
|
|
runM3ControlPanelGuard();
|
|
runWorkbenchHardwarePanelContract();
|
|
|
|
console.log("hwlab-cloud-web check ok: workbench shell, M3 control guard, 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 tagPattern = /<\/?section\b[^>]*>/giu;
|
|
tagPattern.lastIndex = start;
|
|
let depth = 0;
|
|
for (let match = tagPattern.exec(source); match; match = tagPattern.exec(source)) {
|
|
if (match[0].startsWith("</")) {
|
|
depth -= 1;
|
|
if (depth === 0) return source.slice(start, tagPattern.lastIndex);
|
|
} else {
|
|
depth += 1;
|
|
}
|
|
}
|
|
return source.slice(start);
|
|
}
|
|
|
|
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 defaultFirstViewportHtml(source) {
|
|
return `${beforeSection(source, "help")}\n${rightSidebarDefaultHtml(source)}`;
|
|
}
|
|
|
|
function rightSidebarDefaultHtml(source) {
|
|
const start = source.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
|
|
if (start === -1) return "";
|
|
const wiringStart = source.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
|
|
if (wiringStart !== -1) return source.slice(start, start + wiringStart);
|
|
const end = source.slice(start).search(/<\/aside>/u);
|
|
return end === -1 ? source.slice(start) : source.slice(start, start + end);
|
|
}
|
|
|
|
function visibleTextFromHtml(fragment) {
|
|
return fragment
|
|
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
|
|
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
|
|
.replace(/<[^>]+>/gu, " ")
|
|
.replace(/ /gu, " ")
|
|
.replace(/>/gu, ">")
|
|
.replace(/</gu, "<")
|
|
.replace(/&/gu, "&")
|
|
.replace(/\s+/gu, " ")
|
|
.trim();
|
|
}
|
|
|
|
function textHasForbiddenTerm(text, term) {
|
|
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
|
|
return new RegExp(`\\b${escapeRegExp(term)}\\b`, "u").test(text);
|
|
}
|
|
return text.includes(term);
|
|
}
|
|
|
|
function compactLayoutSmokeFailures(report) {
|
|
return {
|
|
status: report.status,
|
|
sourceMode: report.sourceMode,
|
|
artifacts: report.artifacts,
|
|
blockers: (report.blockers ?? []).map(compactLayoutFailure),
|
|
failures: (report.failures ?? []).map(compactLayoutFailure)
|
|
};
|
|
}
|
|
|
|
function compactLayoutFailure(failure) {
|
|
return {
|
|
checkId: failure.checkId ?? failure.scope ?? null,
|
|
viewport: failure.viewport ?? null,
|
|
selector: failure.selector ?? null,
|
|
failureType: failure.failureType ?? "blocked",
|
|
summary: failure.summary ?? null,
|
|
artifact: failure.artifact ?? null
|
|
};
|
|
}
|
|
|
|
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 "";
|
|
}
|