fix: 收口 M3 控制面板操作体验

This commit is contained in:
unidesk-code-queue-runner
2026-05-24 08:20:52 +00:00
parent bb4d58b07d
commit c0516aa7bc
5 changed files with 440 additions and 42 deletions
+106 -4
View File
@@ -271,6 +271,23 @@ const requiredHardwareStatusTerms = Object.freeze([
"unverified_cloud_api_m3_status"
]);
const requiredM3ControlUserFlowTerms = Object.freeze([
"m3-flow-summary",
"m3-action-summary",
"m3-do1-target",
"m3-di1-observed",
"m3-trust-summary",
"m3-trace-summary",
"DO1 -> patch-panel -> DI1",
"查看最近操作/trace",
"选择 DO1 true/false 后点击“写入 DO1”",
"最近 trace 与 operation 可在“可信记录”页签追踪",
"DO1目标=",
"DI1观测=",
"trusted=",
"durable="
]);
const workbenchMarkers = Object.freeze([
"data-app-shell",
"workbench-shell",
@@ -541,6 +558,11 @@ function runStaticSmoke() {
evidence: requiredHardwareStatusTerms
});
addCheck(checks, blockers, "m3-control-user-flow", m3ControlUserFlowContract(files), "Right control panel exposes an operator-readable DO1 -> patch-panel -> DI1 action summary, current action, DO1 target, DI1 observation, trusted/durable state, and recent trace navigation.", {
blocker: "contract_blocker",
evidence: requiredM3ControlUserFlowTerms
});
addCheck(checks, blockers, "m3-default-wiring-source", defaultTopologyIsM3Only(), "Default 16666 workbench topology exposes only the M3 res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 wiring.", {
blocker: "runtime_blocker",
evidence: ["res_boxsimu_1:DO1", "hwlab-patch-panel", "res_boxsimu_2:DI1"]
@@ -2180,6 +2202,43 @@ function hardwareStatusStructureContract({ html, app, styles }) {
);
}
function m3ControlUserFlowContract({ html, app, styles }) {
const source = `${html}\n${app}\n${styles}`;
const controlPanel = html.match(/<section\b[^>]*\bdata-side-panel=["']control["'][\s\S]*?<\/section>/u)?.[0] ?? "";
const flowSummaryBody = functionBody(app, "renderM3FlowSummary");
const operationDetailBody = functionBody(app, "m3OperationDetail");
const operationRecordsBody = functionBody(app, "operationRecordCards");
return (
requiredM3ControlUserFlowTerms.every((term) => source.includes(term)) &&
/id=["']m3-flow-summary["'][^>]*aria-label=["']M3 DO1 到 DI1 操作摘要["']/u.test(controlPanel) &&
/id=["']m3-flow-state["']/u.test(controlPanel) &&
/id=["']m3-action-summary["']/u.test(controlPanel) &&
/id=["']m3-do1-target["']/u.test(controlPanel) &&
/id=["']m3-di1-observed["']/u.test(controlPanel) &&
/id=["']m3-trust-summary["']/u.test(controlPanel) &&
/id=["']m3-trace-summary["']/u.test(controlPanel) &&
/id=["']m3-records-jump["']/u.test(controlPanel) &&
/function\s+renderM3FlowSummary\s*\(/u.test(app) &&
/function\s+m3OperationDo1Target\s*\(/u.test(app) &&
/function\s+m3OperationDi1Observed\s*\(/u.test(app) &&
/function\s+m3TrustSummaryText\s*\(/u.test(app) &&
/function\s+m3TraceSummaryText\s*\(/u.test(app) &&
/m3OperationDo1Target\(operation/u.test(flowSummaryBody) &&
/m3OperationDi1Observed\(operation/u.test(flowSummaryBody) &&
/m3TrustSummaryText\(operation, contract\)/u.test(flowSummaryBody) &&
/m3TraceSummaryText\(operation, status\)/u.test(flowSummaryBody) &&
/DO1目标=/u.test(operationDetailBody) &&
/DI1观测=/u.test(operationDetailBody) &&
/DO1目标=/u.test(operationRecordsBody) &&
/DI1观测=/u.test(operationRecordsBody) &&
/trusted=/u.test(operationRecordsBody) &&
/durable=/u.test(operationRecordsBody) &&
/\.m3-flow-summary\s*\{/u.test(styles) &&
/\.m3-flow-line\s*\{/u.test(styles) &&
/\.m3-flow-kv\s*\{/u.test(styles)
);
}
function defaultTopologyIsM3Only() {
const activeConnections = gateSummary.topology.patchPanel.activeConnections ?? [];
return (
@@ -5623,14 +5682,24 @@ async function handleM3StatusFixtureApi({ request, response, url, options = {} }
action: body?.action ?? "di.read",
value: body?.action === "di.read" ? options.m3State.diValue : options.m3State.doValue,
result: {
value: body?.action === "di.read" ? options.m3State.diValue : options.m3State.doValue
value: body?.action === "di.read" ? options.m3State.diValue : options.m3State.doValue,
targetReadback: body?.action === "do.write"
? {
status: "succeeded",
value: options.m3State.diValue,
resourceId: "res_boxsimu_2",
port: "DI1"
}
: null
},
operationId: `op_fixture_${options.m3State.sequence}`,
traceId: `trc_fixture_${options.m3State.sequence}`,
auditId: null,
evidenceId: null,
auditId: `aud_fixture_${options.m3State.sequence}`,
evidenceId: `evd_fixture_${options.m3State.sequence}`,
evidenceState: {
status: "blocked",
durable: false,
blocker: "runtime_durable_adapter_auth_blocked",
reason: "runtime_durable_adapter_auth_blocked"
},
controlPath: {
@@ -6525,7 +6594,15 @@ async function inspectM3StatusFixtureViewport(browser, url, viewport) {
document.body.scrollWidth <= document.body.clientWidth + 2 &&
(!right || right.scrollWidth <= right.clientWidth + 2) &&
(!hardwareList || hardwareList.scrollWidth <= hardwareList.clientWidth + 2),
boxTabClickable: visible('[data-hardware-tab="box1"]') && visible('[data-hardware-tab="box2"]')
boxTabClickable: visible('[data-hardware-tab="box1"]') && visible('[data-hardware-tab="box2"]'),
controlSummaryVisible:
visible("#m3-flow-summary") &&
text.includes("DO1 -> patch-panel -> DI1") &&
text.includes("当前 action") &&
text.includes("DO1 目标") &&
text.includes("DI1 观测") &&
text.includes("可信状态") &&
text.includes("查看最近操作/trace")
};
});
const directRuntimeRequests = requests.filter((request) => /gateway-simu|box-simu|patch-panel|:7101|:7201|:7301/iu.test(request.href));
@@ -6537,6 +6614,7 @@ async function inspectM3StatusFixtureViewport(browser, url, viewport) {
dom.outerScrollLocked &&
dom.noHorizontalOverflow &&
dom.boxTabClickable &&
dom.controlSummaryVisible &&
dom.rightWidth >= viewport.expectedRightWidthMin &&
requests.some((request) => request.method === "GET" && request.pathname === "/v1/m3/status") &&
directRuntimeRequests.length === 0;
@@ -6573,6 +6651,7 @@ async function inspectM3StatusFixtureRefresh(browser, url) {
await page.locator("#m3-write-do").click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DO1true"), null, { timeout: 8000 });
const trueBox1 = await hardwarePanelText(page);
const trueControlSummary = await m3ControlSummaryText(page);
await page.locator('[data-hardware-tab="box2"]').click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DI1true"), null, { timeout: 8000 });
const trueBox2 = await hardwarePanelText(page);
@@ -6585,6 +6664,9 @@ async function inspectM3StatusFixtureRefresh(browser, url) {
await page.locator('[data-hardware-tab="box2"]').click();
await page.waitForFunction(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, "").includes("DI1false"), null, { timeout: 8000 });
const falseBox2 = await hardwarePanelText(page);
const falseControlSummary = await m3ControlSummaryText(page);
await page.locator("#m3-records-jump").click();
const recordsText = await page.evaluate(() => document.querySelector("#records-list")?.textContent?.replace(/\s+/gu, " ").trim() ?? "");
const statusRequests = requests.filter((request) => request.method === "GET" && request.pathname === "/v1/m3/status").length;
const ioPosts = requests.filter((request) => request.method === "POST" && request.pathname === "/v1/m3/io").length;
const directRuntimeRequests = requests.filter((request) => /gateway-simu|box-simu|patch-panel|:7101|:7201|:7301/iu.test(request.href));
@@ -6593,8 +6675,21 @@ async function inspectM3StatusFixtureRefresh(browser, url) {
initialBox1.includes("DO1false") &&
trueBox1.includes("DO1true") &&
trueBox2.includes("DI1true") &&
trueControlSummary.includes("DO1 目标 res_boxsimu_1:DO1=true") &&
trueControlSummary.includes("DI1 观测 res_boxsimu_2:DI1=true") &&
trueControlSummary.includes("trusted=false") &&
trueControlSummary.includes("durable=false") &&
trueControlSummary.includes("operation=op_fixture_") &&
trueControlSummary.includes("trace=trc_fixture_") &&
falseBox1.includes("DO1false") &&
falseBox2.includes("DI1false") &&
falseControlSummary.includes("DO1 目标 res_boxsimu_1:DO1=false") &&
falseControlSummary.includes("DI1 观测 res_boxsimu_2:DI1=false") &&
falseControlSummary.includes("trusted=false") &&
falseControlSummary.includes("durable=false") &&
recordsText.includes("DO1目标=res_boxsimu_1:DO1=false") &&
recordsText.includes("DI1观测=res_boxsimu_2:DI1=false") &&
recordsText.includes("trace=trc_fixture_") &&
statusRequests >= 3 &&
ioPosts === 2 &&
directRuntimeRequests.length === 0 &&
@@ -6604,8 +6699,11 @@ async function inspectM3StatusFixtureRefresh(browser, url) {
initialBox1,
trueBox1,
trueBox2,
trueControlSummary,
falseBox1,
falseBox2,
falseControlSummary,
recordsText,
statusRequests,
ioPosts,
directRuntimeRequests,
@@ -6620,6 +6718,10 @@ async function hardwarePanelText(page) {
return page.evaluate(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, " ").trim() ?? "");
}
async function m3ControlSummaryText(page) {
return page.evaluate(() => document.querySelector("#m3-flow-summary")?.textContent?.replace(/\s+/gu, " ").trim() ?? "");
}
function localGateReviewRow({ category, check, statusKey, owner, detail, evidence, updatedAt, next }) {
const label = {
pass: "通过",