fix(diffs): restore Windows browser discovery roots (#111257)

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
LZY3538
2026-08-04 00:22:24 +08:00
committed by GitHub
parent d6e704621b
commit d5887c110e
2 changed files with 132 additions and 10 deletions
+110
View File
@@ -1,6 +1,7 @@
// Diffs tests cover browser plugin behavior.
import fs from "node:fs/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type {
@@ -48,10 +49,16 @@ describe("PlaywrightDiffScreenshotter", () => {
let rootDir: string;
let outputPath: string;
let cleanupRootDir: () => Promise<void>;
let originalPlatform: PropertyDescriptor;
beforeEach(async () => {
vi.useFakeTimers();
vi.resetModules();
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
if (!platformDescriptor) {
throw new Error("process.platform descriptor is unavailable");
}
originalPlatform = platformDescriptor;
({ PlaywrightDiffScreenshotter } = await import("./browser.js"));
({ rootDir, cleanup: cleanupRootDir } = await createTempDiffRoot("openclaw-diffs-browser-"));
outputPath = path.join(rootDir, "preview.png");
@@ -59,11 +66,114 @@ describe("PlaywrightDiffScreenshotter", () => {
});
afterEach(async () => {
Object.defineProperty(process, "platform", originalPlatform);
vi.unstubAllEnvs();
vi.restoreAllMocks();
await vi.runAllTimersAsync();
vi.useRealTimers();
await cleanupRootDir();
});
async function renderWithBrowserDiscovery(): Promise<{ executablePath?: string }> {
launchMock.mockResolvedValue(createMockBrowser([]));
const screenshotter = new PlaywrightDiffScreenshotter({ config: {}, browserIdleMs: 1_000 });
await screenshotter.screenshotHtml({
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
outputPath,
theme: "dark",
image: {
format: "png",
qualityPreset: "standard",
scale: 1,
maxWidth: 960,
maxPixels: 8_000_000,
},
});
return firstMockCall(launchMock, "browser launch")[0] as { executablePath?: string };
}
function stubWindowsBrowserDiscoveryEnv(params: {
localAppData: string;
programFiles: string;
programFilesX86: string;
}): void {
Object.defineProperty(process, "platform", {
...originalPlatform,
value: "win32",
});
vi.stubEnv("PATH", "");
vi.stubEnv("OPENCLAW_BROWSER_EXECUTABLE_PATH", "");
vi.stubEnv("BROWSER_EXECUTABLE_PATH", "");
vi.stubEnv("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "");
vi.stubEnv("LOCALAPPDATA", params.localAppData);
vi.stubEnv("ProgramFiles", params.programFiles);
vi.stubEnv("ProgramFiles(x86)", params.programFilesX86);
}
it("uses the Windows per-user install root when LOCALAPPDATA is blank", async () => {
stubWindowsBrowserDiscoveryEnv({
localAppData: " \t ",
programFiles: "",
programFilesX86: " ",
});
vi.spyOn(os, "homedir").mockReturnValue("C:\\Users\\test");
const chromePath = "C:\\Users\\test\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe";
const accessMock = vi.spyOn(fs, "access").mockImplementation(async (candidate) => {
if (String(candidate) !== chromePath) {
throw new Error("ENOENT");
}
});
await expect(renderWithBrowserDiscovery()).resolves.toEqual(
expect.objectContaining({ executablePath: chromePath }),
);
expect(accessMock.mock.calls.map(([candidate]) => String(candidate))).toEqual([chromePath]);
});
it("uses standard Windows system roots when install-root overrides are blank", async () => {
stubWindowsBrowserDiscoveryEnv({
localAppData: " ",
programFiles: " \t ",
programFilesX86: "",
});
vi.spyOn(os, "homedir").mockReturnValue("C:\\Users\\test");
const accessMock = vi.spyOn(fs, "access").mockRejectedValue(new Error("ENOENT"));
await expect(renderWithBrowserDiscovery()).resolves.not.toHaveProperty("executablePath");
const candidates = accessMock.mock.calls.map(([candidate]) => String(candidate));
expect(candidates).toEqual([
"C:\\Users\\test\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
"C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
]);
expect(candidates.every((candidate) => path.win32.isAbsolute(candidate))).toBe(true);
});
it("preserves custom Windows install-root precedence", async () => {
stubWindowsBrowserDiscoveryEnv({
localAppData: "D:\\User Apps",
programFiles: "D:\\System Apps",
programFilesX86: "D:\\System Apps x86",
});
const customChromePath = "D:\\User Apps\\Google\\Chrome\\Application\\chrome.exe";
const accessMock = vi.spyOn(fs, "access").mockImplementation(async (candidate) => {
if (String(candidate) !== customChromePath) {
throw new Error("ENOENT");
}
});
await expect(renderWithBrowserDiscovery()).resolves.toEqual(
expect.objectContaining({ executablePath: customChromePath }),
);
expect(accessMock.mock.calls.map(([candidate]) => String(candidate))).toEqual([
customChromePath,
]);
});
it("reuses the same browser across renders and closes it after the idle window", async () => {
const { pages, browser, screenshotter } = await createScreenshotterHarness();
+22 -10
View File
@@ -1,9 +1,11 @@
// Diffs plugin module implements browser behavior.
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { chromium } from "playwright-core";
import type { OpenClawConfig } from "../api.js";
import type { DiffRenderOptions, DiffTheme } from "./types.js";
@@ -520,17 +522,27 @@ function commonExecutablePathsForPlatform(): string[] {
}
if (process.platform === "win32") {
const localAppData = process.env.LOCALAPPDATA ?? "";
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
const joinWindowsPath = path.win32.join;
const localAppData =
normalizeOptionalString(process.env.LOCALAPPDATA) ??
joinWindowsPath(os.homedir(), "AppData", "Local");
const programFiles = normalizeOptionalString(process.env.ProgramFiles) ?? "C:\\Program Files";
const programFilesX86 =
normalizeOptionalString(process.env["ProgramFiles(x86)"]) ?? "C:\\Program Files (x86)";
return [
path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
path.join(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
path.join(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
joinWindowsPath(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
joinWindowsPath(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
joinWindowsPath(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
joinWindowsPath(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
joinWindowsPath(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
joinWindowsPath(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
joinWindowsPath(
programFilesX86,
"BraveSoftware",
"Brave-Browser",
"Application",
"brave.exe",
),
];
}