fix: 收口 M3 控制面板操作体验
This commit is contained in:
@@ -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: "通过",
|
||||
|
||||
Reference in New Issue
Block a user