mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(browser): warn when Chrome extension version drifts after upgrades (#119641)
* fix(browser): detect Chrome extension version drift * fix(browser): diagnose paired Chrome extension version drift Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -387,6 +387,10 @@ openclaw doctor
|
||||
|
||||
Migrates config, audits DM policies, and checks gateway health. Details: [Doctor](/gateway/doctor)
|
||||
|
||||
If you use the unpacked Chrome extension, also run `openclaw browser doctor --browser-profile chrome`.
|
||||
For a version-mismatch warning, reload the extension from `chrome://extensions`;
|
||||
fully restart Chrome if the warning remains.
|
||||
|
||||
### Restart the gateway
|
||||
|
||||
```bash
|
||||
|
||||
@@ -301,6 +301,9 @@ openclaw doctor
|
||||
development fallback after the command says native bootstrap is ready.
|
||||
- **Extension was loaded before native setup:** restart Chrome once to clear its
|
||||
cached native-host miss, then rerun the ordered install flow.
|
||||
- **Extension version mismatch:** reload the unpacked OpenClaw extension from
|
||||
`chrome://extensions`, then rerun browser doctor. Fully restart Chrome if the
|
||||
running and bundled versions still differ.
|
||||
- **Waiting for local OpenClaw:** run `extension status`; install or repair the
|
||||
owned native host.
|
||||
- **Automatic setup disabled:** enable it in Settings or click **Use local
|
||||
|
||||
@@ -19,6 +19,7 @@ import { createBrowserRouteDispatcher } from "../src/browser/routes/dispatcher.j
|
||||
import { createBrowserRouteContext } from "../src/browser/server-context.js";
|
||||
import { getFreePort } from "../src/browser/test-port.js";
|
||||
import { getBrowserControlState, stopBrowserControlService } from "../src/control-service.js";
|
||||
import chromeExtensionManifest from "./manifest.json" with { type: "json" };
|
||||
import { relayTestKey } from "./relay-key.test-support.js";
|
||||
|
||||
declare const chrome: {
|
||||
@@ -387,6 +388,24 @@ describe.runIf(runE2E)("Chrome native bootstrap Chromium E2E", () => {
|
||||
refreshConfigFromDisk: false,
|
||||
});
|
||||
const dispatcher = createBrowserRouteDispatcher(routeContext);
|
||||
const matchingDoctor = await dispatcher.dispatch({
|
||||
method: "GET",
|
||||
path: "/doctor",
|
||||
query: { profile: "e2e" },
|
||||
});
|
||||
expect(matchingDoctor.status).toBe(200);
|
||||
expect(matchingDoctor.body).toMatchObject({
|
||||
checks: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "extension-version",
|
||||
status: "pass",
|
||||
summary: `running ${chromeExtensionManifest.version}; bundled ${chromeExtensionManifest.version} (match)`,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
process.stderr.write(
|
||||
`[browser-extension-e2e] doctor version match ${chromeExtensionManifest.version}\n`,
|
||||
);
|
||||
const tabsResponse = await dispatcher.dispatch({
|
||||
method: "GET",
|
||||
path: "/tabs",
|
||||
@@ -632,8 +651,45 @@ describe.runIf(runE2E)("Chrome native bootstrap Chromium E2E", () => {
|
||||
.poll(() => relay.bridge.accessibleTabs().some((tab) => tab.tabId === tabId))
|
||||
.toBe(false);
|
||||
|
||||
const installedManifestPath = path.join(installed, "manifest.json");
|
||||
const installedManifest = JSON.parse(await fs.readFile(installedManifestPath, "utf8")) as {
|
||||
version: string;
|
||||
};
|
||||
const outdatedVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0";
|
||||
await fs.writeFile(
|
||||
installedManifestPath,
|
||||
`${JSON.stringify({ ...installedManifest, version: outdatedVersion }, null, 2)}\n`,
|
||||
);
|
||||
await context.close();
|
||||
context = await launchChromium();
|
||||
await loadUnpackedExtension(context, installed);
|
||||
expect(await waitForExtensionId(context, installed)).toBe(extensionId);
|
||||
const outdatedExtensionPage = await context.newPage();
|
||||
await outdatedExtensionPage.goto(`chrome-extension://${extensionId}/options.html`);
|
||||
await expect.poll(() => relay.bridge.identity?.extensionVersion).toBe(outdatedVersion);
|
||||
const outdatedDoctor = await dispatcher.dispatch({
|
||||
method: "GET",
|
||||
path: "/doctor",
|
||||
query: { profile: "e2e" },
|
||||
});
|
||||
expect(outdatedDoctor.status).toBe(200);
|
||||
expect(outdatedDoctor.body).toMatchObject({
|
||||
ok: true,
|
||||
checks: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "extension-version",
|
||||
status: "warn",
|
||||
summary: `running ${outdatedVersion}; bundled ${chromeExtensionManifest.version} (mismatch)`,
|
||||
fixHint: expect.stringMatching(/reload/i),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
process.stderr.write(
|
||||
`[browser-extension-e2e] doctor version mismatch running=${outdatedVersion} bundled=${chromeExtensionManifest.version} status=WARN\n`,
|
||||
);
|
||||
|
||||
const extensionContext = routeContext.forProfile("e2e");
|
||||
await extensionPage.evaluate(
|
||||
await outdatedExtensionPage.evaluate(
|
||||
async () => await chrome.runtime.sendMessage({ type: "unpair" }),
|
||||
);
|
||||
await expect.poll(() => relay.bridge.extensionConnected).toBe(false);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// Browser tests cover doctor plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import chromeExtensionManifest from "../../chrome-extension/manifest.json" with { type: "json" };
|
||||
import { buildBrowserDoctorReport } from "./doctor.js";
|
||||
|
||||
const outdatedExtensionVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0";
|
||||
const equivalentExtensionVersion =
|
||||
chromeExtensionManifest.version.split(".").length < 4
|
||||
? `${chromeExtensionManifest.version}.0`
|
||||
: chromeExtensionManifest.version.replace(/\.0$/, "");
|
||||
|
||||
function collectWarningCheckIds(checks: readonly { id: string; status: string }[]): string[] {
|
||||
const ids: string[] = [];
|
||||
for (const check of checks) {
|
||||
@@ -46,6 +53,7 @@ describe("buildBrowserDoctorReport", () => {
|
||||
const websocketCheck = report.checks.find((check) => check.id === "cdp-websocket");
|
||||
expect(websocketCheck?.status).toBe("info");
|
||||
expect(websocketCheck?.summary).toBe("Browser is launchable but not running");
|
||||
expect(report.checks.find((check) => check.id === "extension-version")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails when Chrome MCP attach is not ready", () => {
|
||||
@@ -77,6 +85,7 @@ describe("buildBrowserDoctorReport", () => {
|
||||
expect(report.ok).toBe(false);
|
||||
const attachCheck = report.checks.find((check) => check.id === "attach-target");
|
||||
expect(attachCheck?.status).toBe("fail");
|
||||
expect(report.checks.find((check) => check.id === "extension-version")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps managed launch warnings non-fatal", () => {
|
||||
@@ -242,4 +251,48 @@ describe("buildBrowserDoctorReport", () => {
|
||||
summary: "unavailable: SystemInfo domain unavailable",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["outdated", outdatedExtensionVersion, "warn"],
|
||||
["current", chromeExtensionManifest.version, "pass"],
|
||||
["equivalent missing version component", equivalentExtensionVersion, "pass"],
|
||||
["maximum valid version", "65535.65535.65535.65535", "warn"],
|
||||
["unavailable", undefined, "info"],
|
||||
["terminal-control input", "2.0.0\u001b[31m", "info"],
|
||||
["oversized version component", "65536.0", "info"],
|
||||
["nonzero leading zero", "02.0.0", "info"],
|
||||
["all-zero version", "0.0.0.0", "info"],
|
||||
["too many version components", "2.0.0.0.0", "info"],
|
||||
] as const)("classifies %s extension version evidence", (_label, extensionVersion, severity) => {
|
||||
const report = buildBrowserDoctorReport({
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "chrome",
|
||||
driver: "extension",
|
||||
transport: "extension",
|
||||
running: true,
|
||||
pid: null,
|
||||
cdpPort: 18792,
|
||||
chosenBrowser: null,
|
||||
userDataDir: null,
|
||||
color: "#00AA00",
|
||||
headless: false,
|
||||
attachOnly: true,
|
||||
},
|
||||
extensionVersion,
|
||||
});
|
||||
|
||||
const versionCheck = report.checks.find((check) => check.id === "extension-version");
|
||||
expect(versionCheck?.status).toBe(severity);
|
||||
if (severity === "warn") {
|
||||
expect(versionCheck?.summary).toContain(
|
||||
`running ${extensionVersion}; bundled ${chromeExtensionManifest.version}`,
|
||||
);
|
||||
expect(versionCheck?.fixHint).toMatch(/reload/i);
|
||||
} else {
|
||||
expect(versionCheck?.fixHint).toBeUndefined();
|
||||
expect(versionCheck?.summary).not.toContain("\u001b");
|
||||
}
|
||||
expect(report.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Turns BrowserStatus into profile-aware diagnostic checks and fix hints for
|
||||
* CLI, tool, and HTTP doctor responses.
|
||||
*/
|
||||
import chromeExtensionManifest from "../../chrome-extension/manifest.json" with { type: "json" };
|
||||
import { formatBrowserGraphicsSummary } from "./chrome.graphics.js";
|
||||
import type { BrowserStatus, BrowserTransport } from "./client.types.js";
|
||||
|
||||
@@ -27,9 +28,24 @@ export type BrowserDoctorReport = {
|
||||
status: BrowserStatus;
|
||||
};
|
||||
|
||||
function isChromeExtensionVersion(value: unknown): value is string {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const components = value.split(".");
|
||||
return (
|
||||
components.length <= 4 &&
|
||||
components.every(
|
||||
(component) => /^(?:0|[1-9]\d{0,4})$/.test(component) && Number(component) <= 65_535,
|
||||
) &&
|
||||
components.some((component) => component !== "0")
|
||||
);
|
||||
}
|
||||
|
||||
/** Build a browser doctor report from a status response and environment facts. */
|
||||
export function buildBrowserDoctorReport(params: {
|
||||
status: BrowserStatus;
|
||||
extensionVersion?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
uid?: number;
|
||||
@@ -88,6 +104,34 @@ export function buildBrowserDoctorReport(params: {
|
||||
"Install the OpenClaw Chrome extension (openclaw browser extension path), run openclaw browser extension pair, and paste the pairing string into the extension popup.",
|
||||
}),
|
||||
});
|
||||
|
||||
const runningVersion = isChromeExtensionVersion(params.extensionVersion)
|
||||
? params.extensionVersion
|
||||
: undefined;
|
||||
const bundledVersion = isChromeExtensionVersion(chromeExtensionManifest.version)
|
||||
? chromeExtensionManifest.version
|
||||
: undefined;
|
||||
// Chrome treats absent version components as zero, so trailing zeroes do not indicate drift.
|
||||
const mismatch = Boolean(
|
||||
runningVersion &&
|
||||
bundledVersion &&
|
||||
runningVersion.replace(/(?:\.0)+$/, "") !== bundledVersion.replace(/(?:\.0)+$/, ""),
|
||||
);
|
||||
checks.push({
|
||||
id: "extension-version",
|
||||
label: "Chrome extension version",
|
||||
status: !runningVersion || !bundledVersion ? "info" : mismatch ? "warn" : "pass",
|
||||
summary:
|
||||
runningVersion && bundledVersion
|
||||
? `running ${runningVersion}; bundled ${bundledVersion} (${mismatch ? "mismatch" : "match"})`
|
||||
: "version data unavailable",
|
||||
...(mismatch
|
||||
? {
|
||||
fixHint:
|
||||
"Reload the OpenClaw extension from chrome://extensions. If the versions still differ, fully quit and reopen Chrome.",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
id: "managed-executable",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Browser tests cover basic.existing session plugin behavior.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import chromeExtensionManifest from "../../../chrome-extension/manifest.json" with { type: "json" };
|
||||
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
|
||||
|
||||
const { inspectChromeGraphicsDiagnosticsMock } = vi.hoisted(() => ({
|
||||
@@ -214,6 +215,43 @@ describe("basic browser routes", () => {
|
||||
inspectChromeGraphicsDiagnosticsMock.mockReset();
|
||||
});
|
||||
|
||||
it("reports version drift only from the selected extension profile owner", async () => {
|
||||
const outdatedVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0";
|
||||
const state = {
|
||||
...createManagedProfileState(
|
||||
{ name: "chrome", driver: "extension", attachOnly: true },
|
||||
{
|
||||
isHttpReachable: async () => true,
|
||||
isTransportAvailable: async () => true,
|
||||
},
|
||||
),
|
||||
extensionRelays: new Map([
|
||||
["chrome", { bridge: { identity: { extensionVersion: outdatedVersion } } }],
|
||||
["other", { bridge: { identity: { extensionVersion: chromeExtensionManifest.version } } }],
|
||||
]),
|
||||
};
|
||||
|
||||
const response = await callBasicRouteWithState({
|
||||
route: "/doctor",
|
||||
query: { profile: "chrome" },
|
||||
state,
|
||||
});
|
||||
const report = responseBodyRecord(response);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(report.checks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "extension-version",
|
||||
status: "warn",
|
||||
summary: expect.stringContaining(
|
||||
`running ${outdatedVersion}; bundled ${chromeExtensionManifest.version}`,
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(report.status).not.toHaveProperty("chromeExtension");
|
||||
});
|
||||
|
||||
it("releases the doctor transaction, restarts once, and retries the live probe", async () => {
|
||||
const ensureBrowserAvailable = vi.fn(async () => {});
|
||||
const ensureTabAvailable = vi
|
||||
|
||||
@@ -397,7 +397,14 @@ export function registerBrowserBasicRoutes(app: BrowserRouteRegistrar, ctx: Brow
|
||||
signal: req.signal,
|
||||
run: async (signal) => {
|
||||
const status = await buildBrowserStatus(ctx, profileCtx, signal);
|
||||
const doctorReport = buildBrowserDoctorReport({ status });
|
||||
const doctorReport = buildBrowserDoctorReport({
|
||||
status,
|
||||
extensionVersion:
|
||||
status.transport === "extension"
|
||||
? ctx.state().extensionRelays?.get(profileCtx.profile.name)?.bridge.identity
|
||||
?.extensionVersion
|
||||
: undefined,
|
||||
});
|
||||
if (toBoolean(req.query.deep) === true || toBoolean(req.query.live) === true) {
|
||||
doctorReport.checks.push(await runBrowserLiveProbe(profileCtx, signal));
|
||||
doctorReport.ok = doctorReport.checks.every((check) => check.status !== "fail");
|
||||
|
||||
@@ -490,41 +490,203 @@ describe("browser manage output", () => {
|
||||
expect(getBrowserManageCallBrowserRequestMock()).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints authenticated extension drift from the canonical browser doctor report", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
{
|
||||
id: "extension-version",
|
||||
label: "Chrome extension version",
|
||||
status: "warn",
|
||||
summary: "running 2.0.0; bundled 2.2.0 (mismatch)",
|
||||
fixHint: "Reload the OpenClaw extension.",
|
||||
},
|
||||
],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "chrome",
|
||||
driver: "extension",
|
||||
transport: "extension",
|
||||
running: true,
|
||||
cdpReady: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
return { profiles: [{ name: "chrome", running: true }] };
|
||||
}
|
||||
if (req.path === "/tabs") {
|
||||
return { running: true, tabs: [] };
|
||||
}
|
||||
throw new Error(`unexpected browser route: ${req.path}`);
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(lastRuntimeLog()).toContain(
|
||||
"WARN extension-version: running 2.0.0; bundled 2.2.0 (mismatch); Reload the OpenClaw extension.",
|
||||
);
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(getBrowserManageCallBrowserRequestMock().mock.calls[0]?.[1]).toMatchObject({
|
||||
path: "/doctor",
|
||||
query: { profile: "chrome" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unavailable extension version evidence informational and nonfatal", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
checks: [
|
||||
{
|
||||
id: "extension-version",
|
||||
label: "Chrome extension version",
|
||||
status: "info",
|
||||
summary: "version data unavailable",
|
||||
},
|
||||
],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "chrome",
|
||||
transport: "extension",
|
||||
running: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return req.path === "/profiles"
|
||||
? { profiles: [{ name: "chrome", running: true }] }
|
||||
: { running: true, tabs: [] };
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(lastRuntimeLog()).toContain("INFO extension-version: version data unavailable");
|
||||
expect(lastRuntimeLog()).not.toContain("WARN extension-version");
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves one nonfatal JSON report for confirmed extension version drift", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
checks: [
|
||||
{
|
||||
id: "extension-version",
|
||||
label: "Chrome extension version",
|
||||
status: "warn",
|
||||
summary: "running 2.0.0; bundled 2.2.0 (mismatch)",
|
||||
fixHint: "Reload the OpenClaw extension.",
|
||||
},
|
||||
],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "chrome",
|
||||
transport: "extension",
|
||||
running: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return req.path === "/profiles"
|
||||
? { profiles: [{ name: "chrome", running: true }] }
|
||||
: { running: true, tabs: [] };
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--json", "doctor"], { from: "user" });
|
||||
|
||||
expect(parseSingleRuntimeJson()).toMatchObject({
|
||||
ok: true,
|
||||
checks: expect.arrayContaining([
|
||||
expect.objectContaining({ name: "extension-version", ok: true, warning: true }),
|
||||
]),
|
||||
});
|
||||
expect(getBrowserCliRuntime().writeJson).toHaveBeenCalledTimes(1);
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("runs exactly one deep snapshot after consuming the canonical doctor report", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
checks: [],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "chrome",
|
||||
transport: "extension",
|
||||
running: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
return { profiles: [{ name: "chrome", running: true }] };
|
||||
}
|
||||
if (req.path === "/tabs") {
|
||||
return { running: true, tabs: [] };
|
||||
}
|
||||
if (req.path === "/snapshot") {
|
||||
return { ok: true, format: "aria", nodes: [{ role: "document" }] };
|
||||
}
|
||||
throw new Error(`unexpected browser route: ${req.path}`);
|
||||
});
|
||||
|
||||
const program = createBrowserManageProgram();
|
||||
await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor", "--deep"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(lastRuntimeLog()).toContain("OK live-snapshot: 1 nodes/lines");
|
||||
const snapshotCalls = getBrowserManageCallBrowserRequestMock().mock.calls.filter(
|
||||
([, request]) => request.path === "/snapshot",
|
||||
);
|
||||
expect(snapshotCalls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("prints a readable browser doctor report", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/") {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
driver: "openclaw",
|
||||
transport: "cdp",
|
||||
running: true,
|
||||
cdpReady: true,
|
||||
cdpHttp: true,
|
||||
pid: 4321,
|
||||
cdpPort: 18792,
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
chosenBrowser: "chrome",
|
||||
userDataDir: null,
|
||||
color: "#00AA00",
|
||||
headless: false,
|
||||
noSandbox: false,
|
||||
executablePath: null,
|
||||
attachOnly: false,
|
||||
graphics: {
|
||||
status: "available",
|
||||
observedAt: 123,
|
||||
acceleration: "software",
|
||||
renderer: "ANGLE (Google, SwiftShader Device)",
|
||||
vendor: "Google Inc.",
|
||||
version: "OpenGL ES 3.0",
|
||||
backend: "(gl=angle,angle=swiftshader)",
|
||||
devices: [],
|
||||
featureStatus: {},
|
||||
disabledFeatures: [],
|
||||
driverBugWorkarounds: [],
|
||||
videoDecoding: [],
|
||||
videoEncoding: [],
|
||||
checks: [],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
driver: "openclaw",
|
||||
transport: "cdp",
|
||||
running: true,
|
||||
cdpReady: true,
|
||||
cdpHttp: true,
|
||||
pid: 4321,
|
||||
cdpPort: 18792,
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
chosenBrowser: "chrome",
|
||||
userDataDir: null,
|
||||
color: "#00AA00",
|
||||
headless: false,
|
||||
noSandbox: false,
|
||||
executablePath: null,
|
||||
attachOnly: false,
|
||||
graphics: {
|
||||
status: "available",
|
||||
observedAt: 123,
|
||||
acceleration: "software",
|
||||
renderer: "ANGLE (Google, SwiftShader Device)",
|
||||
vendor: "Google Inc.",
|
||||
version: "OpenGL ES 3.0",
|
||||
backend: "(gl=angle,angle=swiftshader)",
|
||||
devices: [],
|
||||
featureStatus: {},
|
||||
disabledFeatures: [],
|
||||
driverBugWorkarounds: [],
|
||||
videoDecoding: [],
|
||||
videoEncoding: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -562,12 +724,15 @@ describe("browser manage output", () => {
|
||||
|
||||
it("prints one complete JSON browser doctor failure before setting exit status", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/") {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
enabled: false,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: false,
|
||||
checks: [],
|
||||
status: {
|
||||
enabled: false,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
@@ -596,12 +761,15 @@ describe("browser manage output", () => {
|
||||
|
||||
it("prints one JSON browser doctor report and succeeds when every check passes", async () => {
|
||||
getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => {
|
||||
if (req.path === "/") {
|
||||
if (req.path === "/doctor") {
|
||||
return {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: true,
|
||||
checks: [],
|
||||
status: {
|
||||
enabled: true,
|
||||
profile: "openclaw",
|
||||
transport: "cdp",
|
||||
running: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (req.path === "/profiles") {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import type { Command } from "commander";
|
||||
import { formatBrowserGraphicsSummary } from "../browser/chrome.graphics.js";
|
||||
import type { BrowserDoctorReport } from "../browser/doctor.js";
|
||||
import {
|
||||
BROWSER_TAB_REFERENCE_HELP,
|
||||
callBrowserRequest,
|
||||
@@ -37,6 +38,7 @@ type BrowserDoctorCheck = {
|
||||
ok: boolean;
|
||||
detail?: string;
|
||||
warning?: boolean;
|
||||
info?: boolean;
|
||||
};
|
||||
|
||||
function sanitizeTableCell(value: string): string {
|
||||
@@ -132,7 +134,7 @@ function logBrowserTabs(tabs: BrowserTab[], json?: boolean) {
|
||||
}
|
||||
|
||||
function formatDoctorLine(check: BrowserDoctorCheck): string {
|
||||
const prefix = check.warning ? "WARN" : check.ok ? "OK" : "FAIL";
|
||||
const prefix = check.warning ? "WARN" : check.info ? "INFO" : check.ok ? "OK" : "FAIL";
|
||||
return `${prefix} ${check.name}${check.detail ? `: ${check.detail}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -156,10 +158,18 @@ function formatBrowserDoctorGatewayError(error: unknown): string {
|
||||
|
||||
async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, deep?: boolean) {
|
||||
const checks: BrowserDoctorCheck[] = [];
|
||||
let status: BrowserStatus | null;
|
||||
let report: BrowserDoctorReport;
|
||||
|
||||
try {
|
||||
status = await fetchBrowserStatus(parent, profile);
|
||||
report = await callBrowserRequest<BrowserDoctorReport>(
|
||||
parent,
|
||||
{
|
||||
method: "GET",
|
||||
path: "/doctor",
|
||||
query: resolveProfileQuery(profile),
|
||||
},
|
||||
{ timeoutMs: BROWSER_MANAGE_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
checks.push({
|
||||
name: "gateway",
|
||||
ok: true,
|
||||
@@ -174,6 +184,7 @@ async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, dee
|
||||
return { ok: false, checks };
|
||||
}
|
||||
|
||||
const status = report.status;
|
||||
checks.push({
|
||||
name: "plugin",
|
||||
ok: status.enabled,
|
||||
@@ -191,6 +202,16 @@ async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, dee
|
||||
? `running${status.cdpReady === false ? ", CDP not ready" : ""}`
|
||||
: "not running; run `openclaw browser start`",
|
||||
});
|
||||
const extensionVersionCheck = report.checks.find((check) => check.id === "extension-version");
|
||||
if (extensionVersionCheck) {
|
||||
checks.push({
|
||||
name: extensionVersionCheck.id,
|
||||
ok: extensionVersionCheck.status !== "fail",
|
||||
warning: extensionVersionCheck.status === "warn",
|
||||
info: extensionVersionCheck.status === "info",
|
||||
detail: `${extensionVersionCheck.summary}${extensionVersionCheck.fixHint ? `; ${extensionVersionCheck.fixHint}` : ""}`,
|
||||
});
|
||||
}
|
||||
if (status.graphics) {
|
||||
checks.push({
|
||||
name: "graphics",
|
||||
|
||||
Reference in New Issue
Block a user