
























@@ -3,7 +3,14 @@ import path from "node:path";
33import { describe, expect, it } from "vitest";
44import type { BrowserConfig } from "../config/config.js";
55import { resolveUserPath } from "../utils.js";
6-import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js";
6+import {
7+getManagedBrowserMissingDisplayError,
8+OPENCLAW_BROWSER_HEADLESS_ENV,
9+resolveBrowserConfig,
10+resolveManagedBrowserHeadlessMode,
11+resolveProfile,
12+shouldStartLocalBrowserServer,
13+} from "./config.js";
714import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
815916function withEnv<T>(env: Record<string, string | undefined>, fn: () => T): T {
@@ -315,6 +322,111 @@ describe("browser config", () => {
315322expect(remote?.headless).toBe(false);
316323});
317324325+describe("managed browser headless mode", () => {
326+const noDisplayEnv = {
327+DISPLAY: undefined,
328+WAYLAND_DISPLAY: undefined,
329+[OPENCLAW_BROWSER_HEADLESS_ENV]: undefined,
330+};
331+332+it("falls back to headless for local managed Linux profiles without display", () => {
333+const resolved = resolveBrowserConfig({});
334+const profile = resolveProfile(resolved, "openclaw")!;
335+336+expect(
337+resolveManagedBrowserHeadlessMode(resolved, profile, {
338+platform: "linux",
339+env: noDisplayEnv,
340+}),
341+).toEqual({ headless: true, source: "linux-display-fallback" });
342+});
343+344+it("does not apply the no-display fallback to remote CDP profiles", () => {
345+const resolved = resolveBrowserConfig({
346+profiles: {
347+remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
348+},
349+});
350+const profile = resolveProfile(resolved, "remote")!;
351+352+expect(
353+resolveManagedBrowserHeadlessMode(resolved, profile, {
354+platform: "linux",
355+env: noDisplayEnv,
356+}),
357+).toEqual({ headless: false, source: "default" });
358+});
359+360+it("lets explicit profile headless=false beat the Linux no-display fallback", () => {
361+const resolved = resolveBrowserConfig({
362+headless: true,
363+profiles: {
364+openclaw: { cdpPort: 18800, color: "#FF4500", headless: false },
365+},
366+});
367+const profile = resolveProfile(resolved, "openclaw")!;
368+369+expect(
370+resolveManagedBrowserHeadlessMode(resolved, profile, {
371+platform: "linux",
372+env: noDisplayEnv,
373+}),
374+).toEqual({ headless: false, source: "profile" });
375+});
376+377+it("lets explicit global headless=false beat the Linux no-display fallback", () => {
378+const resolved = resolveBrowserConfig({ headless: false });
379+const profile = resolveProfile(resolved, "openclaw")!;
380+381+expect(
382+resolveManagedBrowserHeadlessMode(resolved, profile, {
383+platform: "linux",
384+env: noDisplayEnv,
385+}),
386+).toEqual({ headless: false, source: "config" });
387+});
388+389+it("lets OPENCLAW_BROWSER_HEADLESS override profile/global config", () => {
390+const resolved = resolveBrowserConfig({
391+profiles: {
392+openclaw: { cdpPort: 18800, color: "#FF4500", headless: false },
393+},
394+});
395+const profile = resolveProfile(resolved, "openclaw")!;
396+397+expect(
398+resolveManagedBrowserHeadlessMode(resolved, profile, {
399+platform: "linux",
400+env: { ...noDisplayEnv, [OPENCLAW_BROWSER_HEADLESS_ENV]: "1" },
401+}),
402+).toEqual({ headless: true, source: "env" });
403+});
404+405+it("returns an actionable error only when headed mode is explicitly selected", () => {
406+const defaultResolved = resolveBrowserConfig({});
407+const defaultProfile = resolveProfile(defaultResolved, "openclaw")!;
408+expect(
409+getManagedBrowserMissingDisplayError(defaultResolved, defaultProfile, {
410+platform: "linux",
411+env: noDisplayEnv,
412+}),
413+).toBeNull();
414+415+const profileResolved = resolveBrowserConfig({
416+profiles: {
417+openclaw: { cdpPort: 18800, color: "#FF4500", headless: false },
418+},
419+});
420+const profile = resolveProfile(profileResolved, "openclaw")!;
421+expect(
422+getManagedBrowserMissingDisplayError(profileResolved, profile, {
423+platform: "linux",
424+env: noDisplayEnv,
425+}),
426+).toContain("browser.profiles.openclaw.headless=false");
427+});
428+});
429+318430it("inherits executablePath from global browser config when profile override is not set", () => {
319431const resolved = resolveBrowserConfig({
320432executablePath: "~/bin/chrome-global",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。