From 48e078e55c3ed5ad2b4e8dc8fadfff0f130a5e03 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 04:53:56 +0000 Subject: [PATCH] fix: harden workbench smoke dependencies --- scripts/dev-artifact-publish.mjs | 2 + scripts/src/dev-cloud-workbench-smoke-lib.mjs | 106 +++++++++++++++--- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index 827102d3..6305a3d8 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -365,6 +365,8 @@ function copyHeaders(headers) { const result = {}; for (const [key, value] of Object.entries(headers)) { if (value === undefined) continue; + if (key.toLowerCase() === "transfer-encoding") continue; + if (key.toLowerCase() === "content-length") continue; result[key] = value; } return result; diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index db1f83a1..7be56d96 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import http from "node:http"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { gateSummary } from "../../web/hwlab-cloud-web/gate-summary.mjs"; import { runtime } from "../../web/hwlab-cloud-web/runtime.mjs"; @@ -172,6 +172,10 @@ const markdownRendererPackages = Object.freeze([ "commonmark" ]); +const playwrightDependencyCommand = "npm install"; +const playwrightDependencySummary = + "Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally."; + export function runDevCloudWorkbenchStaticSmoke() { return runStaticSmoke(); } @@ -194,6 +198,40 @@ export async function runDevCloudWorkbenchSmoke(argv = []) { return runStaticSmoke(); } +async function importPlaywright() { + try { + return normalizePlaywrightModule(await import("playwright")); + } catch (localError) { + const globalRoot = npmGlobalRoot(); + if (globalRoot) { + try { + return normalizePlaywrightModule(await import(pathToFileURL(path.join(globalRoot, "playwright/index.js")).href)); + } catch { + // Fall through to the original local resolution error so the remediation points at package.json. + } + } + throw localError; + } +} + +function normalizePlaywrightModule(module) { + if (module?.chromium) return module; + if (module?.default?.chromium) return module.default; + return module; +} + +function npmGlobalRoot() { + try { + return execFileSync("npm", ["root", "-g"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5000 + }).trim(); + } catch { + return ""; + } +} + export function parseSmokeArgs(argv) { const args = { mode: "static", url: defaultLiveUrl }; for (let index = 0; index < argv.length; index += 1) { @@ -1698,7 +1736,7 @@ function liveHtmlLooksLikeWorkbench(html) { async function inspectLiveDom(url) { let chromium; try { - ({ chromium } = await import("playwright")); + ({ chromium } = await importPlaywright()); } catch (error) { return { status: "skip", @@ -1792,7 +1830,7 @@ async function inspectLiveDom(url) { async function inspectLiveHelpRoute(url) { let chromium; try { - ({ chromium } = await import("playwright")); + ({ chromium } = await importPlaywright()); } catch (error) { return { status: "skip", @@ -1837,7 +1875,7 @@ async function inspectLiveHelpRoute(url) { async function inspectLiveUserJourney(url) { let chromium; try { - ({ chromium } = await import("playwright")); + ({ chromium } = await importPlaywright()); } catch (error) { return { status: "skip", @@ -2054,18 +2092,38 @@ async function runLocalAgentFixtureSmoke({ } = {}) { let chromium; try { - ({ chromium } = await import("playwright")); + ({ chromium } = await importPlaywright()); } catch (error) { + const summary = `Local Code Agent browser fixture smoke skipped because Playwright is unavailable: ${error.message}`; return { status: "skip", task: "DC-DCSN-P0-2026-003", mode, evidenceLevel: "SOURCE", devLive: false, - summary: `Local Code Agent browser fixture smoke skipped because Playwright is unavailable: ${error.message}`, - checks: [], - blockers: [], - safety: staticSafety() + summary, + checks: [ + { + id: "local-agent-fixture-browser-dependency", + status: "skip", + summary: playwrightDependencySummary, + evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"] + } + ], + blockers: [ + { + type: "environment_blocker", + scope: "local-agent-fixture-browser-dependency", + status: "open", + summary + } + ], + safety: staticSafety(), + dependency: { + package: "playwright", + declaredIn: "package.json", + installCommand: playwrightDependencyCommand + } }; } @@ -2240,8 +2298,9 @@ async function runLocalAgentFixtureSmoke({ export async function runDevCloudWorkbenchMobileSmoke() { let chromium; try { - ({ chromium } = await import("playwright")); + ({ chromium } = await importPlaywright()); } catch (error) { + const summary = `Mobile browser smoke skipped because Playwright is unavailable: ${error.message}`; return { status: "skip", task: "DC-DCSN-P0-2026-003", @@ -2249,10 +2308,29 @@ export async function runDevCloudWorkbenchMobileSmoke() { viewport: { width: 390, height: 844 }, evidenceLevel: "SOURCE", devLive: false, - summary: `Mobile browser smoke skipped because Playwright is unavailable: ${error.message}`, - checks: [], - blockers: [], - safety: staticSafety() + summary, + checks: [ + { + id: "mobile-browser-dependency", + status: "skip", + summary: playwrightDependencySummary, + evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"] + } + ], + blockers: [ + { + type: "environment_blocker", + scope: "mobile-browser-dependency", + status: "open", + summary + } + ], + safety: staticSafety(), + dependency: { + package: "playwright", + declaredIn: "package.json", + installCommand: playwrightDependencyCommand + } }; }