diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 86f6a692..da296519 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -233,6 +233,8 @@ test("source/default smoke covers #276 two-column wiring long-table contract", ( ["res_boxsimu_1", "res_boxsimu_2", "DO1 对 DI1"] ); assert.equal(check.evidence.includes("legacy 8-column table forbidden"), true); + assert.equal(check.evidence.includes("trace/evidence header forbidden"), true); + assert.equal(check.evidence.includes("状态/证据/trace kept in summary/detail"), true); }); test("source/default smoke covers #288 gate single-table contract", () => { @@ -748,8 +750,14 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou assert.equal(desktopDefault.wiring.longTableOk, true); assert.equal(desktopDefault.wiring.noHorizontalScroll, true); assert.equal(desktopDefault.wiring.headers.length, 2); + assert.deepEqual(desktopDefault.wiring.devicePair, ["res_boxsimu_1", "res_boxsimu_2"]); + assert.equal(desktopDefault.wiring.ioPairs.some((row) => row.join("|") === "DO1|DI1"), true); assert.deepEqual(desktopDefault.wiring.legacyHeaderHits, []); + assert.deepEqual(desktopDefault.wiring.metadataColumnHits, []); + assert.equal(desktopDefault.wiring.metadataInSummary, true); + assert.equal(desktopDefault.wiring.metadataInDetails, true); assert.equal(desktopDefault.wiring.horizontalScroll.panelScrollWidth <= desktopDefault.wiring.horizontalScroll.panelClientWidth + 2, true); + assert.equal(desktopDefault.noHorizontalOverflow.right, true); assert.equal(Object.hasOwn(desktopDefault.boxes.shell, "text"), false); assert.equal(Object.hasOwn(desktopDefault.semanticOverlapChecks[0].boxes["#m3-control-form"], "text"), false); assert.equal(desktopDefault.failures.length, 0); @@ -761,6 +769,13 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou assert.equal(mobileDefault.keyTargetsReachable, true); assert.equal(mobileDefault.wiring.longTableOk, true); assert.equal(mobileDefault.wiring.noHorizontalScroll, true); + assert.deepEqual(mobileDefault.wiring.devicePair, ["res_boxsimu_1", "res_boxsimu_2"]); + assert.equal(mobileDefault.wiring.ioPairs.some((row) => row.join("|") === "DO1|DI1"), true); + assert.deepEqual(mobileDefault.wiring.legacyHeaderHits, []); + assert.deepEqual(mobileDefault.wiring.metadataColumnHits, []); + assert.equal(mobileDefault.wiring.metadataInSummary, true); + assert.equal(mobileDefault.wiring.metadataInDetails, true); + assert.equal(mobileDefault.noHorizontalOverflow.right, true); const compact = compactLayoutSmokeCliOutput(report); assert.equal(compact.status, "pass"); diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index f2ed4158..7e37c583 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -166,7 +166,10 @@ const forbiddenLegacyWiringColumns = Object.freeze([ "目标端口", "状态", "证据来源", - "轨迹/证据" + "轨迹/证据", + "trace", + "evidence", + "trace/evidence" ]); const requiredWiringSummaryTerms = Object.freeze(["hwlab-patch-panel", "状态", "证据来源", "轨迹/证据"]); @@ -463,6 +466,8 @@ function runStaticSmoke() { "DO1 对 DI1", "data-wiring-layout=\"two-column-long-table\"", "legacy 8-column table forbidden", + "trace/evidence header forbidden", + "状态/证据/trace kept in summary/detail", ...requiredWiringSummaryTerms ] }); @@ -1941,6 +1946,14 @@ function textHasForbiddenTerm(text, term) { return text.includes(term); } +function normalizeWiringHeaderText(text) { + return String(text ?? "") + .replace(/\s+/gu, " ") + .replace(/\s*\/\s*/gu, "/") + .trim() + .toLowerCase(); +} + function hasCompletePrimaryNavigation(html) { const navLabelsPresent = ["工作台", "内部复核", "使用说明"].every((label) => html.includes(`>${label}<`)) && @@ -2005,6 +2018,7 @@ function wiringTableContract(html, styles = "", app = "") { const headerCount = (wiringPanel.match(/]*\bdata-wiring-row=["']io["'][\s\S]*?<\/tr>/u)?.[0] ?? ""; const bodyCellCount = (bodyRow.match(/]*>([\s\S]*?)<\/th>/gu)].map((match) => normalizeWiringHeaderText(visibleTextFromHtml(match[1]))); const renderWiringBody = functionBody(app, "renderWiringList"); return ( /data-wiring-layout=["']two-column-long-table["']/u.test(wiringPanel) && @@ -2014,7 +2028,8 @@ function wiringTableContract(html, styles = "", app = "") { requiredWiringLongTableHeaders.every((header) => wiringPanel.includes(header)) && ["DO1", "DI1"].every((port) => wiringPanel.includes(port)) && requiredWiringSummaryTerms.every((term) => wiringPanel.includes(term)) && - forbiddenLegacyWiringColumns.every((column) => !wiringPanel.includes(`${column}`)) && + headerTexts.length === 2 && + forbiddenLegacyWiringColumns.every((column) => !headerTexts.includes(normalizeWiringHeaderText(column))) && /id=["']wiring-summary["']/u.test(wiringPanel) && /overflow-x:\s*hidden/u.test(styles.match(/\.wiring-table-wrap\s*\{[\s\S]*?\}/u)?.[0] ?? "") && !/\.wiring-table\s*\{[\s\S]*?min-width:\s*9\d\dpx/u.test(styles) && @@ -2983,14 +2998,22 @@ async function inspectLiveDom(url, options = {}) { document.querySelector("#wiring-target-device")?.textContent?.trim() ?? "" ], wiringHeaderCount: document.querySelectorAll(".wiring-table thead th").length, - legacyWiringHeaders: [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.trim() ?? "").filter((text) => - ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"].includes(text) - ), + legacyWiringHeaders: [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "").filter((text) => { + const normalized = text.replace(/\s*\/\s*/gu, "/").trim().toLowerCase(); + return ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据", "trace", "evidence", "trace/evidence"].includes(normalized); + }), m3WiringRows: [...document.querySelectorAll("#wiring-body tr")].map((row) => [...row.querySelectorAll("td")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "") ), wiringBodyCellCounts: [...document.querySelectorAll("#wiring-body tr")].map((row) => row.querySelectorAll("td").length), wiringSummaryText: document.querySelector("#wiring-summary")?.textContent?.replace(/\s+/gu, " ").trim() ?? "", + wiringMetadataColumnHits: [...document.querySelectorAll(".wiring-table thead th")].filter((cell) => /状态|证据来源|轨迹\/证据|\btrace\b|\bevidence\b/iu.test(cell.textContent?.replace(/\s+/gu, " ").trim() ?? "")).length, + wiringMetadataInSummary: /状态/u.test(document.querySelector("#wiring-summary")?.textContent ?? "") && + /证据来源/u.test(document.querySelector("#wiring-summary")?.textContent ?? "") && + /轨迹\/证据/u.test(document.querySelector("#wiring-summary")?.textContent ?? ""), + wiringMetadataInDetails: [...document.querySelectorAll(".wiring-detail")].some((detail) => /状态/u.test(detail.textContent ?? "")) && + [...document.querySelectorAll(".wiring-detail")].some((detail) => /证据来源/u.test(detail.textContent ?? "")) && + [...document.querySelectorAll(".wiring-detail")].some((detail) => /轨迹\/证据/u.test(detail.textContent ?? "")), coreControlsVisible: { commandInput: visible("#command-input"), commandSend: visible("#command-send"), @@ -3022,6 +3045,7 @@ async function inspectLiveDom(url, options = {}) { dom.wiringLayout === "two-column-long-table" && dom.wiringHeaderCount === 2 && dom.legacyWiringHeaders.length === 0 && + dom.wiringMetadataColumnHits === 0 && requiredWiringLongTableHeaders.every((header) => dom.wiringHeaderDevices.includes(header)) && dom.wiringBodyCellCounts.every((count) => count === 2) && dom.m3WiringRows.some((row) => @@ -3033,6 +3057,8 @@ async function inspectLiveDom(url, options = {}) { row[1].includes("轨迹/证据") ) && requiredWiringSummaryTerms.every((term) => dom.wiringSummaryText.includes(term)) && + dom.wiringMetadataInSummary && + dom.wiringMetadataInDetails && (postGuard ? postGuard.attempts.length === 0 : true) && Object.values(dom.coreControlsVisible).every(Boolean); return { @@ -3046,6 +3072,8 @@ async function inspectLiveDom(url, options = {}) { `gateRouteAvailable=${dom.gateRouteAvailable}`, `wiringLayout=${dom.wiringLayout}`, `wiringHeaderDevices=${dom.wiringHeaderDevices.join("|")}`, + `legacyWiringHeaders=${dom.legacyWiringHeaders.join("|") || "none"}`, + `wiringMetadataColumnHits=${dom.wiringMetadataColumnHits}`, `wiringBodyCellCounts=${dom.wiringBodyCellCounts.join("|")}`, `m3WiringRows=${dom.m3WiringRows.length}`, ...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : []) @@ -5793,7 +5821,8 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { [".hardware-status", ".side-workspace", "hardware status vs side workspace"], ["#command-form", ".right-sidebar", "Code Agent command form vs right sidebar"] ]; - const wiringLegacyHeaders = ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]; + const wiringLegacyHeaders = ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据", "trace", "evidence", "trace/evidence"]; + const normalizeHeaderText = (text) => String(text ?? "").replace(/\s+/gu, " ").replace(/\s*\/\s*/gu, "/").trim().toLowerCase(); const boxForElement = (element) => { if (!element) return null; const box = element.getBoundingClientRect(); @@ -6104,10 +6133,18 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { const wiringPanel = document.querySelector("#panel-wiring"); const wiringWrap = document.querySelector(".wiring-table-wrap"); const wiringTable = document.querySelector(".wiring-long-table"); - const wiringHeaders = [...document.querySelectorAll(".wiring-table thead th")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + const wiringHeaderCells = [...document.querySelectorAll(".wiring-table thead th")]; + const wiringHeaders = wiringHeaderCells.map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + const wiringDevicePair = [ + document.querySelector("#wiring-source-device")?.textContent?.trim() ?? "", + document.querySelector("#wiring-target-device")?.textContent?.trim() ?? "" + ]; const wiringRows = [...document.querySelectorAll("#wiring-body tr")].map((row) => [...row.querySelectorAll("td")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "") ); + const wiringIoPairs = [...document.querySelectorAll("#wiring-body tr")].map((row) => + [...row.querySelectorAll("td")].map((cell) => [...cell.querySelectorAll(".wiring-port")].map((port) => port.textContent?.trim() ?? "").join(" ")) + ); const wiringCellGeometry = [...document.querySelectorAll(".wiring-long-table th, .wiring-long-table td, #wiring-summary")].map((element) => { const box = element.getBoundingClientRect(); const style = getComputedStyle(element); @@ -6127,7 +6164,16 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { const wiringTextNonOverlapping = wiringCellBoxes.every((box, index) => wiringCellBoxes.every((other, otherIndex) => index === otherIndex || !boxesOverlap(box, other, -1)) ); - const wiringLegacyHeaderHits = wiringHeaders.filter((header) => wiringLegacyHeaders.includes(header)); + const normalizedLegacyHeaders = wiringLegacyHeaders.map(normalizeHeaderText); + const wiringLegacyHeaderHits = wiringHeaders.filter((header) => normalizedLegacyHeaders.includes(normalizeHeaderText(header))); + const wiringMetadataColumnHits = wiringHeaderCells.filter((cell) => /状态|证据来源|轨迹\/证据|\btrace\b|\bevidence\b/iu.test(normalizeHeaderText(cell.textContent))).map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + const wiringSummaryText = document.querySelector("#wiring-summary")?.textContent?.replace(/\s+/gu, " ").trim() ?? ""; + const wiringDetails = [...document.querySelectorAll(".wiring-detail")].map((detail) => detail.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + const wiringMetadataInSummary = ["状态", "证据来源", "轨迹/证据"].every((term) => wiringSummaryText.includes(term)); + const wiringMetadataInDetails = + wiringDetails.some((detail) => detail.includes("状态")) && + wiringDetails.some((detail) => detail.includes("证据来源")) && + wiringDetails.some((detail) => detail.includes("轨迹/证据")); const wiringHorizontalScroll = { panelClientWidth: wiringPanel?.clientWidth ?? 0, panelScrollWidth: wiringPanel?.scrollWidth ?? 0, @@ -6145,9 +6191,9 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { const wiringLongTableOk = wiringWrap?.getAttribute("data-wiring-layout") === "two-column-long-table" && wiringHeaders.length === 2 && - wiringHeaders.includes("res_boxsimu_1 互连设备 A") && - wiringHeaders.includes("res_boxsimu_2 互连设备 B") && + JSON.stringify(wiringDevicePair) === JSON.stringify(["res_boxsimu_1", "res_boxsimu_2"]) && wiringLegacyHeaderHits.length === 0 && + wiringMetadataColumnHits.length === 0 && wiringRows.some((row) => row.length === 2 && row[0].includes("DO1") && @@ -6155,7 +6201,10 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { row[1].includes("DI1") && row[1].includes("证据来源") && row[1].includes("轨迹/证据") - ); + ) && + wiringIoPairs.some((row) => JSON.stringify(row) === JSON.stringify(["DO1", "DI1"])) && + wiringMetadataInSummary && + wiringMetadataInDetails; if (!wiringLongTableOk) { failures.push({ failureType: "blocked/skip", @@ -6164,8 +6213,15 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { summary: "#276 wiring panel is not the required two-column long table with device headers and DO1/DI1 row details.", wiring: { headers: wiringHeaders, + devicePair: wiringDevicePair, legacyHeaderHits: wiringLegacyHeaderHits, + metadataColumnHits: wiringMetadataColumnHits, + metadataInSummary: wiringMetadataInSummary, + metadataInDetails: wiringMetadataInDetails, rows: wiringRows, + ioPairs: wiringIoPairs, + summaryText: wiringSummaryText, + details: wiringDetails, layout: wiringWrap?.getAttribute("data-wiring-layout") ?? null } }); @@ -6307,8 +6363,15 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { textContained: wiringTextContained, textNonOverlapping: wiringTextNonOverlapping, headers: wiringHeaders, + devicePair: wiringDevicePair, legacyHeaderHits: wiringLegacyHeaderHits, + metadataColumnHits: wiringMetadataColumnHits, + metadataInSummary: wiringMetadataInSummary, + metadataInDetails: wiringMetadataInDetails, rows: wiringRows, + ioPairs: wiringIoPairs, + summaryText: wiringSummaryText, + details: wiringDetails, horizontalScroll: wiringHorizontalScroll, cellGeometry: wiringCellGeometry },