Merge pull request #309 from pikasTech/fix/layout-smoke-dev-gate-273

test: gate Cloud Workbench layout smoke
This commit is contained in:
Lyon
2026-05-23 17:20:49 +08:00
committed by GitHub
9 changed files with 636 additions and 33 deletions
+181 -22
View File
@@ -843,19 +843,38 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
repoRoot,
args.artifactDir ?? path.join("tmp", "dev-cloud-workbench-layout-smoke", layoutArtifactTimestamp())
);
const layoutViewports = workbenchLayoutViewports();
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Workbench layout smoke skipped because Playwright is unavailable: ${error.message}`;
return {
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "skip",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url: useLiveUrl ? args.url : null,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("skip"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [
{
id: "layout-browser-dependency",
@@ -881,8 +900,9 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
artifacts: {
screenshotDir: artifactRoot,
reportPath: args.reportPath ?? null
}
};
},
safety: layoutSafety()
});
}
if (!useLiveUrl && args.build === true) {
@@ -902,11 +922,6 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
let browser;
try {
browser = await chromium.launch({ headless: true });
const layoutViewports = [
{ id: "desktop", width: 1366, height: 768, isMobile: false },
{ id: "narrow-desktop", width: 1024, height: 768, isMobile: false },
{ id: "mobile", width: 390, height: 844, isMobile: true }
];
const viewportResults = {};
for (const viewport of layoutViewports) {
viewportResults[viewport.id] = await inspectWorkbenchLayoutViewport(browser, url, viewport, { artifactRoot });
@@ -1037,13 +1052,28 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
summary: check.summary,
observations: check.observations
}));
return {
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: blockers.length === 0 ? "pass" : "blocked",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, blockers.length === 0 ? "pass" : "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke(blockers.length === 0 ? "pass" : "blocked"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, blockers.length === 0 ? "pass" : "blocked"),
evidenceLevel: useLiveUrl ? blockers.length === 0 ? "DEV-LIVE-LAYOUT" : "BLOCKED" : "SOURCE",
devLive: false,
summary: useLiveUrl
@@ -1063,27 +1093,35 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
]),
reportPath: args.reportPath ?? null
},
safety: {
...staticSafety(),
sourceIsDevLive: false,
layoutOnly: true,
codeAgentPostSent: false,
hardwareWriteApis: false,
hitTestMethod: "document.elementsFromPoint plus normal Playwright clicks without forced clicks",
statement: "Layout smoke opens the workbench, uses real Playwright clicks for sidebar controls, and samples hit targets with elementsFromPoint. It does not send Code Agent chat, call M3 IO, mutate DEV, or claim M3 DEV-LIVE hardware-loop acceptance."
}
};
safety: layoutSafety()
});
} catch (error) {
return {
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "blocked",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("blocked"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary: `Workbench layout smoke failed: ${error.message}`,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [],
blockers: [
{
@@ -1109,14 +1147,56 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
screenshots: [],
reportPath: args.reportPath ?? null
},
safety: staticSafety()
};
safety: layoutSafety()
});
} finally {
if (browser) await browser.close();
if (server) await server.close();
}
}
function workbenchLayoutViewports() {
return [
{ id: "desktop", width: 1366, height: 768, isMobile: false },
{ id: "narrow-desktop", width: 1024, height: 768, isMobile: false },
{ id: "mobile", width: 390, height: 844, isMobile: true }
];
}
function layoutSafety() {
return {
...staticSafety(),
sourceIsDevLive: false,
layoutOnly: true,
codeAgentPostSent: false,
hardwareWriteApis: false,
hitTestMethod: "document.elementsFromPoint plus normal Playwright clicks without forced clicks",
statement: "Layout smoke opens the workbench, uses real Playwright clicks for sidebar controls, and samples hit targets with elementsFromPoint. It does not send Code Agent chat, call M3 IO, mutate DEV, or claim M3 DEV-LIVE hardware-loop acceptance; it must not claim M3 DEV-LIVE hardware-loop acceptance."
};
}
function sanitizeLayoutReport(report) {
return stripLayoutObservationNoise(report);
}
function stripLayoutObservationNoise(value) {
if (Array.isArray(value)) return value.map(stripLayoutObservationNoise);
if (!value || typeof value !== "object") return value;
const cleaned = {};
const looksLikeElementBox =
Number.isFinite(value.left) &&
Number.isFinite(value.top) &&
Number.isFinite(value.right) &&
Number.isFinite(value.bottom) &&
Number.isFinite(value.width) &&
Number.isFinite(value.height);
for (const [key, item] of Object.entries(value)) {
if (looksLikeElementBox && key === "text") continue;
cleaned[key] = stripLayoutObservationNoise(item);
}
return cleaned;
}
function addDomOnlyCodeAgentCheck(checks) {
checks.push({
id: "live-code-agent-browser-journey",
@@ -1152,6 +1232,85 @@ function addDomOnlyCodeAgentCheck(checks) {
});
}
function layoutReportLifecycle(useLiveUrl, status) {
return {
version: "v1",
state: "active",
activeEndpoint: runtime.endpoints.api,
activeBrowserEndpoint: runtime.endpoints.frontend,
deprecatedEndpoint: null,
summary: useLiveUrl
? `DEV live layout smoke ${status}; this is UI layout/clickability evidence only and not M3 hardware-loop acceptance.`
: `Repository source/static layout smoke ${status}; this runs without public DEV dependency and cannot be used as DEV-LIVE evidence.`
};
}
function layoutSourceContract() {
return {
status: "pass",
documents: [
"docs/cloud-web-workbench.md",
"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."
};
}
function layoutValidationCommands() {
return [
"node --check scripts/dev-cloud-workbench-layout-smoke.mjs",
"node --check scripts/src/dev-cloud-workbench-smoke-lib.mjs",
"npm run web:check",
"npm run web:layout",
"npm run web:layout:build",
"npm run web:layout:live"
];
}
function layoutLocalSmoke(status) {
return {
status,
commands: [
"npm run web:check",
"npm run web:layout",
"npm run web:layout:build"
],
evidence: [
"web:check runs the SOURCE/static layout smoke as a repo-owned frontend gate.",
"web:layout runs the source/static Playwright geometry, hit-target, overflow, and outer-scroll checks without public DEV dependency.",
"web:layout:build rebuilds local dist and checks the built Cloud Web assets."
],
summary: "SOURCE/static and local-build layout checks classify UI overlap, covered hit targets, overflow, and outer-scroll regressions as layout blockers."
};
}
function layoutDryRun() {
return {
status: "not_applicable",
commands: ["npm run web:layout"],
evidence: ["Layout smoke is browser evidence, not a dry-run substitute."],
summary: "No dry-run mode is accepted for layout/clickability evidence."
};
}
function layoutDevPreconditions(useLiveUrl, status) {
return {
status: useLiveUrl ? status : "not_applicable",
requirements: [
"SOURCE/static PR checks must pass without public DEV dependency.",
"local-build mode must rebuild and inspect Cloud Web dist before publish/apply.",
"DEV live mode must target http://74.48.78.17:16666/ after a controlled DEV deployment or confirmed live revision.",
"The report must include viewport, selector, failureType, and artifact/report paths for failures.",
"Layout pass is not M3 DEV-LIVE trusted closure and does not replace #227 DO/DI functional evidence."
],
commands: ["npm run web:layout:live"],
summary: useLiveUrl
? "DEV live layout smoke was executed as UI layout/clickability evidence only."
: "DEV live layout smoke is a post-deploy check and is intentionally not required for SOURCE/static gates."
};
}
function baseReport({
mode,
status,