Merge pull request #265 from pikasTech/fix/workbench-first-viewport-cleanup-117-120
fix: clean workbench first viewport labels
This commit is contained in:
@@ -72,6 +72,9 @@ const defaultHomepageForbiddenTerms = Object.freeze([
|
||||
"验收",
|
||||
"M0-M5",
|
||||
"BLOCKED",
|
||||
"SOURCE",
|
||||
"DRY-RUN",
|
||||
"DEV-LIVE",
|
||||
"db-live",
|
||||
"m3-hardware-loop-runtime",
|
||||
"执行轨迹",
|
||||
@@ -133,7 +136,7 @@ const requiredHardwareStatusTerms = Object.freeze([
|
||||
"Patch Panel 连线",
|
||||
"DO1 -> patch-panel -> DI1",
|
||||
"AI/AO/FREQ",
|
||||
"future capability",
|
||||
"后续能力",
|
||||
"SOURCE",
|
||||
"DEV-LIVE",
|
||||
"BLOCKED"
|
||||
@@ -1109,14 +1112,12 @@ function beforeSection(source, viewId) {
|
||||
}
|
||||
|
||||
function defaultHomepageHidesBackstage({ html }) {
|
||||
const workspaceHtml = sectionSource(html, "workspace");
|
||||
const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`;
|
||||
return defaultHomepageForbiddenTerms.every((term) => !visibleShellHtml.includes(term));
|
||||
const firstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
|
||||
return defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(firstViewportText, term));
|
||||
}
|
||||
|
||||
function defaultWorkspaceIsSanitized({ html, app }) {
|
||||
const workspaceHtml = sectionSource(html, "workspace");
|
||||
const defaultShell = `${beforeSection(html, "help")}\n${workspaceHtml}`;
|
||||
const defaultShellText = visibleTextFromHtml(defaultFirstViewportHtml(html));
|
||||
const userFacingBodies = [
|
||||
functionBody(app, "codeAgentStatusMessage"),
|
||||
functionBody(app, "codeAgentPromptText"),
|
||||
@@ -1127,14 +1128,47 @@ function defaultWorkspaceIsSanitized({ html, app }) {
|
||||
functionBody(app, "renderWiringList")
|
||||
].join("\n");
|
||||
return (
|
||||
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live/u.test(defaultShell) &&
|
||||
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u.test(userFacingBodies) &&
|
||||
defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(defaultShellText, term)) &&
|
||||
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口|SOURCE fixture|DEV-LIVE/u.test(userFacingBodies) &&
|
||||
/服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) &&
|
||||
/Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) &&
|
||||
/等待可信记录/u.test(functionBody(app, "renderWiringList"))
|
||||
);
|
||||
}
|
||||
|
||||
function defaultFirstViewportHtml(html) {
|
||||
return `${beforeSection(html, "help")}\n${rightSidebarDefaultHtml(html)}`;
|
||||
}
|
||||
|
||||
function rightSidebarDefaultHtml(html) {
|
||||
const start = html.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
|
||||
if (start === -1) return "";
|
||||
const wiringStart = html.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
|
||||
if (wiringStart !== -1) return html.slice(start, start + wiringStart);
|
||||
const end = html.slice(start).search(/<\/aside>/u);
|
||||
return end === -1 ? html.slice(start) : html.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 hasCompletePrimaryNavigation(html) {
|
||||
const navLabelsPresent = ["工作台", "内部复核", "使用说明", "资源树"].every((label) => html.includes(`>${label}<`));
|
||||
const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`));
|
||||
@@ -1190,7 +1224,7 @@ function hardwareStatusStructureContract({ html, app, styles }) {
|
||||
/function\s+hasTrustedM3LiveEvidence\s*\(/u.test(app) &&
|
||||
/function\s+linkStatusLabel\s*\(/u.test(app) &&
|
||||
/function\s+normalizePort\s*\(/u.test(app) &&
|
||||
/Web 不提供硬件写 RPC;不会新增伪硬件写操作。/u.test(app) &&
|
||||
/页面不直连模拟器,也不伪造硬件状态。/u.test(app) &&
|
||||
/\.hardware-section\s*\{/u.test(styles) &&
|
||||
/\.hardware-row\s*\{/u.test(styles)
|
||||
);
|
||||
@@ -1370,8 +1404,8 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
|
||||
blockedCodeAgentUiLabels.some((label) => app.includes(label)) &&
|
||||
/provider_unavailable/u.test(app) &&
|
||||
/OPENAI_API_KEY/u.test(app) &&
|
||||
/只有真实 completed 回复才标 DEV-LIVE/u.test(app) &&
|
||||
/不能因为只有 conversationId 标为开发实况/u.test(app) &&
|
||||
/只有真实完成回复才显示为完成/u.test(app) &&
|
||||
/不能因为只有会话编号就当成实况完成/u.test(app) &&
|
||||
/isTrustedCodeAgentProvider/u.test(app) &&
|
||||
/Boolean\(value\?\.model\)/u.test(app) &&
|
||||
/Boolean\(value\?\.backend\)/u.test(app) &&
|
||||
@@ -1492,8 +1526,8 @@ function m3FixtureCountsNotDevLive(app) {
|
||||
/hasTrustedPatchPanelLiveReport/u.test(liveEvidenceBody) &&
|
||||
/serviceId\s*!==\s*M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(patchPanelReportBody) &&
|
||||
/trustedM3Link\(\[link\]\)/u.test(patchPanelReportBody) &&
|
||||
/counts 不是 patch-panel live report/u.test(linkDetailBody) &&
|
||||
/SOURCE fixture 包含目标接线/u.test(linkDetailBody)
|
||||
/只读运行计数已返回,但还不能证明接线盘闭环/u.test(linkDetailBody) &&
|
||||
/来源拓扑包含目标接线/u.test(linkDetailBody)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1869,7 +1903,7 @@ async function inspectLiveDom(url) {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
|
||||
const dom = await page.evaluate(async () => {
|
||||
const dom = await page.evaluate(async (forbiddenTerms) => {
|
||||
const workspace = document.querySelector('[data-view="workspace"]');
|
||||
const gate = document.querySelector('[data-view="gate"]');
|
||||
const help = document.querySelector('[data-view="help"]');
|
||||
@@ -1884,10 +1918,34 @@ async function inspectLiveDom(url) {
|
||||
const box = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" && style.display !== "none" && box.width > 0 && box.height > 0;
|
||||
};
|
||||
const visibleTextInFirstViewport = () => {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const chunks = [];
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
const textContent = node.textContent?.replace(/\s+/gu, " ").trim();
|
||||
if (!textContent) continue;
|
||||
const parent = node.parentElement;
|
||||
if (!parent) continue;
|
||||
const style = getComputedStyle(parent);
|
||||
if (style.visibility === "hidden" || style.display === "none") continue;
|
||||
const box = parent.getBoundingClientRect();
|
||||
const inViewport = box.width > 0 && box.height > 0 && box.bottom > 0 && box.right > 0 && box.top < window.innerHeight && box.left < window.innerWidth;
|
||||
if (inViewport) chunks.push(textContent);
|
||||
}
|
||||
return chunks.join(" ").replace(/\s+/gu, " ").trim();
|
||||
};
|
||||
const textHasForbiddenTerm = (candidateText, term) => {
|
||||
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
|
||||
const escaped = term.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
return new RegExp(`\\b${escaped}\\b`, "u").test(candidateText);
|
||||
}
|
||||
return candidateText.includes(term);
|
||||
};
|
||||
window.scrollTo(0, 240);
|
||||
document.documentElement.scrollTop = 240;
|
||||
document.body.scrollTop = 240;
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
const firstViewportText = visibleTextInFirstViewport();
|
||||
const rootAfterScrollAttempt = {
|
||||
windowScrollY: window.scrollY,
|
||||
htmlScrollTop: document.documentElement.scrollTop,
|
||||
@@ -1903,6 +1961,8 @@ async function inspectLiveDom(url) {
|
||||
diagnosticsHidden: diagnostics ? diagnostics.hidden : null,
|
||||
outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2,
|
||||
rootAfterScrollAttempt,
|
||||
firstViewportForbiddenPresent: forbiddenTerms.filter((term) => textHasForbiddenTerm(firstViewportText, term)),
|
||||
firstViewportTextSample: firstViewportText.slice(0, 400),
|
||||
labelsPresent: ["硬件资源", "Agent 对话", "工作清单", "可信记录", "使用说明"].every((label) => text.includes(label)),
|
||||
coreControlsVisible: {
|
||||
commandInput: visible("#command-input"),
|
||||
@@ -1914,7 +1974,7 @@ async function inspectLiveDom(url) {
|
||||
recordsTab: visible("#tab-records")
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [...defaultHomepageForbiddenTerms]);
|
||||
const pass =
|
||||
dom.title === "HWLAB 云工作台" &&
|
||||
dom.bodyOverflow === "hidden" &&
|
||||
@@ -1927,12 +1987,18 @@ async function inspectLiveDom(url) {
|
||||
dom.rootAfterScrollAttempt.windowScrollY === 0 &&
|
||||
dom.rootAfterScrollAttempt.htmlScrollTop === 0 &&
|
||||
dom.rootAfterScrollAttempt.bodyScrollTop === 0 &&
|
||||
dom.firstViewportForbiddenPresent.length === 0 &&
|
||||
dom.labelsPresent &&
|
||||
Object.values(dom.coreControlsVisible).every(Boolean);
|
||||
return {
|
||||
status: pass ? "pass" : "blocked",
|
||||
summary: pass ? "Browser DOM confirms the default workbench, core controls, and outer scroll lock." : "Browser DOM did not satisfy the workbench contract.",
|
||||
evidence: [`title=${dom.title}`, `bodyOverflow=${dom.bodyOverflow}`, `htmlOverflow=${dom.htmlOverflow}`],
|
||||
evidence: [
|
||||
`title=${dom.title}`,
|
||||
`bodyOverflow=${dom.bodyOverflow}`,
|
||||
`htmlOverflow=${dom.htmlOverflow}`,
|
||||
`firstViewportForbidden=${dom.firstViewportForbiddenPresent.join(",") || "none"}`
|
||||
],
|
||||
observations: dom
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
+63
-51
@@ -17,7 +17,7 @@ const STATUS_LABELS = Object.freeze({
|
||||
active: "活动",
|
||||
available: "可用",
|
||||
blocked_after_cloud_api: "cloud-api 后阻塞",
|
||||
blocked: "阻塞 BLOCKED",
|
||||
blocked: "待处理",
|
||||
completed: "完成",
|
||||
connected: "已连接",
|
||||
degraded: "降级",
|
||||
@@ -36,9 +36,9 @@ const STATUS_LABELS = Object.freeze({
|
||||
recorded: "已记录",
|
||||
sent: "已发送",
|
||||
requires: "依赖",
|
||||
source: "来源 SOURCE",
|
||||
"dry-run": "演练 DRY-RUN",
|
||||
"dev-live": "开发实况 DEV-LIVE"
|
||||
source: "来源",
|
||||
"dry-run": "演练",
|
||||
"dev-live": "实况已验证"
|
||||
});
|
||||
|
||||
const M3_TRUSTED_ROUTE = Object.freeze({
|
||||
@@ -913,7 +913,7 @@ function renderUnblockList() {
|
||||
},
|
||||
{
|
||||
title: "证据升级规则",
|
||||
detail: "SOURCE、LOCAL、DRY-RUN 与 fixture 只能作为解释材料,不能升级为 DEV-LIVE 验收证据。",
|
||||
detail: "源码、本地与演练记录只能作为解释材料,不能升级为实况验收证据。",
|
||||
tone: "dry-run"
|
||||
}
|
||||
];
|
||||
@@ -931,7 +931,7 @@ function renderHardwareStatus(summary) {
|
||||
const patchTone = m3Live ? "live" : patchPanel.state === "active" ? "source" : patchPanel.state;
|
||||
const linkTone = m3Live ? "live" : sourceLink || patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked";
|
||||
|
||||
el.hardwareSourceStatus.textContent = m3Live ? "DEV-LIVE" : "SOURCE";
|
||||
el.hardwareSourceStatus.textContent = m3Live ? "实况已验证" : "只读核对";
|
||||
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`;
|
||||
|
||||
replaceChildren(
|
||||
@@ -943,8 +943,8 @@ function renderHardwareStatus(summary) {
|
||||
label: "Gateway-SIMU",
|
||||
value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看",
|
||||
detail: counts
|
||||
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
|
||||
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`,
|
||||
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读运行计数;仅用于资源核对。`
|
||||
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自内置拓扑。`,
|
||||
sourceKind: runtimeCountsSourceKind,
|
||||
tone: runtimeCountsTone
|
||||
},
|
||||
@@ -952,8 +952,8 @@ function renderHardwareStatus(summary) {
|
||||
label: "BOX-SIMU",
|
||||
value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看",
|
||||
detail: counts
|
||||
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
|
||||
: `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`,
|
||||
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读运行计数;仅用于资源核对。`
|
||||
: `${gateSummary.boxResourceCount} 个 BOX 资源来自内置拓扑。`,
|
||||
sourceKind: runtimeCountsSourceKind,
|
||||
tone: runtimeCountsTone
|
||||
}
|
||||
@@ -964,22 +964,22 @@ function renderHardwareStatus(summary) {
|
||||
rows: [
|
||||
{
|
||||
label: "DO1",
|
||||
value: m3Live ? "verified" : "not wired",
|
||||
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。",
|
||||
value: m3Live ? "已验证" : "待接线",
|
||||
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DO1 电平的完整可信记录。",
|
||||
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
|
||||
tone: m3Live ? "live" : "blocked"
|
||||
},
|
||||
{
|
||||
label: "DI1",
|
||||
value: m3Live ? "verified" : "not wired",
|
||||
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。",
|
||||
value: m3Live ? "已验证" : "待回读",
|
||||
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DI1 回读的完整可信记录。",
|
||||
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
|
||||
tone: m3Live ? "live" : "blocked"
|
||||
},
|
||||
{
|
||||
label: "AI/AO/FREQ",
|
||||
value: "future capability",
|
||||
detail: "当前未声明模拟量/频率闭环,按 future capability 处理。",
|
||||
value: "后续能力",
|
||||
detail: "当前未声明模拟量或频率闭环,按后续能力处理。",
|
||||
sourceKind: "SOURCE",
|
||||
tone: "disabled"
|
||||
}
|
||||
@@ -992,8 +992,8 @@ function renderHardwareStatus(summary) {
|
||||
label: "hwlab-patch-panel",
|
||||
value: statusLabel(patchPanel.state),
|
||||
detail: m3Live
|
||||
? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route。`
|
||||
: `${patchPanel.activeConnectionCount} 条 SOURCE fixture 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE。`,
|
||||
? `接线盘实况记录 ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定目标链路。`
|
||||
: `${patchPanel.activeConnectionCount} 条内置接线记录;工作台只展示目标链路,未升级为实况验收。`,
|
||||
sourceKind: m3Live ? "DEV-LIVE" : "SOURCE",
|
||||
tone: patchTone
|
||||
},
|
||||
@@ -1011,10 +1011,10 @@ function renderHardwareStatus(summary) {
|
||||
rows: [
|
||||
{
|
||||
label: "硬件控制",
|
||||
value: state.m3Control.contract?.status === "available" ? "cloud-api 受控" : "阻塞",
|
||||
value: state.m3Control.contract?.status === "available" ? "受控路径" : "等待探测",
|
||||
detail: state.m3Control.contract?.status === "available"
|
||||
? "M3 控制只调用 same-origin /v1/m3/io,由 cloud-api 转发 gateway-simu/box-simu 并触发 patch-panel;Web 不直连 gateway/box-simu。Web 不提供硬件写 RPC;不会新增伪硬件写操作。"
|
||||
: "M3 控制路径未就绪;Web 不提供直连 gateway/box-simu 或伪硬件状态写入。",
|
||||
? "控制只通过受控后端执行,并经由 gateway-simu、box-simu 与 patch-panel;页面不直连模拟器,也不伪造硬件状态。"
|
||||
: "控制路径未就绪;页面不直连模拟器,也不伪造硬件状态。",
|
||||
sourceKind: state.m3Control.contract?.status === "available" ? "SOURCE" : "BLOCKED",
|
||||
tone: state.m3Control.contract?.status === "available" ? "source" : "blocked"
|
||||
}
|
||||
@@ -1040,7 +1040,7 @@ function hardwareRow(item) {
|
||||
row.dataset.sourceKind = item.sourceKind;
|
||||
const head = document.createElement("div");
|
||||
head.className = "hardware-row-head";
|
||||
head.append(textSpan(item.label, "hardware-row-label"), badge(item.sourceKind, item.sourceKind));
|
||||
head.append(textSpan(item.label, "hardware-row-label"), badge(item.badgeLabel ?? hardwareEvidenceLabel(item.sourceKind), item.sourceKind));
|
||||
row.append(head);
|
||||
row.append(textSpan(item.value, `hardware-row-value tone-${toneClass(item.tone)}`));
|
||||
row.append(textSpan(item.detail, "hardware-row-detail"));
|
||||
@@ -1060,18 +1060,30 @@ function trustedM3Link(connections) {
|
||||
|
||||
function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) {
|
||||
if (hasTrustedM3LiveEvidence(liveM3Evidence)) {
|
||||
return `DEV-LIVE M3:${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort};${m3LiveDetail(liveM3Evidence)}。`;
|
||||
return `实况链路:${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort};${m3LiveDetail(liveM3Evidence)}。`;
|
||||
}
|
||||
if (sourceLink) {
|
||||
return `BLOCKED:SOURCE fixture 包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},但缺少 patch-panel live report 与 operation/trace/audit/evidence IDs。`;
|
||||
return `来源拓扑包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},仍在等待完整可信记录。`;
|
||||
}
|
||||
if (counts) {
|
||||
return "BLOCKED:runtime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。";
|
||||
return "只读运行计数已返回,但还不能证明接线盘闭环。";
|
||||
}
|
||||
if (patchPanel.activeConnectionCount > 0) {
|
||||
return `SOURCE 仅看到非 M3 patch-panel fixture 接线;required M3 route ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍 BLOCKED,未冒充 live。`;
|
||||
return `当前只看到非目标接线;目标链路 ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍在等待可信记录。`;
|
||||
}
|
||||
return "BLOCKED:当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。";
|
||||
return "当前还没有 DO1 -> patch-panel -> DI1 的可信链路记录。";
|
||||
}
|
||||
|
||||
function hardwareEvidenceLabel(sourceKind) {
|
||||
const normalized = String(sourceKind ?? "").toUpperCase();
|
||||
return (
|
||||
{
|
||||
SOURCE: "来源拓扑",
|
||||
BLOCKED: "待可信记录",
|
||||
"DEV-LIVE": "实况已验证",
|
||||
"DRY-RUN": "演练记录"
|
||||
}[normalized] ?? statusLabel(sourceKind)
|
||||
);
|
||||
}
|
||||
|
||||
function linkStatusLabel(status) {
|
||||
@@ -1202,13 +1214,13 @@ function renderM3ControlStatus() {
|
||||
? "执行中"
|
||||
: operationSucceeded
|
||||
? operation.evidenceState?.status === "green"
|
||||
? "控制可达 / 证据 green"
|
||||
: "控制可达 / 证据 blocked"
|
||||
? "控制可达 / 记录完整"
|
||||
: "控制可达 / 等待记录"
|
||||
: operationBlocked
|
||||
? "操作 BLOCKED"
|
||||
? "操作待处理"
|
||||
: contractAvailable
|
||||
? "cloud-api 受控"
|
||||
: "BLOCKED";
|
||||
? "受控路径可用"
|
||||
: "等待探测";
|
||||
const tone = pending
|
||||
? "dry-run"
|
||||
: operationSucceeded
|
||||
@@ -1244,16 +1256,16 @@ function controlRows() {
|
||||
: [];
|
||||
return [
|
||||
{
|
||||
title: "cloud-api 控制路径",
|
||||
title: "控制路径",
|
||||
detail: state.m3Control.contract?.status === "available"
|
||||
? "按钮只调用 /v1/m3/io;cloud-api 再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
|
||||
: state.m3Control.contract?.blockedReason ?? "等待 /v1/m3/io 合同探测;不可用时不会直连 gateway/box-simu。",
|
||||
? "按钮只走受控后端,再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
|
||||
: "等待受控路径探测;不可用时不会直连 gateway/box-simu。",
|
||||
tone: state.m3Control.contract?.status === "available" ? "source" : "blocked"
|
||||
},
|
||||
...operationCards,
|
||||
{
|
||||
title: "读取目标",
|
||||
detail: `${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 只能通过 /v1/m3/io 的 di.read 读取;不能从浏览器直连 box-simu。`,
|
||||
detail: `${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 只能通过受控路径读取;浏览器不直连 box-simu。`,
|
||||
tone: "source"
|
||||
},
|
||||
{
|
||||
@@ -1263,7 +1275,7 @@ function controlRows() {
|
||||
},
|
||||
{
|
||||
title: "只读探测",
|
||||
detail: "技术复核探测仍保持 same-origin read-only,并放在二级页面。",
|
||||
detail: "内部复核探测保持只读,并放在二级页面。",
|
||||
tone: "source"
|
||||
}
|
||||
];
|
||||
@@ -1328,9 +1340,9 @@ function renderWiringList(summary = null) {
|
||||
tr.append(cell(M3_TRUSTED_ROUTE.toResourceId, "mono wrap"));
|
||||
tr.append(cell(M3_TRUSTED_ROUTE.toPort, "mono"));
|
||||
const status = document.createElement("td");
|
||||
status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked"));
|
||||
status.append(badge(m3Live ? "实况已验证" : "待可信记录", m3Live ? "dev-live" : "blocked"));
|
||||
tr.append(status);
|
||||
tr.append(cell(m3Live ? "DEV-LIVE" : "来源拓扑", "wrap"));
|
||||
tr.append(cell(m3Live ? "实况可信记录" : "来源拓扑", "wrap"));
|
||||
tr.append(cell(traceEvidence, "mono wrap"));
|
||||
|
||||
replaceChildren(el.wiringBody, tr);
|
||||
@@ -2010,8 +2022,8 @@ function codeAgentStatusMessage(availability) {
|
||||
if (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||||
return {
|
||||
role: "system",
|
||||
title: "Code Agent 状态:SOURCE fixture",
|
||||
text: "当前只用于本地浏览器 smoke:可以验证发送、处理中、回复渲染和 evidence 展示,但不会标记为 DEV-LIVE。",
|
||||
title: "Code Agent 状态:本地验证记录",
|
||||
text: "当前只用于本地浏览器验证:可以检查发送、处理中、回复渲染和证据展示,但不会标记为实况完成。",
|
||||
status: "source"
|
||||
};
|
||||
}
|
||||
@@ -2019,38 +2031,38 @@ function codeAgentStatusMessage(availability) {
|
||||
return {
|
||||
role: "system",
|
||||
title: "Code Agent 状态:真实通道已接入",
|
||||
text: "真实 /v1/agent/chat 后端已接入;发送后只有实际 completed 回复才会标记为 DEV-LIVE。",
|
||||
text: "受控对话通道已接入;发送后只有真实完成的回复才会标记为已完成。",
|
||||
status: "source"
|
||||
};
|
||||
}
|
||||
return {
|
||||
role: "system",
|
||||
title: "Code Agent 状态:等待只读探测",
|
||||
text: "正在读取 same-origin /v1 与 /health/live;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容标成 DEV-LIVE。",
|
||||
text: "正在确认服务状态;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容当成真实回复。",
|
||||
status: "source"
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentPromptText(availability) {
|
||||
if (availability?.status === "blocked") {
|
||||
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
|
||||
return "可以继续输入并保留草稿;发送会走受控对话通道。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
|
||||
}
|
||||
return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。";
|
||||
return "请在底部输入中文消息并点击发送;完成态只来自真实完成的后端回复。";
|
||||
}
|
||||
|
||||
function codeAgentControlSummary(availability) {
|
||||
if (latestCompletedAgentMessage()) {
|
||||
return state.conversationId
|
||||
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。`
|
||||
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。";
|
||||
? `当前会话 ${state.conversationId};最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。`
|
||||
: "最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。";
|
||||
}
|
||||
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
|
||||
return "最近一次为 SOURCE fixture 回复;它证明本地浏览器对话 UX,不作为 DEV-LIVE 或 #143 完成证据。";
|
||||
return "最近一次为本地验证回复;它只证明浏览器对话体验,不作为实况完成证据。";
|
||||
}
|
||||
if (availability?.status === "blocked") {
|
||||
return codeAgentBlockedSummary(availability);
|
||||
}
|
||||
return "输入区会调用 cloud-api 受控 Code Agent 接口;只有真实 completed 回复才标 DEV-LIVE,不能因为只有 conversationId 标为开发实况。";
|
||||
return "输入区会调用受控 Code Agent 接口;只有真实完成回复才显示为完成,不能因为只有会话编号就当成实况完成。";
|
||||
}
|
||||
|
||||
function codeAgentBlockedSummary(availability) {
|
||||
@@ -2071,10 +2083,10 @@ function codeAgentAvailabilitySummary() {
|
||||
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
|
||||
}
|
||||
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
|
||||
return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。";
|
||||
return "同源 API 可响应;当前 Code Agent 回复来源是本地验证记录,仅用于浏览器验证,不是实况完成。";
|
||||
}
|
||||
if (state.codeAgentAvailability?.status === "available") {
|
||||
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
|
||||
return "同源 API 可响应;Code Agent 真实通道已接入,只有真实完成回复才显示为完成。";
|
||||
}
|
||||
return "同源只读接口可响应。技术复核详情已放在二级页面。";
|
||||
}
|
||||
|
||||
@@ -211,35 +211,35 @@
|
||||
<p class="eyebrow">运行界面</p>
|
||||
<h2>硬件状态:BOX-SIMU / Gateway-SIMU / hwlab-patch-panel</h2>
|
||||
</div>
|
||||
<span class="state-tag tone-source" id="hardware-source-status">SOURCE</span>
|
||||
<span class="state-tag tone-source" id="hardware-source-status">只读核对</span>
|
||||
</div>
|
||||
<div id="hardware-list" class="hardware-list" data-static-source-fallback="SOURCE">
|
||||
<article class="hardware-section">
|
||||
<span class="hardware-section-title">SOURCE 兜底状态</span>
|
||||
<span class="hardware-section-title">只读资源概览</span>
|
||||
<div class="hardware-rows">
|
||||
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
|
||||
<div class="hardware-row-head">
|
||||
<span class="hardware-row-label">Gateway/Box SOURCE fixture</span>
|
||||
<span class="state-tag tone-source">SOURCE</span>
|
||||
<span class="hardware-row-label">Gateway/Box 资源</span>
|
||||
<span class="state-tag tone-source">来源拓扑</span>
|
||||
</div>
|
||||
<span class="hardware-row-value">离线查看</span>
|
||||
<span class="hardware-row-detail">等待 app.mjs 或同源只读 counts 覆盖;当前仅展示 SOURCE 拓扑摘要。</span>
|
||||
<span class="hardware-row-detail">等待同源只读数据覆盖;当前展示内置拓扑摘要。</span>
|
||||
</div>
|
||||
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
|
||||
<div class="hardware-row-head">
|
||||
<span class="hardware-row-label">hwlab-patch-panel</span>
|
||||
<span class="state-tag tone-source">SOURCE</span>
|
||||
<span class="state-tag tone-source">来源拓扑</span>
|
||||
</div>
|
||||
<span class="hardware-row-value">来源拓扑</span>
|
||||
<span class="hardware-row-value">目标接线</span>
|
||||
<span class="hardware-row-detail">res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 仅为源码兜底显示。</span>
|
||||
</div>
|
||||
<div class="hardware-row tone-border-blocked" data-source-kind="BLOCKED">
|
||||
<div class="hardware-row-head">
|
||||
<span class="hardware-row-label">DO1 -> patch-panel -> DI1</span>
|
||||
<span class="state-tag tone-blocked">BLOCKED</span>
|
||||
<span class="state-tag tone-blocked">待可信记录</span>
|
||||
</div>
|
||||
<span class="hardware-row-value">m3-route-required / waiting-for-dev-live</span>
|
||||
<span class="hardware-row-detail">缺少 patch-panel live report 与 operation/trace/audit/evidence IDs,不能标为 DEV-LIVE。</span>
|
||||
<span class="hardware-row-value">等待可信记录</span>
|
||||
<span class="hardware-row-detail">尚未看到完整可信记录,不能作为实况闭环证据。</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -297,7 +297,7 @@
|
||||
<section class="side-panel" id="panel-wiring" role="tabpanel" aria-labelledby="tab-wiring" data-side-panel="wiring" hidden>
|
||||
<div class="panel-title-row">
|
||||
<h2>接线:hwlab-patch-panel</h2>
|
||||
<span class="state-tag tone-source">来源 SOURCE 接线</span>
|
||||
<span class="state-tag tone-source">来源拓扑接线</span>
|
||||
</div>
|
||||
<div class="table-wrap wiring-table-wrap">
|
||||
<table class="wiring-table">
|
||||
@@ -320,9 +320,9 @@
|
||||
<td class="mono wrap">hwlab-patch-panel</td>
|
||||
<td class="mono wrap">res_boxsimu_2</td>
|
||||
<td class="mono">DI1</td>
|
||||
<td><span class="state-tag tone-blocked">阻塞 BLOCKED</span></td>
|
||||
<td class="wrap">来源拓扑 / required M3 route</td>
|
||||
<td class="mono wrap">等待可信记录(m3-route-required / waiting-for-dev-live)</td>
|
||||
<td><span class="state-tag tone-blocked">待可信记录</span></td>
|
||||
<td class="wrap">来源拓扑</td>
|
||||
<td class="mono wrap">等待可信记录</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -332,7 +332,7 @@
|
||||
<section class="side-panel" id="panel-records" role="tabpanel" aria-labelledby="tab-records" data-side-panel="records" hidden>
|
||||
<div class="panel-title-row">
|
||||
<h2>可信记录</h2>
|
||||
<span class="state-tag tone-source">来源 SOURCE + 只读查询</span>
|
||||
<span class="state-tag tone-source">来源拓扑 + 只读查询</span>
|
||||
</div>
|
||||
<div id="records-list" class="compact-list"></div>
|
||||
</section>
|
||||
|
||||
@@ -191,17 +191,18 @@ for (const hardwareTerm of [
|
||||
"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, /function hardwareEvidenceLabel/);
|
||||
assert.match(app, /dataset\.sourceKind/);
|
||||
assert.match(styles, /\.record-group\s*{/);
|
||||
assert.match(styles, /\.record-group-title\s*{/);
|
||||
@@ -282,10 +283,9 @@ for (const helpTerm of [
|
||||
]) {
|
||||
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, /OPENAI_API_KEY/u, /Secret/u, /secretRef/u, /hwlab-code-agent-provider/u, /m3-route-required/u, /waiting-for-dev-live/u]) {
|
||||
assert.doesNotMatch(visibleShellHtml, backendTerm, `default workbench must not expose backend term ${backendTerm}`);
|
||||
const defaultFirstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
|
||||
for (const backendTerm of ["Gate", "诊断", "验收", "BLOCKED", "SOURCE", "DRY-RUN", "DEV-LIVE", "M0-M5", "执行轨迹", "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}`);
|
||||
@@ -355,8 +355,8 @@ 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(app, /按钮只走受控后端/);
|
||||
assert.match(app, /页面不直连模拟器,也不伪造硬件状态。/);
|
||||
assert.match(styles, /\.m3-control-form\s*{/);
|
||||
assert.match(app, /Code Agent 调用失败/);
|
||||
assert.match(app, /normalizeBlockedAgentResult/);
|
||||
@@ -370,8 +370,8 @@ assert.match(app, /latestCompletedAgentMessage/);
|
||||
assert.match(app, /服务受阻/);
|
||||
assert.match(app, /provider_unavailable/);
|
||||
assert.match(app, /OPENAI_API_KEY/);
|
||||
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
|
||||
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
|
||||
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/);
|
||||
@@ -558,6 +558,39 @@ function beforeSection(source, viewId) {
|
||||
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 functionBody(source, functionName) {
|
||||
const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u"));
|
||||
if (!match) return "";
|
||||
|
||||
@@ -52,13 +52,13 @@ export function runM3ControlPanelGuard() {
|
||||
|
||||
for (const copy of [
|
||||
"M3 IO 控制请求失败",
|
||||
"等待 /v1/m3/io 合同探测;不可用时不会直连 gateway/box-simu。",
|
||||
"不能从浏览器直连 box-simu",
|
||||
"等待受控路径探测;不可用时不会直连 gateway/box-simu。",
|
||||
"浏览器不直连 box-simu",
|
||||
"阻塞原因=",
|
||||
"缺少 patch-panel live report 与 operation/trace/audit/evidence IDs",
|
||||
"Web 不提供直连 gateway/box-simu 或伪硬件状态写入。",
|
||||
"控制可达 / 证据 blocked",
|
||||
"阻塞 BLOCKED"
|
||||
"仍在等待完整可信记录",
|
||||
"页面不直连模拟器,也不伪造硬件状态。",
|
||||
"控制可达 / 等待记录",
|
||||
"待可信记录"
|
||||
]) {
|
||||
assert.match(browserSource, new RegExp(escapeRegExp(copy), "u"), `missing Chinese blocked/error copy: ${copy}`);
|
||||
}
|
||||
@@ -87,8 +87,8 @@ function assertControlPanelDom(html) {
|
||||
for (const header of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) {
|
||||
assert.match(wiringPanel, new RegExp(`<th>${escapeRegExp(header)}<\\/th>`, "u"), `wiring table missing ${header}`);
|
||||
}
|
||||
assert.match(wiringPanel, /阻塞 BLOCKED/u, "static wiring row must stay blocked");
|
||||
assert.match(wiringPanel, /m3-route-required \/ waiting-for-dev-live/u, "static wiring row must require live evidence");
|
||||
assert.match(wiringPanel, /待可信记录/u, "static wiring row must stay blocked");
|
||||
assert.match(wiringPanel, /等待可信记录/u, "static wiring row must require live evidence");
|
||||
}
|
||||
|
||||
function assertControlPanelSource(app) {
|
||||
@@ -125,9 +125,9 @@ function assertControlPanelSource(app) {
|
||||
assert.match(actionBody, /evidenceState\?\.status === "green" \? "DEV-LIVE" : "BLOCKED"/u, "M3 operation sourceKind must not claim DEV-LIVE unless evidence is green");
|
||||
|
||||
const controlRowsBody = functionBody(app, "controlRows");
|
||||
assert.match(controlRowsBody, /按钮只调用 \/v1\/m3\/io/u, "control copy must name same-origin control route");
|
||||
assert.match(controlRowsBody, /按钮只走受控后端/u, "control copy must describe the controlled backend route");
|
||||
assert.match(controlRowsBody, /gateway-simu -> box-simu -> hwlab-patch-panel/u, "control copy must show backend-owned path");
|
||||
assert.match(controlRowsBody, /di\.read/u, "control rows must describe DI read affordance");
|
||||
assert.match(controlRowsBody, /受控路径读取/u, "control rows must describe DI read affordance");
|
||||
|
||||
const operationDetailBody = functionBody(app, "m3OperationDetail");
|
||||
for (const field of ["operation", "trace", "audit", "evidence"]) {
|
||||
@@ -187,7 +187,7 @@ function assertNoStaticDevLiveClaim({ html, app }) {
|
||||
const controlPanel = elementSectionById(html, "panel-control");
|
||||
const wiringPanel = elementSectionById(html, "panel-wiring");
|
||||
assert.doesNotMatch(`${controlPanel}\n${wiringPanel}`, /tone-dev-live|开发实况 DEV-LIVE/u, "static M3 panels must not claim DEV-LIVE");
|
||||
assert.match(`${controlPanel}\n${wiringPanel}`, /tone-blocked|阻塞 BLOCKED/u, "static M3 panels must keep blocked/source posture");
|
||||
assert.match(`${controlPanel}\n${wiringPanel}`, /tone-blocked|待可信记录/u, "static M3 panels must keep blocked/source posture");
|
||||
assert.doesNotMatch(
|
||||
functionBody(app, "renderM3ControlStatus"),
|
||||
/operationSucceeded[\s\S]{0,160}\?\s*"dev-live"\s*:\s*"degraded"[\s\S]{0,160}operation\.status === "succeeded"/u,
|
||||
|
||||
Reference in New Issue
Block a user