import fs from "node:fs" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" ;
import { writeStateDirDotEnv } from "../config/test-helpers.js" ;
const mocks = vi.hoisted(() => ({
hasAnyAuthProfileStoreSource: vi.fn(() => true ),
loadAuthProfileStoreForSecretsRuntime: vi.fn(),
resolvePreferredNodePath: vi.fn(),
resolveGatewayProgramArguments: vi.fn(),
resolveSystemNodeInfo: vi.fn(),
renderSystemNodeWarning: vi.fn(),
buildServiceEnvironment: vi.fn(),
}));
vi.mock("./daemon-install-auth-profiles-source.runtime.js" , () => ({
hasAnyAuthProfileStoreSource: mocks.hasAnyAuthProfileStoreSource,
}));
vi.mock("./daemon-install-auth-profiles-store.runtime.js" , () => ({
loadAuthProfileStoreForSecretsRuntime: mocks.loadAuthProfileStoreForSecretsRuntime,
}));
vi.mock("../daemon/runtime-paths.js" , () => ({
resolvePreferredNodePath: mocks.resolvePreferredNodePath,
resolveSystemNodeInfo: mocks.resolveSystemNodeInfo,
renderSystemNodeWarning: mocks.renderSystemNodeWarning,
}));
vi.mock("../daemon/program-args.js" , () => ({
resolveGatewayProgramArguments: mocks.resolveGatewayProgramArguments,
}));
vi.mock("../daemon/service-env.js" , () => ({
buildServiceEnvironment: mocks.buildServiceEnvironment,
}));
import {
buildGatewayInstallPlan,
gatewayInstallErrorHint,
resolveGatewayDevMode,
} from "./daemon-install-helpers.js" ;
afterEach(() => {
vi.resetAllMocks();
});
describe("resolveGatewayDevMode" , () => {
it("detects dev mode for src ts entrypoints" , () => {
expect(resolveGatewayDevMode(["node" , "/Users/me/openclaw/src/cli/index.ts" ])).toBe(true );
expect(resolveGatewayDevMode(["node" , "C:\\Users\\me\\openclaw\\src\\cli\\index.ts" ])).toBe(
true ,
);
expect(resolveGatewayDevMode(["node" , "/Users/me/openclaw/dist/cli/index.js" ])).toBe(false );
});
});
function mockNodeGatewayPlanFixture(
params: {
workingDirectory?: string;
version?: string;
supported?: boolean ;
warning?: string;
serviceEnvironment?: Record<string, string>;
} = {},
) {
const {
workingDirectory = "/Users/me" ,
version = "22.0.0" ,
supported = true ,
warning,
serviceEnvironment = { OPENCLAW_PORT: "3000" },
} = params;
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node" );
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node" , "gateway" ],
workingDirectory,
});
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
version: 1 ,
profiles: {},
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node" ,
version,
supported,
});
mocks.renderSystemNodeWarning.mockReturnValue(warning);
mocks.buildServiceEnvironment.mockReturnValue(serviceEnvironment);
}
describe("buildGatewayInstallPlan" , () => {
// Prevent tests from reading the developer's real ~/.openclaw/.env when
// passing `env: {}` (which falls back to os.homedir for state-dir resolution).
let isolatedHome: string;
beforeEach(() => {
isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), "oc-plan-test-" ));
});
afterEach(() => {
fs.rmSync(isolatedHome, { recursive: true , force: true });
});
const isolatedPlanEnv = (env: Record<string, string | undefined> = {}) => ({
HOME: isolatedHome,
...env,
});
it("uses provided nodePath and returns plan" , async () => {
mockNodeGatewayPlanFixture();
const plan = await buildGatewayInstallPlan({
env: { HOME: isolatedHome },
port: 3000 ,
runtime: "node" ,
nodePath: "/custom/node" ,
});
expect(plan.programArguments).toEqual(["node" , "gateway" ]);
expect(plan.workingDirectory).toBe("/Users/me" );
expect(plan.environment).toEqual({ OPENCLAW_PORT: "3000" });
expect(mocks.resolvePreferredNodePath).not.toHaveBeenCalled();
expect(mocks.buildServiceEnvironment).toHaveBeenCalledWith(
expect.objectContaining({
env: { HOME: isolatedHome },
port: 3000 ,
extraPathDirs: ["/custom" ],
}),
);
});
it("does not prepend '.' when nodePath is a bare executable name" , async () => {
mockNodeGatewayPlanFixture();
await buildGatewayInstallPlan({
env: { HOME: isolatedHome },
port: 3000 ,
runtime: "node" ,
nodePath: "node" ,
});
expect(mocks.buildServiceEnvironment).toHaveBeenCalledWith(
expect.objectContaining({
extraPathDirs: undefined,
}),
);
});
it("emits warnings when renderSystemNodeWarning returns one" , async () => {
const warn = vi.fn();
mockNodeGatewayPlanFixture({
workingDirectory: undefined,
version: "18.0.0" ,
supported: false ,
warning: "Node too old" ,
serviceEnvironment: {},
});
await buildGatewayInstallPlan({
env: isolatedPlanEnv(),
port: 3000 ,
runtime: "node" ,
warn,
});
expect(warn).toHaveBeenCalledWith("Node too old" , "Gateway runtime" );
expect(mocks.resolvePreferredNodePath).toHaveBeenCalled();
});
it("merges safe config env while dropping unsafe values and keeping service precedence" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
HOME: "/Users/service" ,
OPENCLAW_PORT: "3000" ,
},
});
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv(),
port: 3000 ,
runtime: "node" ,
config: {
env: {
HOME: "/Users/config" ,
CUSTOM_VAR: "custom-value" ,
EMPTY_KEY: "" ,
TRIMMED_KEY: " " ,
vars: {
GOOGLE_API_KEY: "test-key" , // pragma: allowlist secret
OPENCLAW_PORT: "9999" ,
NODE_OPTIONS: "--require /tmp/evil.js" ,
SAFE_KEY: "safe-value" ,
},
},
},
});
expect(plan.environment.GOOGLE_API_KEY).toBe("test-key" );
expect(plan.environment.CUSTOM_VAR).toBe("custom-value" );
expect(plan.environment.SAFE_KEY).toBe("safe-value" );
expect(plan.environment.NODE_OPTIONS).toBeUndefined();
expect(plan.environment.EMPTY_KEY).toBeUndefined();
expect(plan.environment.TRIMMED_KEY).toBeUndefined();
expect(plan.environment.HOME).toBe("/Users/service" );
expect(plan.environment.OPENCLAW_PORT).toBe("3000" );
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBe(
"CUSTOM_VAR,GOOGLE_API_KEY,OPENCLAW_PORT,SAFE_KEY" ,
);
});
it("skips auth-profile store load when no auth-profile source exists" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000" ,
},
});
mocks.hasAnyAuthProfileStoreSource.mockReturnValue(false );
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv(),
port: 3000 ,
runtime: "node" ,
});
expect(mocks.loadAuthProfileStoreForSecretsRuntime).not.toHaveBeenCalled();
expect(plan.environment.OPENCLAW_PORT).toBe("3000" );
});
it("uses the provided authStore without probing auth-profile runtime" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000" ,
},
});
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv({
OPENAI_API_KEY: "sk-openai-test" ,
}),
port: 3000 ,
runtime: "node" ,
authStore: {
version: 1 ,
profiles: {
"openai:default" : {
type: "api_key" ,
provider: "openai" ,
keyRef: { source: "env" , provider: "default" , id: "OPENAI_API_KEY" },
},
},
},
});
expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test" );
expect(mocks.hasAnyAuthProfileStoreSource).not.toHaveBeenCalled();
expect(mocks.loadAuthProfileStoreForSecretsRuntime).not.toHaveBeenCalled();
});
it("merges only portable auth-profile env refs into the service environment" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000" ,
},
});
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
version: 1 ,
profiles: {
"node:default" : {
type: "token" ,
provider: "node" ,
tokenRef: { source: "env" , provider: "default" , id: "NODE_OPTIONS" },
},
"git:default" : {
type: "token" ,
provider: "git" ,
tokenRef: { source: "env" , provider: "default" , id: "GIT_ASKPASS" },
},
"broken:default" : {
type: "token" ,
provider: "broken" ,
tokenRef: { source: "env" , provider: "default" , id: "BAD KEY" },
},
"openai:default" : {
type: "api_key" ,
provider: "openai" ,
keyRef: { source: "env" , provider: "default" , id: "OPENAI_API_KEY" },
},
"anthropic:default" : {
type: "token" ,
provider: "anthropic" ,
tokenRef: { source: "env" , provider: "default" , id: "ANTHROPIC_TOKEN" },
},
"missing:default" : {
type: "token" ,
provider: "missing" ,
tokenRef: { source: "env" , provider: "default" , id: "MISSING_TOKEN" },
},
},
});
const warn = vi.fn();
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv({
NODE_OPTIONS: "--require ./pwn.js" ,
GIT_ASKPASS: "/tmp/askpass.sh" ,
OPENAI_API_KEY: "sk-openai-test" , // pragma: allowlist secret
ANTHROPIC_TOKEN: "ant-test-token" ,
}),
port: 3000 ,
runtime: "node" ,
warn,
});
expect(plan.environment.NODE_OPTIONS).toBeUndefined();
expect(plan.environment.GIT_ASKPASS).toBeUndefined();
expect(plan.environment["BAD KEY" ]).toBeUndefined();
expect(plan.environment.MISSING_TOKEN).toBeUndefined();
expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test" );
expect(plan.environment.ANTHROPIC_TOKEN).toBe("ant-test-token" );
expect(warn).toHaveBeenCalledWith(expect.stringContaining("NODE_OPTIONS" ), "Auth profile" );
expect(warn).toHaveBeenCalledWith(expect.stringContaining("GIT_ASKPASS" ), "Auth profile" );
});
});
describe("buildGatewayInstallPlan — dotenv merge" , () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-plan-dotenv-" ));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true , force: true });
});
it("merges .env vars with config and service precedence" , async () => {
await writeStateDirDotEnv(
"BRAVE_API_KEY=BSA-from-env\nOPENROUTER_API_KEY=or-key\nMY_KEY=from-dotenv\nHOME=/from-dotenv\n" ,
{
stateDir: path.join(tmpDir, ".openclaw" ),
},
);
mockNodeGatewayPlanFixture({
serviceEnvironment: {
HOME: "/from-service" ,
OPENCLAW_PORT: "3000" ,
},
});
const plan = await buildGatewayInstallPlan({
env: { HOME: tmpDir },
port: 3000 ,
runtime: "node" ,
config: {
env: {
vars: {
MY_KEY: "from-config" ,
},
},
},
});
expect(plan.environment.BRAVE_API_KEY).toBe("BSA-from-env" );
expect(plan.environment.OPENROUTER_API_KEY).toBe("or-key" );
expect(plan.environment.MY_KEY).toBe("from-config" );
expect(plan.environment.HOME).toBe("/from-service" );
expect(plan.environment.OPENCLAW_PORT).toBe("3000" );
});
it("works when .env file does not exist" , async () => {
mockNodeGatewayPlanFixture({ serviceEnvironment: { OPENCLAW_PORT: "3000" } });
const plan = await buildGatewayInstallPlan({
env: { HOME: tmpDir },
port: 3000 ,
runtime: "node" ,
});
expect(plan.environment.OPENCLAW_PORT).toBe("3000" );
});
it("preserves safe custom vars from an existing service env and merges PATH" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
HOME: "/from-service" ,
OPENCLAW_PORT: "3000" ,
PATH: "/managed/bin:/usr/bin" ,
TMPDIR: "/tmp" ,
},
});
const plan = await buildGatewayInstallPlan({
env: { HOME: tmpDir },
port: 3000 ,
runtime: "node" ,
existingEnvironment: {
PATH: ".:/tmp/evil:/custom/go/bin:/usr/bin" ,
GOBIN: "/Users/test/.local/gopath/bin" ,
BLOGWATCHER_HOME: "/Users/test/.blogwatcher" ,
NODE_OPTIONS: "--require /tmp/evil.js" ,
GOPATH: "/Users/test/.local/gopath" ,
OPENCLAW_SERVICE_MARKER: "openclaw" ,
},
});
expect(plan.environment.PATH).toBe("/managed/bin:/usr/bin:/custom/go/bin" );
expect(plan.environment.GOBIN).toBe("/Users/test/.local/gopath/bin" );
expect(plan.environment.BLOGWATCHER_HOME).toBe("/Users/test/.blogwatcher" );
expect(plan.environment.NODE_OPTIONS).toBeUndefined();
expect(plan.environment.GOPATH).toBeUndefined();
expect(plan.environment.OPENCLAW_SERVICE_MARKER).toBeUndefined();
});
it("drops keys that were previously tracked as managed service env" , async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
HOME: "/from-service" ,
OPENCLAW_PORT: "3000" ,
PATH: "/managed/bin:/usr/bin" ,
},
});
const plan = await buildGatewayInstallPlan({
env: { HOME: tmpDir },
port: 3000 ,
runtime: "node" ,
existingEnvironment: {
PATH: "/custom/go/bin:/usr/bin" ,
GOBIN: "/Users/test/.local/gopath/bin" ,
BLOGWATCHER_HOME: "/Users/test/.blogwatcher" ,
GOPATH: "/Users/test/.local/gopath" ,
OPENCLAW_SERVICE_MANAGED_ENV_KEYS: "GOBIN,GOPATH" ,
},
});
expect(plan.environment.PATH).toBe("/managed/bin:/usr/bin:/custom/go/bin" );
expect(plan.environment.GOBIN).toBeUndefined();
expect(plan.environment.BLOGWATCHER_HOME).toBe("/Users/test/.blogwatcher" );
expect(plan.environment.GOPATH).toBeUndefined();
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
});
});
describe("gatewayInstallErrorHint" , () => {
it("returns platform-specific hints" , () => {
expect(gatewayInstallErrorHint("win32" )).toContain("Startup-folder login item" );
expect(gatewayInstallErrorHint("win32" )).toContain("elevated PowerShell" );
expect(gatewayInstallErrorHint("linux" )).toMatch(
/(?:openclaw|openclaw)( --profile isolated)? gateway install/,
);
});
});
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Dauer der Verarbeitung: 0.20 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland