test: add Cloud Workbench smoke contract
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { gateSummary } from "../../web/hwlab-cloud-web/gate-summary.mjs";
|
||||
import { runtime } from "../../web/hwlab-cloud-web/runtime.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const webRoot = path.join(repoRoot, "web/hwlab-cloud-web");
|
||||
const defaultLiveUrl = "http://74.48.78.17:16666/";
|
||||
const helpOwner = "codex_1779444232735_1";
|
||||
|
||||
const readOnlyRpcMethods = Object.freeze([
|
||||
"system.health",
|
||||
"cloud.adapter.describe",
|
||||
"audit.event.query",
|
||||
"evidence.record.query"
|
||||
]);
|
||||
|
||||
const requiredWebAssets = Object.freeze([
|
||||
"index.html",
|
||||
"styles.css",
|
||||
"app.mjs",
|
||||
"runtime.mjs",
|
||||
"gate-summary.mjs",
|
||||
"workbench-hardware-panel.mjs",
|
||||
"help.md",
|
||||
"third_party/marked/marked.esm.js",
|
||||
"third_party/marked/LICENSE"
|
||||
]);
|
||||
|
||||
const chineseWorkbenchLabels = Object.freeze([
|
||||
"HWLAB 云工作台",
|
||||
"硬件资源",
|
||||
"Agent 对话",
|
||||
"执行轨迹",
|
||||
"可信记录",
|
||||
"使用说明"
|
||||
]);
|
||||
|
||||
const workbenchMarkers = Object.freeze([
|
||||
"data-app-shell",
|
||||
"workbench-shell",
|
||||
"activity-rail",
|
||||
"explorer",
|
||||
"resource-tree",
|
||||
"center-workspace",
|
||||
"conversation-list",
|
||||
"trace-list",
|
||||
"right-sidebar",
|
||||
"hardware-list",
|
||||
"command-form"
|
||||
]);
|
||||
|
||||
const forbiddenWritePatterns = Object.freeze([
|
||||
/callRpc\(\s*["']hardware\./u,
|
||||
/hardware\.operation\.request/u,
|
||||
/hardware\.invoke\.shell/u,
|
||||
/audit\.event\.write/u,
|
||||
/evidence\.record\.write/u,
|
||||
/\/v1\/rpc\//u,
|
||||
/\/wiring\/apply/u,
|
||||
/\/wiring\/reload/u,
|
||||
/--live --confirm-dev --confirmed-non-production/u
|
||||
]);
|
||||
|
||||
const markdownRendererPackages = Object.freeze([
|
||||
"marked",
|
||||
"markdown-it",
|
||||
"micromark",
|
||||
"remark",
|
||||
"remark-parse",
|
||||
"unified",
|
||||
"commonmark"
|
||||
]);
|
||||
|
||||
export function runDevCloudWorkbenchStaticSmoke() {
|
||||
return runStaticSmoke();
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchSmoke(argv = []) {
|
||||
const args = parseSmokeArgs(argv);
|
||||
return args.mode === "live" ? runLiveSmoke(args) : runStaticSmoke();
|
||||
}
|
||||
|
||||
export function parseSmokeArgs(argv) {
|
||||
const args = { mode: "static", url: defaultLiveUrl };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--static") {
|
||||
args.mode = "static";
|
||||
} else if (arg === "--live") {
|
||||
args.mode = "live";
|
||||
} else if (arg === "--url") {
|
||||
index += 1;
|
||||
if (!argv[index]) throw new Error("--url requires a value");
|
||||
args.url = argv[index];
|
||||
} else if (arg === "--help") {
|
||||
args.help = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function runStaticSmoke() {
|
||||
const checks = [];
|
||||
const blockers = [];
|
||||
const files = readStaticFiles();
|
||||
const source = Object.values(files).join("\n");
|
||||
|
||||
addCheck(checks, blockers, "static-web-assets", assetsExist(), "Cloud Web source assets are present.", {
|
||||
evidence: requiredWebAssets.map((file) => `web/hwlab-cloud-web/${file}`)
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "default-workbench-shell", hasDefaultWorkbench(files), "Default route is the VS Code-style Cloud Workbench.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: ["workspace view is visible by default", "Gate view is hidden by default", ...workbenchMarkers]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "secondary-gate-status-help", secondaryViewsAreNotDefault(files), "Gate/status/diagnostics/help surfaces are not accepted as the default homepage.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: ["routeFromLocation final fallback is workspace", "non-workspace data-view sections must be hidden"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "chinese-workbench-labels", labelsPresent(source, chineseWorkbenchLabels), "Current Chinese workbench labels are present.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: chineseWorkbenchLabels
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "outer-scroll-contract", hasScrollLockContract(files.styles), "Outer page scrolling is locked and internal workbench panes own scrolling.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: ["html/body overflow hidden", "workbench-shell 100vh/100dvh overflow hidden", ".view overflow auto"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "same-origin-readonly-boundary", hasSameOriginReadOnlyBoundary(files.app), "Workbench data access stays same-origin and JSON-RPC diagnostics stay read-only.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: ["/health/live", "/v1", "/json-rpc", ...readOnlyRpcMethods]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "no-hardware-write-api", noForbiddenWriteSurface(source), "Workbench source does not expose hardware write APIs or live mutation commands.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: forbiddenWritePatterns.map(String)
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "source-not-dev-live", sourceEvidenceNotDevLive(source), "SOURCE/LOCAL/DRY-RUN/fixture evidence is not promoted to DEV-LIVE.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: ["M3 remains blocked without live loop evidence", "checked-in evidence records remain dry-run"]
|
||||
});
|
||||
|
||||
const help = inspectHelpContract(files);
|
||||
addCheck(checks, blockers, "help-md-contract", help.status, help.summary, {
|
||||
blocker: help.blocker,
|
||||
owner: helpOwner,
|
||||
observations: help.observations
|
||||
});
|
||||
|
||||
return baseReport({
|
||||
mode: "static",
|
||||
status: blockers.length === 0 ? "pass" : "blocked",
|
||||
checks,
|
||||
blockers,
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
help: {
|
||||
status: help.status,
|
||||
...help.observations
|
||||
},
|
||||
safety: staticSafety()
|
||||
});
|
||||
}
|
||||
|
||||
async function runLiveSmoke(args) {
|
||||
const checks = [];
|
||||
const blockers = [];
|
||||
const http = await fetchText(args.url);
|
||||
addCheck(checks, blockers, "live-http-html", http.ok && http.status === 200 && /text\/html/u.test(http.contentType) && liveHtmlLooksLikeWorkbench(http.body), "Live URL serves the Cloud Workbench HTML.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: [args.url, `HTTP ${http.status ?? "none"}`, http.contentType ?? "no content-type"]
|
||||
});
|
||||
|
||||
const dom = http.ok ? await inspectLiveDom(args.url) : { status: "skip", summary: "Browser DOM check skipped because HTTP fetch failed.", evidence: [] };
|
||||
addCheck(checks, blockers, "live-browser-dom", dom.status, dom.summary, {
|
||||
blocker: dom.status === "pass" ? null : "observability_blocker",
|
||||
evidence: dom.evidence,
|
||||
observations: dom.observations
|
||||
});
|
||||
|
||||
if (dom.status === "skip") {
|
||||
blockers.push({
|
||||
type: "observability_blocker",
|
||||
scope: "live-browser-dom",
|
||||
status: "open",
|
||||
summary: dom.summary
|
||||
});
|
||||
}
|
||||
|
||||
const status = blockers.length === 0 ? "pass" : "blocked";
|
||||
return baseReport({
|
||||
mode: "live",
|
||||
url: args.url,
|
||||
status,
|
||||
checks,
|
||||
blockers,
|
||||
evidenceLevel: status === "pass" ? "DEV-LIVE-READONLY" : "BLOCKED",
|
||||
devLive: status === "pass",
|
||||
safety: {
|
||||
prodTouched: false,
|
||||
servicesRestarted: false,
|
||||
heavyE2E: false,
|
||||
hardwareWriteApis: false,
|
||||
sourceIsDevLive: false,
|
||||
liveMode: "read-only-http-and-optional-dom",
|
||||
statement: "Live smoke is read-only and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, help = undefined, safety, url = null }) {
|
||||
return {
|
||||
status,
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
refs: ["pikasTech/HWLAB#108", "pikasTech/HWLAB#99", "pikasTech/HWLAB#78"],
|
||||
mode,
|
||||
url,
|
||||
generatedAt: new Date().toISOString(),
|
||||
evidenceLevel,
|
||||
devLive,
|
||||
endpoints: {
|
||||
frontend: runtime.endpoints.frontend,
|
||||
api: runtime.endpoints.api,
|
||||
edge: runtime.endpoints.edge
|
||||
},
|
||||
checks,
|
||||
blockers,
|
||||
help,
|
||||
safety
|
||||
};
|
||||
}
|
||||
|
||||
function addCheck(checks, blockers, id, result, summary, options = {}) {
|
||||
const status = result === true ? "pass" : result === false ? "blocked" : result;
|
||||
const check = {
|
||||
id,
|
||||
status,
|
||||
summary,
|
||||
...(options.owner ? { owner: options.owner } : {}),
|
||||
...(options.evidence ? { evidence: options.evidence } : {}),
|
||||
...(options.observations ? { observations: options.observations } : {})
|
||||
};
|
||||
checks.push(check);
|
||||
if (status === "blocked" && options.blocker) {
|
||||
blockers.push({
|
||||
type: options.blocker,
|
||||
scope: id,
|
||||
status: "open",
|
||||
summary
|
||||
});
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
function readStaticFiles() {
|
||||
return {
|
||||
html: readText("web/hwlab-cloud-web/index.html"),
|
||||
styles: readText("web/hwlab-cloud-web/styles.css"),
|
||||
app: readText("web/hwlab-cloud-web/app.mjs"),
|
||||
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
|
||||
panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs")
|
||||
};
|
||||
}
|
||||
|
||||
function readText(relativePath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
}
|
||||
|
||||
function assetsExist() {
|
||||
return requiredWebAssets.every((file) => fs.existsSync(path.join(webRoot, file)));
|
||||
}
|
||||
|
||||
function hasDefaultWorkbench({ html, app }) {
|
||||
const workspaceTag = firstTagForDataView(html, "workspace");
|
||||
const gateTag = firstTagForDataView(html, "gate");
|
||||
return (
|
||||
labelsPresent(html, workbenchMarkers) &&
|
||||
/<title>\s*HWLAB 云工作台\s*<\/title>/u.test(html) &&
|
||||
/data-route=["']workspace["']/u.test(html) &&
|
||||
workspaceTag !== null &&
|
||||
!/\bhidden\b/u.test(workspaceTag) &&
|
||||
gateTag !== null &&
|
||||
/\bhidden\b/u.test(gateTag) &&
|
||||
finalRouteFallback(app) === "workspace"
|
||||
);
|
||||
}
|
||||
|
||||
function secondaryViewsAreNotDefault({ html, app }) {
|
||||
const finalFallback = finalRouteFallback(app);
|
||||
const unhiddenSecondaryViews = [...html.matchAll(/<section\b[^>]*\bdata-view=["']([^"']+)["'][^>]*>/gu)]
|
||||
.filter((match) => match[1] !== "workspace")
|
||||
.filter((match) => !/\bhidden\b/u.test(match[0]));
|
||||
const diagnosticsPanel = html.match(/<section\b[^>]*\bdata-side-panel=["']diagnostics["'][^>]*>/u)?.[0] ?? "";
|
||||
return finalFallback === "workspace" && unhiddenSecondaryViews.length === 0 && /\bhidden\b/u.test(diagnosticsPanel);
|
||||
}
|
||||
|
||||
function firstTagForDataView(html, view) {
|
||||
return html.match(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(view)}["'][^>]*>`, "u"))?.[0] ?? null;
|
||||
}
|
||||
|
||||
function finalRouteFallback(app) {
|
||||
const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? "";
|
||||
const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]);
|
||||
return returns.at(-1) ?? null;
|
||||
}
|
||||
|
||||
function labelsPresent(source, labels) {
|
||||
return labels.every((label) => source.includes(label));
|
||||
}
|
||||
|
||||
function hasScrollLockContract(styles) {
|
||||
return (
|
||||
/html,\s*\nbody\s*\{[^\}]*height:\s*100%;[^\}]*overflow:\s*hidden;/su.test(styles) &&
|
||||
/body\s*>\s*\[data-app-shell\]\s*\{[^\}]*min-height:\s*0;/su.test(styles) &&
|
||||
/\.workbench-shell\s*\{[^\}]*height:\s*100(?:d)?vh;[^\}]*overflow:\s*hidden;/su.test(styles) &&
|
||||
/\.view\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) &&
|
||||
/\.(?:resource-tree|compact-list|conversation-list|trace-list|hardware-list)[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles)
|
||||
);
|
||||
}
|
||||
|
||||
function hasSameOriginReadOnlyBoundary(app) {
|
||||
const rpcCalls = [...app.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]).sort();
|
||||
return (
|
||||
/fetchJson\("\/health\/live"\)/u.test(app) &&
|
||||
/fetchJson\("\/v1"\)/u.test(app) &&
|
||||
/fetchJson\("\/json-rpc"/u.test(app) &&
|
||||
JSON.stringify(rpcCalls) === JSON.stringify([...readOnlyRpcMethods].sort()) &&
|
||||
!/fetchJson\(\s*["']https?:\/\//u.test(app)
|
||||
);
|
||||
}
|
||||
|
||||
function noForbiddenWriteSurface(source) {
|
||||
return forbiddenWritePatterns.every((pattern) => !pattern.test(source));
|
||||
}
|
||||
|
||||
function sourceEvidenceNotDevLive(source) {
|
||||
const m3 = gateSummary.milestones.find((item) => item.id === "M3");
|
||||
return (
|
||||
m3?.status === "blocked" &&
|
||||
gateSummary.evidenceRecords.every((record) => record.dryRun === true) &&
|
||||
gateSummary.safety.allowNetwork === false &&
|
||||
["来源 SOURCE", "演练 DRY-RUN", "开发实况 DEV-LIVE", "阻塞 BLOCKED"].every((label) => source.includes(label))
|
||||
);
|
||||
}
|
||||
|
||||
function inspectHelpContract({ html, app }) {
|
||||
const markdownFiles = findHelpMarkdownFiles();
|
||||
const rendererPackages = findMarkdownRenderers();
|
||||
const route = inspectHelpRoute(html, app);
|
||||
const implemented = markdownFiles.includes("web/hwlab-cloud-web/help.md") && rendererPackages.length > 0 && route.nonDefaultRoute;
|
||||
const status = route.defaultRouteRisk ? "blocked" : implemented ? "pass" : "pending";
|
||||
return {
|
||||
status,
|
||||
blocker: route.defaultRouteRisk ? "runtime_blocker" : null,
|
||||
summary: implemented ? "Markdown help route from PR #114 is present and remains non-default." : `Markdown help page contract is pending; owner=${helpOwner}.`,
|
||||
observations: {
|
||||
owner: helpOwner,
|
||||
markdownHelpFilesPresent: markdownFiles.length > 0,
|
||||
markdownHelpFiles: markdownFiles,
|
||||
matureMarkdownRendererPresent: rendererPackages.length > 0,
|
||||
markdownRenderers: rendererPackages,
|
||||
nonDefaultRoute: route.nonDefaultRoute,
|
||||
implemented,
|
||||
route
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function findHelpMarkdownFiles() {
|
||||
const files = [
|
||||
"web/hwlab-cloud-web/help.md",
|
||||
"web/hwlab-cloud-web/help/README.md",
|
||||
"web/hwlab-cloud-web/help/index.md",
|
||||
"docs/dev-cloud-workbench-help.md",
|
||||
"docs/cloud-web-workbench.md"
|
||||
];
|
||||
return files.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
||||
}
|
||||
|
||||
function findMarkdownRenderers() {
|
||||
const packages = ["package.json", "web/hwlab-cloud-web/package.json"]
|
||||
.filter((file) => fs.existsSync(path.join(repoRoot, file)))
|
||||
.flatMap((file) => {
|
||||
const json = JSON.parse(readText(file));
|
||||
return Object.keys({ ...(json.dependencies ?? {}), ...(json.devDependencies ?? {}) });
|
||||
});
|
||||
const source = `${readText("web/hwlab-cloud-web/app.mjs")}\n${readText("web/hwlab-cloud-web/index.html")}`;
|
||||
const imported = markdownRendererPackages.filter((name) => new RegExp(`from\\s+["']${escapeRegExp(name)}["']|import\\(["']${escapeRegExp(name)}["']\\)`, "u").test(source));
|
||||
const vendoredMarked = /from\s+["']\.\/third_party\/marked\/marked\.esm\.js["']/u.test(source) &&
|
||||
fs.existsSync(path.join(repoRoot, "web/hwlab-cloud-web/third_party/marked/marked.esm.js")) &&
|
||||
fs.existsSync(path.join(repoRoot, "web/hwlab-cloud-web/third_party/marked/LICENSE"));
|
||||
const renderers = [...packages, ...imported].filter((name) => markdownRendererPackages.includes(name));
|
||||
if (vendoredMarked) renderers.push("marked:vendored");
|
||||
return [...new Set(renderers)].sort();
|
||||
}
|
||||
|
||||
function inspectHelpRoute(html, app) {
|
||||
const helpViewTag = firstTagForDataView(html, "help");
|
||||
const explicitRoutePresent =
|
||||
/data-route=["']help["']/u.test(html) ||
|
||||
/data-view=["']help["']/u.test(html) ||
|
||||
/#help\b/u.test(html + app) ||
|
||||
/pathname[\s\S]{0,160}\/help/u.test(app);
|
||||
const finalFallback = finalRouteFallback(app);
|
||||
const helpViewHidden = helpViewTag ? /\bhidden\b/u.test(helpViewTag) : false;
|
||||
return {
|
||||
explicitRoutePresent,
|
||||
finalFallback,
|
||||
helpViewHidden,
|
||||
routePolicy: html.match(/\bdata-help-route-policy=["']([^"']+)["']/u)?.[1] ?? null,
|
||||
nonDefaultRoute:
|
||||
explicitRoutePresent &&
|
||||
finalFallback === "workspace" &&
|
||||
(helpViewTag ? helpViewHidden : true) &&
|
||||
/data-help-route-policy=["']non-default-internal-help["']/u.test(html),
|
||||
defaultRouteRisk: finalFallback === "help" || (helpViewTag !== null && !helpViewHidden)
|
||||
};
|
||||
}
|
||||
|
||||
function staticSafety() {
|
||||
return {
|
||||
prodTouched: false,
|
||||
servicesRestarted: false,
|
||||
heavyE2E: false,
|
||||
hardwareWriteApis: false,
|
||||
devLive: false,
|
||||
sourceIsDevLive: false,
|
||||
statement: "Static repository/source evidence is not DEV-LIVE and cannot promote SOURCE, LOCAL, DRY-RUN, or fixture observations."
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchText(url) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 6000);
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal, headers: { Accept: "text/html" } });
|
||||
const body = await response.text();
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
contentType: response.headers.get("content-type") ?? "",
|
||||
body
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: null,
|
||||
contentType: "",
|
||||
body: "",
|
||||
error: error.name === "AbortError" ? "timeout" : error.message
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function liveHtmlLooksLikeWorkbench(html) {
|
||||
return /HWLAB 云工作台/u.test(html) && labelsPresent(html, workbenchMarkers) && !/<body[^>]*>\s*<(?:main|section)[^>]*(?:gate|diagnostics|status|help)/iu.test(html);
|
||||
}
|
||||
|
||||
async function inspectLiveDom(url) {
|
||||
let chromium;
|
||||
try {
|
||||
({ chromium } = await import("playwright"));
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "skip",
|
||||
summary: `Browser DOM check skipped because Playwright is unavailable: ${error.message}`,
|
||||
evidence: ["playwright import failed"]
|
||||
};
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
|
||||
const dom = await page.evaluate(() => {
|
||||
const workspace = document.querySelector('[data-view="workspace"]');
|
||||
const gate = document.querySelector('[data-view="gate"]');
|
||||
const diagnostics = document.querySelector('[data-side-panel="diagnostics"]');
|
||||
const bodyStyle = getComputedStyle(document.body);
|
||||
const htmlStyle = getComputedStyle(document.documentElement);
|
||||
const text = document.body.textContent ?? "";
|
||||
return {
|
||||
title: document.title,
|
||||
bodyOverflow: bodyStyle.overflow,
|
||||
htmlOverflow: htmlStyle.overflow,
|
||||
workspaceHidden: workspace ? workspace.hidden : null,
|
||||
gateHidden: gate ? gate.hidden : null,
|
||||
diagnosticsHidden: diagnostics ? diagnostics.hidden : null,
|
||||
outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2,
|
||||
labelsPresent: ["硬件资源", "Agent 对话", "执行轨迹", "可信记录", "使用说明"].every((label) => text.includes(label))
|
||||
};
|
||||
});
|
||||
const pass =
|
||||
dom.title === "HWLAB 云工作台" &&
|
||||
dom.bodyOverflow === "hidden" &&
|
||||
dom.htmlOverflow === "hidden" &&
|
||||
dom.workspaceHidden === false &&
|
||||
dom.gateHidden === true &&
|
||||
dom.diagnosticsHidden === true &&
|
||||
dom.outerScrollLocked &&
|
||||
dom.labelsPresent;
|
||||
return {
|
||||
status: pass ? "pass" : "blocked",
|
||||
summary: pass ? "Browser DOM confirms the default workbench and outer scroll lock." : "Browser DOM did not satisfy the workbench contract.",
|
||||
evidence: [`title=${dom.title}`, `bodyOverflow=${dom.bodyOverflow}`, `htmlOverflow=${dom.htmlOverflow}`],
|
||||
observations: dom
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "skip",
|
||||
summary: `Browser DOM check skipped because the browser could not run: ${error.message}`,
|
||||
evidence: ["browser launch or navigation failed"]
|
||||
};
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
export function printSmokeHelp() {
|
||||
return {
|
||||
status: "usage",
|
||||
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --live --url http://74.48.78.17:16666/",
|
||||
notes: [
|
||||
"Static mode reads repository files and emits SOURCE-level evidence only.",
|
||||
"Live mode is read-only and reports blocked/skip when optional browser DOM evidence is unavailable."
|
||||
]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user