fix(browser): resolve chrome-extension bundled dir from built package layout (#126279)

The browser doctor computed BROWSER_PLUGIN_ROOT by walking one directory
up from the source file. In a source checkout the file lives in
extensions/browser/src/ and chrome-extension assets are at
extensions/browser/chrome-extension, so this worked. In a built package the
compiled artifact lives at the package root (dist/extensions/browser/) and
assets are at dist/extensions/browser/chrome-extension, so the old heuristic
looked for dist/extensions/chrome-extension and failed with ENOENT.

Resolve the package root by searching for package.json, which exists in both
layouts, and fall back to the previous heuristic only when it is absent.

Fixes the doctor report: 'Chrome extension bootstrap status could not be
inspected: ENOENT ... realpath /dist/extensions/chrome-extension'.
This commit is contained in:
施清荃
2026-08-20 16:50:26 +08:00
committed by GitHub
parent 3b6fc03e02
commit a2fe7c395d
2 changed files with 59 additions and 2 deletions
+55 -1
View File
@@ -1,10 +1,15 @@
// Browser tests cover doctor browser plugin behavior.
import { describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../test-support.js";
import {
maybeArchiveLegacyClawdBrowserProfileResidue,
noteChromeMcpBrowserReadiness,
} from "./doctor-browser.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function requireFirstNoteText(noteFn: ReturnType<typeof vi.fn>): string {
const [call] = noteFn.mock.calls;
if (!call) {
@@ -301,6 +306,55 @@ describe("browser doctor readiness", () => {
});
});
describe("browser plugin package layout", () => {
async function expectRepairLayout(layout: "source" | "built") {
const packageRoot = fs.realpathSync(tempDirs.make("openclaw-browser-doctor-"));
const moduleDir = layout === "source" ? path.join(packageRoot, "src") : packageRoot;
const modulePath = path.join(
moduleDir,
layout === "source" ? "doctor-browser.ts" : "browser-doctor.js",
);
fs.mkdirSync(moduleDir, { recursive: true });
fs.writeFileSync(path.join(packageRoot, "package.json"), "{}");
const repairOwnedChromeExtensionNativeHosts = vi.fn(async () => ({
changes: [],
warnings: [],
}));
vi.resetModules();
vi.doMock("node:url", async () => ({
...(await vi.importActual<typeof import("node:url")>("node:url")),
fileURLToPath: () => modulePath,
}));
vi.doMock("./browser/extension-install.js", () => ({
browserExtensionStatus: vi.fn(),
FOUNDATION_CHROME_WEB_STORE_URL: "https://example.invalid",
repairOwnedChromeExtensionNativeHosts,
}));
try {
const { maybeRepairOwnedChromeExtensionNativeHosts } = await import("./doctor-browser.js");
await maybeRepairOwnedChromeExtensionNativeHosts();
expect(repairOwnedChromeExtensionNativeHosts).toHaveBeenCalledWith({
bundledDir: path.join(packageRoot, "chrome-extension"),
pluginRoot: packageRoot,
});
} finally {
vi.doUnmock("node:url");
vi.doUnmock("./browser/extension-install.js");
vi.resetModules();
}
}
it("resolves assets from the source package root", async () => {
await expectRepairLayout("source");
});
it("resolves assets from the built package root", async () => {
await expectRepairLayout("built");
});
});
describe("legacy clawd browser profile cleanup", () => {
it("archives stale clawd residue with the safe trash mover", async () => {
const movePathToTrash = vi.fn(async () => "/tmp/openclaw-home/browser/.trash/clawd");
+4 -1
View File
@@ -35,7 +35,10 @@ const REMOTE_DEBUGGING_PAGES = [
"brave://inspect/#remote-debugging",
"edge://inspect/#remote-debugging",
].join(", ");
const BROWSER_PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const BROWSER_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
const BROWSER_PLUGIN_ROOT = fs.existsSync(path.join(BROWSER_MODULE_DIR, "package.json"))
? BROWSER_MODULE_DIR
: path.dirname(BROWSER_MODULE_DIR);
const BUNDLED_CHROME_EXTENSION_DIR = path.join(BROWSER_PLUGIN_ROOT, "chrome-extension");
type ExistingSessionProfile = {