Merge pull request #369 from pikasTech/fix/d601-panel-wiring-layout-guard-276

test: guard patch-panel wiring long table
This commit is contained in:
Lyon
2026-05-24 00:07:29 +08:00
committed by GitHub
2 changed files with 89 additions and 11 deletions
@@ -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");
+74 -11
View File
@@ -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(/<th\b/gu) ?? []).length;
const bodyRow = wiringPanel.match(/<tr\b[^>]*\bdata-wiring-row=["']io["'][\s\S]*?<\/tr>/u)?.[0] ?? "";
const bodyCellCount = (bodyRow.match(/<td\b/gu) ?? []).length;
const headerTexts = [...wiringPanel.matchAll(/<th\b[^>]*>([\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(`<th>${column}</th>`)) &&
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
},