test: guard resource explorer removal

This commit is contained in:
Code Queue Review
2026-05-23 16:12:47 +00:00
parent d7231a0c54
commit 40cdbcc2bb
3 changed files with 163 additions and 46 deletions
+16 -1
View File
@@ -221,7 +221,10 @@ test("dedicated layout smoke CLI supports static build and DEV live shorthand",
test("source/default smoke covers #352 resource explorer removal contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
assert.equal(report.checks.find((check) => check.id === "feedback-352-resource-explorer-removed")?.status, "pass");
const check = report.checks.find((item) => item.id === "feedback-352-resource-explorer-removed");
assert.equal(check?.status, "pass");
assert.equal(check.evidence.includes("aside#resource-explorer absent"), true);
assert.equal(check.evidence.includes("#explorer-resize absent"), true);
});
test("source/default smoke covers #276 two-column wiring long-table contract", () => {
@@ -723,6 +726,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
"layout-desktop-default",
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-feedback-352-resource-explorer-removed",
"layout-gate-desktop",
"layout-gate-narrow-desktop",
"layout-gate-mobile"
@@ -742,10 +746,18 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
assert.equal(report.safety.hitTestMethod.includes("elementsFromPoint"), true);
assert.match(report.safety.statement, /does not send Code Agent chat, call M3 IO/u);
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip");
const desktopDefault = report.checks.find((check) => check.id === "layout-desktop-default")?.observations;
const resourceExplorerRemoval = report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.observations;
assert.equal(resourceExplorerRemoval.forbiddenSelectors.includes("#explorer-resize"), true);
assert.equal(resourceExplorerRemoval.forbiddenCopy.includes("常用能力"), true);
assert.equal(resourceExplorerRemoval.coverage.desktop.covered, true);
assert.equal(resourceExplorerRemoval.coverage.mobile.removedVisibleCopyAbsent, true);
assert.equal(desktopDefault.removedSelectorsAbsent, true);
assert.equal(desktopDefault.removedVisibleCopyAbsent, true);
assert.equal(desktopDefault.resourceExplorerRemovalGuard, true);
assert.equal(desktopDefault.keyTargetsReachable, true);
assert.equal(desktopDefault.wiring.longTableOk, true);
assert.equal(desktopDefault.wiring.noHorizontalScroll, true);
@@ -766,6 +778,8 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
const mobileDefault = report.checks.find((check) => check.id === "layout-mobile-default")?.observations;
assert.equal(mobileDefault.removedSelectorsAbsent, true);
assert.equal(mobileDefault.removedVisibleCopyAbsent, true);
assert.equal(mobileDefault.resourceExplorerRemovalGuard, true);
assert.equal(mobileDefault.keyTargetsReachable, true);
assert.equal(mobileDefault.wiring.longTableOk, true);
assert.equal(mobileDefault.wiring.noHorizontalScroll, true);
@@ -808,5 +822,6 @@ test("repo-owned web checks expose source build and DEV live layout smoke gates"
assert.equal(cloudWebPackage.scripts["layout:live"].includes("--live --url http://74.48.78.17:16666/"), true);
assert.match(cloudWebCheckSource, /runDevCloudWorkbenchLayoutSmoke/u);
assert.match(cloudWebCheckSource, /tmp\/dev-cloud-workbench-layout-web-check\.json/u);
assert.match(cloudWebCheckSource, /layout-feedback-352-resource-explorer-removed/u);
assert.match(cloudWebCheckSource, /compactLayoutSmokeFailures/u);
});
+137 -45
View File
@@ -157,6 +157,55 @@ const helpRouteAliases = Object.freeze(["/help"]);
const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]);
const removedResourceExplorerSourceTerms = Object.freeze([
"resource-explorer",
"explorer-toggle",
"explorer-resize",
"resource-tree",
"explorer-capabilities",
"capability-count",
"tree-count",
"data-focus-command",
"quick-actions",
"tree-panel",
"explorer-collapsed",
"--explorer-width",
"--explorer-min-width",
"--explorer-max-width"
]);
const removedResourceExplorerDomSelectors = Object.freeze([
"aside#resource-explorer",
"#resource-explorer",
"#explorer-toggle",
"#explorer-resize",
"#resource-tree",
".resource-tree",
"#explorer-capabilities",
".explorer-capabilities",
"#capability-count",
"#tree-count",
"[data-focus-command]",
".quick-actions",
".tree-panel",
".explorer-collapsed"
]);
const removedResourceExplorerCopyTerms = Object.freeze([
"资源浏览与硬件资源树",
"资源浏览",
"硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",
"展开资源树",
"拖拽调整左侧资源树宽度",
"资源查看",
"查看接线",
"查看状态",
"编写任务"
]);
const requiredWiringLongTableHeaders = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]);
const forbiddenLegacyWiringColumns = Object.freeze([
"源设备",
@@ -507,7 +556,7 @@ function runStaticSmoke() {
"aside#resource-explorer absent",
"#explorer-resize absent",
"resource tree / quick actions / capability cards absent",
"right side keeps a 560-740px M3 status workspace boundary through --right-width"
"right side keeps a 560-760px M3 status workspace boundary through --right-width"
]
});
@@ -1052,6 +1101,12 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
summary: "Mobile 390x844 default layout has no resource drawer and preserves Agent input plus M3 tabs/controls.",
observations: mobile.default
},
{
id: "layout-feedback-352-resource-explorer-removed",
status: allViewportCoverage(viewportResults, "resourceExplorerRemovalGuard") ? "pass" : "blocked",
summary: "#352 default workbench has no aside#resource-explorer, #explorer-resize, resource tree/capability copy, or reclaimed left panel across desktop and mobile.",
observations: summarizeResourceExplorerRemovalCoverage(viewportResults)
},
{
id: "layout-right-sidebar-resize-desktop",
status: desktop.rightResize.pass ? "pass" : "blocked",
@@ -1177,7 +1232,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
summary: useLiveUrl
? "Live browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; it is not M3 hardware acceptance."
: "Static local browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; deployment still requires DEV live verification.",
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#352", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks,
blockers,
@@ -1351,7 +1406,7 @@ function layoutSourceContract() {
"docs/reference/cloud-workbench.md",
"docs/reference/code-agent-chat-readiness.md"
],
summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance."
summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, #352 resource-explorer removal, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance."
};
}
@@ -2157,34 +2212,8 @@ function hasMobileWorkbenchLayoutContract({ html, app, styles }) {
function hasResourceExplorerRemovalContract({ html, app, styles }) {
const source = `${html}\n${app}\n${styles}`;
const removedSelectorsAbsent = [
"resource-explorer",
"explorer-toggle",
"explorer-resize",
"resource-tree",
"explorer-capabilities",
"capability-count",
"tree-count",
"data-focus-command",
"quick-actions",
"tree-panel",
"explorer-collapsed",
"--explorer-width",
"--explorer-min-width",
"--explorer-max-width"
].every((term) => !source.includes(term));
const removedTextAbsent = [
"资源浏览与硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",
"展开资源树",
"拖拽调整左侧资源树宽度",
"资源查看",
"查看接线",
"查看状态",
"编写任务"
].every((term) => !source.includes(term));
const removedSelectorsAbsent = removedResourceExplorerSourceTerms.every((term) => !source.includes(term));
const removedTextAbsent = removedResourceExplorerCopyTerms.every((term) => !source.includes(term));
return (
removedSelectorsAbsent &&
removedTextAbsent &&
@@ -5778,7 +5807,7 @@ async function inspectRightSidebarResizeDisabled(page, { viewport }) {
}
async function inspectLayoutState(page, { mode, viewport, compareTo }) {
return page.evaluate(async ({ mode, viewport, compareTo }) => {
return page.evaluate(async ({ mode, viewport, compareTo, resourceExplorerGuard }) => {
const failures = [];
const skip = [];
const selectorLabels = [
@@ -5968,6 +5997,21 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
scrolls: element.scrollHeight > element.clientHeight ? element.scrollTop > before : true
};
};
const visibleTextForElement = (root) => {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const parts = [];
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const parent = node.parentElement;
if (!parent || parent.closest("[hidden], script, style, noscript")) continue;
const style = getComputedStyle(parent);
if (style.display === "none" || style.visibility === "hidden") continue;
const box = parent.getBoundingClientRect();
if (box.width <= 0 || box.height <= 0) continue;
const text = node.textContent?.replace(/\s+/gu, " ").trim();
if (text) parts.push(text);
}
return parts.join(" ").replace(/\s+/gu, " ").trim();
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
@@ -6252,23 +6296,22 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const removedSelectorStates = Object.fromEntries(
[
["#resource-explorer", "resourceExplorer"],
["#explorer-resize", "explorerResize"],
["#explorer-toggle", "explorerToggle"],
["#resource-tree", "resourceTree"],
[".quick-actions", "quickActions"]
].map(([selector, key]) => [key, document.querySelector(selector) === null])
resourceExplorerGuard.domSelectors.map((selector) => [selector, document.querySelector(selector) === null])
);
const removedSelectorsAbsent = Object.values(removedSelectorStates).every(Boolean);
const visibleWorkbenchText = visibleTextForElement(document.body);
const removedVisibleCopyHits = resourceExplorerGuard.copyTerms.filter((term) => visibleWorkbenchText.includes(term));
const removedVisibleCopyAbsent = removedVisibleCopyHits.length === 0;
const resourceExplorerRemovalGuard = removedSelectorsAbsent && removedVisibleCopyAbsent;
const keyTargetsReachable = blockedKeyTargets.length === 0;
if (!removedSelectorsAbsent) {
if (!resourceExplorerRemovalGuard) {
failures.push({
failureType: "blocked/skip",
selector: "#resource-explorer",
selector: "#resource-explorer,#explorer-resize",
viewport,
summary: "Removed resource explorer selectors are still present in the default workbench DOM.",
removedSelectorStates
summary: "Removed resource explorer DOM or legacy copy returned in the default workbench.",
removedSelectorStates,
removedVisibleCopyHits
});
}
if (!rootScrollLocked) {
@@ -6324,7 +6367,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
Object.values(noHorizontalOverflow).every(Boolean) &&
rightWidthTarget &&
keyTargetsReachable &&
removedSelectorsAbsent &&
resourceExplorerRemovalGuard &&
sidePanelOverflowUsable;
const passWithWiring =
pass &&
@@ -6339,6 +6382,9 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
pass: passWithWiring,
removedSelectorStates,
removedSelectorsAbsent,
removedVisibleCopyAbsent,
removedVisibleCopyHits,
resourceExplorerRemovalGuard,
rootScrollLocked,
rootAfterScrollAttempt,
boxes,
@@ -6383,7 +6429,15 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
skip,
failures
};
}, { mode, viewport, compareTo });
}, {
mode,
viewport,
compareTo,
resourceExplorerGuard: {
domSelectors: [...removedResourceExplorerDomSelectors],
copyTerms: [...removedResourceExplorerCopyTerms]
}
});
}
async function inspectGateLayoutState(page, { mode, viewport }) {
@@ -6644,6 +6698,44 @@ function allViewportCoverage(viewportResults, fieldName) {
);
}
function summarizeResourceExplorerRemovalCoverage(viewportResults) {
const coverageEntries = Object.entries(viewportResults).map(([id, result]) => {
const observation = result.default;
return [
id,
{
viewport: observation?.viewport ?? null,
covered: Boolean(observation?.resourceExplorerRemovalGuard),
removedSelectorsAbsent: observation?.removedSelectorsAbsent === true,
removedVisibleCopyAbsent: observation?.removedVisibleCopyAbsent === true,
removedVisibleCopyHits: observation?.removedVisibleCopyHits ?? [],
removedSelectorStates: observation?.removedSelectorStates ?? {}
}
];
});
const coverage = Object.fromEntries(coverageEntries);
const failures = coverageEntries
.filter(([, item]) => !item.covered)
.map(([id, item]) => ({
checkId: "layout-feedback-352-resource-explorer-removed",
viewport: item.viewport,
selector: "#resource-explorer,#explorer-resize",
failureType: "blocked/skip",
summary: `#352 resource explorer removal guard failed in ${id} viewport.`,
removedSelectorsAbsent: item.removedSelectorsAbsent,
removedVisibleCopyAbsent: item.removedVisibleCopyAbsent,
removedVisibleCopyHits: item.removedVisibleCopyHits,
removedSelectorStates: item.removedSelectorStates
}));
return {
status: failures.length === 0 ? "pass" : "blocked",
forbiddenSelectors: [...removedResourceExplorerDomSelectors],
forbiddenCopy: [...removedResourceExplorerCopyTerms],
coverage,
failures
};
}
function summarizeCoverageGap(viewportResults, issueKey) {
const fieldName = {
issue288: "issue288SingleTableReady",
+10
View File
@@ -140,6 +140,7 @@ const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract");
const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
const resourceExplorerRemovalSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "feedback-352-resource-explorer-removed");
const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states");
const codeAgentTimeoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-long-timeout-contract");
const codeAgentQuickPromptsSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-quick-prompts-contract");
@@ -183,6 +184,7 @@ assert.equal(workbenchSmoke.devLive, false);
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
assert.equal(outerScrollSmokeCheck?.status, "pass", "desktop workbench shell must lock outer page scrolling");
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
assert.equal(resourceExplorerRemovalSmokeCheck?.status, "pass", "#352 resource explorer removal source contract must be present");
assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct");
assert.equal(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent chat must not reuse the 4500ms light API timeout");
assert.equal(codeAgentQuickPromptsSmokeCheck?.status, "pass", "Code Agent quick prompts must remain fill-only with explicit send semantics for writes");
@@ -239,6 +241,11 @@ assert.equal(
"pass",
"#276 wiring table must stay readable, non-overlapping, and horizontally contained"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status,
"pass",
"#352 resource explorer removal must remain covered by desktop and 390x844 layout smoke"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status,
"skip",
@@ -248,6 +255,7 @@ for (const layoutCheckId of [
"layout-desktop-default",
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-feedback-352-resource-explorer-removed",
"layout-right-sidebar-resize-desktop",
"layout-right-sidebar-resize-narrow-desktop",
"layout-right-sidebar-resize-mobile",
@@ -413,6 +421,8 @@ for (const removedSelectorTerm of [
}
for (const removedUiText of [
"资源浏览与硬件资源树",
"资源浏览",
"硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",