fix(plugins): normalize windows override imports

This commit is contained in:
Peter Steinberger
2026-04-27 08:35:16 +01:00
parent 4514a73170
commit 15e634d50c
7 changed files with 148 additions and 8 deletions

View File

@@ -93,7 +93,9 @@ GitHub-native replacement for most Parallels package/update validation, with
Telegram proving the same package artifact through the QA live transport.
Cross-OS release checks still cover OS-specific onboarding, installer, and
platform behavior; package/update product validation should start with Package
Acceptance.
Acceptance. The Windows packaged and installer fresh lanes also verify that an
installed package can import a browser-control override from a raw absolute
Windows path.
Package Acceptance has a bounded legacy-compatibility window for already
published packages through `2026.4.25`, including `2026.4.25-beta.*`. Those

View File

@@ -566,6 +566,17 @@ async function runFreshLane(params) {
logPath: join(params.logsDir, "fresh-install.log"),
});
let browserOverrideImportStatus = "skipped";
if (shouldRunWindowsInstalledBrowserOverrideImportSmoke()) {
logLanePhase(lane, "windows-browser-override-import");
browserOverrideImportStatus = await runInstalledBrowserOverrideImportSmoke({
lane,
env,
prefixDir: lane.prefixDir,
logPath: join(params.logsDir, "fresh-windows-browser-override-import.log"),
});
}
logLanePhase(lane, "onboard");
await runOnboard({
lane,
@@ -617,6 +628,7 @@ async function runFreshLane(params) {
installedCommit: installed.commit,
dashboardStatus: "pass",
gatewayPort: lane.gatewayPort,
browserOverrideImportStatus,
agentOutput: trimForSummary(agent.stdout),
};
} finally {
@@ -795,6 +807,17 @@ async function runInstallerFreshSuite(params) {
const installed = readInstalledMetadataFromCliPath(freshShell.cliPath);
verifyInstalledCandidate(installed, params.build);
let browserOverrideImportStatus = "skipped";
if (shouldRunWindowsInstalledBrowserOverrideImportSmoke()) {
logLanePhase(lane, "windows-browser-override-import");
browserOverrideImportStatus = await runInstalledBrowserOverrideImportSmoke({
lane,
env,
prefixDir: resolveInstalledPrefixDirFromCliPath(freshShell.cliPath),
logPath: join(params.logsDir, "installer-fresh-windows-browser-override-import.log"),
});
}
logLanePhase(lane, "onboard");
await runOnboardWithInstalledCli({
lane,
@@ -900,6 +923,7 @@ async function runInstallerFreshSuite(params) {
installedCommit: installed.commit,
gatewayPort: lane.gatewayPort,
dashboardStatus: "pass",
browserOverrideImportStatus,
discordStatus,
agentOutput: trimForSummary(agent.stdout),
};
@@ -2200,6 +2224,100 @@ async function runBundledPluginPostinstall(params) {
});
}
export function shouldRunWindowsInstalledBrowserOverrideImportSmoke(platform = process.platform) {
return platform === "win32";
}
export function buildInstalledBrowserOverrideImportProbeScript() {
return `
import { existsSync } from "node:fs";
import { startLazyPluginServiceModule } from "openclaw/plugin-sdk/browser-node-runtime";
const startedPath = process.env.OPENCLAW_BROWSER_OVERRIDE_STARTED_PATH;
const stoppedPath = process.env.OPENCLAW_BROWSER_OVERRIDE_STOPPED_PATH;
if (!process.env.OPENCLAW_BROWSER_CONTROL_MODULE) {
throw new Error("Missing OPENCLAW_BROWSER_CONTROL_MODULE.");
}
if (!startedPath || !stoppedPath) {
throw new Error("Missing browser override sentinel path env.");
}
const handle = await startLazyPluginServiceModule({
overrideEnvVar: "OPENCLAW_BROWSER_CONTROL_MODULE",
validateOverrideSpecifier: (specifier) => specifier,
loadDefaultModule: async () => {
throw new Error("Default browser control service should not load during override probe.");
},
startExportNames: ["startBrowserControlService"],
stopExportNames: ["stopBrowserControlService"],
});
if (!handle) {
throw new Error("Browser control override probe did not return a service handle.");
}
if (!existsSync(startedPath)) {
throw new Error("Browser control override start sentinel was not written.");
}
await handle.stop();
if (!existsSync(stoppedPath)) {
throw new Error("Browser control override stop sentinel was not written.");
}
console.log("windows browser override import OK");
`.trim();
}
function buildBrowserOverrideProbeServiceModule() {
return `
import { writeFileSync } from "node:fs";
export async function startBrowserControlService() {
writeFileSync(process.env.OPENCLAW_BROWSER_OVERRIDE_STARTED_PATH, "started\\n", "utf8");
}
export async function stopBrowserControlService() {
writeFileSync(process.env.OPENCLAW_BROWSER_OVERRIDE_STOPPED_PATH, "stopped\\n", "utf8");
}
`.trim();
}
async function runInstalledBrowserOverrideImportSmoke(params) {
if (!shouldRunWindowsInstalledBrowserOverrideImportSmoke()) {
return "skipped";
}
const probeDir = join(params.lane.rootDir, "browser override import probe");
mkdirSync(probeDir, { recursive: true });
const overridePath = join(probeDir, "browser override #module.mjs");
const probePath = join(probeDir, "run browser override probe.mjs");
const startedPath = join(probeDir, "started.txt");
const stoppedPath = join(probeDir, "stopped.txt");
writeFileSync(overridePath, `${buildBrowserOverrideProbeServiceModule()}\n`, "utf8");
writeFileSync(probePath, `${buildInstalledBrowserOverrideImportProbeScript()}\n`, "utf8");
await runCommand(process.execPath, [probePath], {
cwd: installedPackageRoot(params.prefixDir),
env: {
...params.env,
OPENCLAW_BROWSER_CONTROL_MODULE: overridePath,
OPENCLAW_BROWSER_OVERRIDE_STARTED_PATH: startedPath,
OPENCLAW_BROWSER_OVERRIDE_STOPPED_PATH: stoppedPath,
},
logPath: params.logPath,
timeoutMs: 60_000,
});
if (!existsSync(startedPath) || !existsSync(stoppedPath)) {
throw new Error("Browser control override import probe did not write both sentinels.");
}
return "pass";
}
function ensureLocalNpmShim(lane) {
const shimPath = npmShimPath(lane.prefixDir);
if (existsSync(shimPath)) {

View File

@@ -12,6 +12,9 @@ describe("toSafeImportPath", () => {
expect(toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe(
"file:///C:/Users/alice/plugin/index.mjs",
);
expect(toSafeImportPath("C:\\Users\\alice\\plugin folder\\x#y.mjs")).toBe(
"file:///C:/Users/alice/plugin%20folder/x%23y.mjs",
);
expect(toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe(
"file://server/share/plugin/index.mjs",
);

View File

@@ -1,4 +1,5 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
/**
* On Windows, Node's ESM loader requires absolute paths to be expressed as
@@ -12,11 +13,7 @@ export function toSafeImportPath(specifier: string): string {
return specifier;
}
if (path.win32.isAbsolute(specifier)) {
const normalizedSpecifier = specifier.replaceAll("\\", "/");
if (normalizedSpecifier.startsWith("//")) {
return new URL(`file:${encodeURI(normalizedSpecifier)}`).href;
}
return new URL(`file:///${encodeURI(normalizedSpecifier)}`).href;
return pathToFileURL(specifier, { windows: true }).href;
}
return specifier;
}

View File

@@ -95,12 +95,14 @@ describe("startLazyPluginServiceModule", () => {
const importModule = vi.fn(async () => ({ startOverride: start }));
try {
await defaultLoadOverrideModule("C:\\Users\\alice\\browser-service.mjs", importModule);
await defaultLoadOverrideModule("C:\\Users\\alice\\plugin folder\\x#y.mjs", importModule);
} finally {
platformSpy.mockRestore();
}
expect(importModule).toHaveBeenCalledWith("file:///C:/Users/alice/browser-service.mjs");
expect(importModule).toHaveBeenCalledWith(
"file:///C:/Users/alice/plugin%20folder/x%23y.mjs",
);
});
it("leaves caller-supplied override loaders responsible for their own specifiers", async () => {

View File

@@ -7338,6 +7338,9 @@ export const runtimeValue = helperValue;`,
expect(__testing.toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe(
"file:///C:/Users/alice/plugin/index.mjs",
);
expect(__testing.toSafeImportPath("C:\\Users\\alice\\plugin folder\\x#y.mjs")).toBe(
"file:///C:/Users/alice/plugin%20folder/x%23y.mjs",
);
expect(__testing.toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe(
"file://server/share/plugin/index.mjs",
);

View File

@@ -8,6 +8,7 @@ import {
agentOutputHasExpectedOkMarker,
buildWindowsDevUpdateToolchainCheckScript,
buildWindowsFreshShellVersionCheckScript,
buildInstalledBrowserOverrideImportProbeScript,
buildWindowsPathBootstrapScript,
canConnectToLoopbackPort,
buildDiscordSmokeGuildsConfig,
@@ -35,6 +36,7 @@ import {
resolveRunnerMatrix,
resolveStaticFileContentType,
shouldExerciseManagedGatewayLifecycleAfterInstall,
shouldRunWindowsInstalledBrowserOverrideImportSmoke,
shouldSkipInstallerDaemonHealthCheck,
shouldStopManagedGatewayBeforeManualFallback,
shouldRunMainChannelDevUpdate,
@@ -289,6 +291,19 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
expect(shouldSkipInstallerDaemonHealthCheck("linux")).toBe(false);
});
it("runs the installed browser override import smoke only on native Windows", () => {
expect(shouldRunWindowsInstalledBrowserOverrideImportSmoke("win32")).toBe(true);
expect(shouldRunWindowsInstalledBrowserOverrideImportSmoke("darwin")).toBe(false);
expect(shouldRunWindowsInstalledBrowserOverrideImportSmoke("linux")).toBe(false);
const script = buildInstalledBrowserOverrideImportProbeScript();
expect(script).toContain('from "openclaw/plugin-sdk/browser-node-runtime"');
expect(script).toContain('overrideEnvVar: "OPENCLAW_BROWSER_CONTROL_MODULE"');
expect(script).toContain("startBrowserControlService");
expect(script).toContain("stopBrowserControlService");
expect(script).toContain("Browser control override start sentinel was not written.");
});
it("normalizes Windows installed CLI paths to the cmd shim", () => {
expect(
normalizeWindowsInstalledCliPath(