import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const fileState = vi.hoisted(() => ({ hasCliDotEnv: false, })); const dotenvState = vi.hoisted(() => { const state = { profileAtDotenvLoad: undefined as string | undefined, containerAtDotenvLoad: undefined as string | undefined, }; return { state, loadDotEnv: vi.fn(() => { state.profileAtDotenvLoad = process.env.OPENCLAW_PROFILE; state.containerAtDotenvLoad = process.env.OPENCLAW_CONTAINER; }), }; }); const maybeRunCliInContainerMock = vi.hoisted(() => vi.fn((argv: string[]) => ({ handled: false, argv })), ); vi.mock("node:fs", async (importOriginal) => { const actual = await importOriginal(); type ExistsSyncPath = Parameters[0]; return { ...actual, existsSync: vi.fn((target: ExistsSyncPath) => { if (typeof target === "string" && target.endsWith(".env")) { return fileState.hasCliDotEnv; } return actual.existsSync(target); }), }; }); vi.mock("./dotenv.js", () => ({ loadCliDotEnv: dotenvState.loadDotEnv, })); vi.mock("../infra/env.js", () => ({ normalizeEnv: vi.fn(), })); vi.mock("../infra/runtime-guard.js", () => ({ assertSupportedRuntime: vi.fn(), })); vi.mock("../infra/path-env.js", () => ({ ensureOpenClawCliOnPath: vi.fn(), })); vi.mock("./route.js", () => ({ tryRouteCli: vi.fn(async () => true), })); vi.mock("./windows-argv.js", () => ({ normalizeWindowsArgv: (argv: string[]) => argv, })); vi.mock("./container-target.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, maybeRunCliInContainer: maybeRunCliInContainerMock, }; }); import { runCli } from "./run-main.js"; describe("runCli profile env bootstrap", () => { const originalProfile = process.env.OPENCLAW_PROFILE; const originalStateDir = process.env.OPENCLAW_STATE_DIR; const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH; const originalContainer = process.env.OPENCLAW_CONTAINER; const originalGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; const originalGatewayUrl = process.env.OPENCLAW_GATEWAY_URL; const originalGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; const originalGatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD; beforeEach(() => { delete process.env.OPENCLAW_PROFILE; delete process.env.OPENCLAW_STATE_DIR; delete process.env.OPENCLAW_CONFIG_PATH; delete process.env.OPENCLAW_CONTAINER; delete process.env.OPENCLAW_GATEWAY_PORT; delete process.env.OPENCLAW_GATEWAY_URL; delete process.env.OPENCLAW_GATEWAY_TOKEN; delete process.env.OPENCLAW_GATEWAY_PASSWORD; dotenvState.state.profileAtDotenvLoad = undefined; dotenvState.state.containerAtDotenvLoad = undefined; dotenvState.loadDotEnv.mockClear(); maybeRunCliInContainerMock.mockClear(); fileState.hasCliDotEnv = false; }); afterEach(() => { if (originalProfile === undefined) { delete process.env.OPENCLAW_PROFILE; } else { process.env.OPENCLAW_PROFILE = originalProfile; } if (originalContainer === undefined) { delete process.env.OPENCLAW_CONTAINER; } else { process.env.OPENCLAW_CONTAINER = originalContainer; } if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; } else { process.env.OPENCLAW_STATE_DIR = originalStateDir; } if (originalConfigPath === undefined) { delete process.env.OPENCLAW_CONFIG_PATH; } else { process.env.OPENCLAW_CONFIG_PATH = originalConfigPath; } if (originalGatewayPort === undefined) { delete process.env.OPENCLAW_GATEWAY_PORT; } else { process.env.OPENCLAW_GATEWAY_PORT = originalGatewayPort; } if (originalGatewayUrl === undefined) { delete process.env.OPENCLAW_GATEWAY_URL; } else { process.env.OPENCLAW_GATEWAY_URL = originalGatewayUrl; } if (originalGatewayToken === undefined) { delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { process.env.OPENCLAW_GATEWAY_TOKEN = originalGatewayToken; } if (originalGatewayPassword === undefined) { delete process.env.OPENCLAW_GATEWAY_PASSWORD; } else { process.env.OPENCLAW_GATEWAY_PASSWORD = originalGatewayPassword; } }); it("applies --profile before dotenv loading", async () => { fileState.hasCliDotEnv = true; await runCli(["node", "openclaw", "--profile", "rawdog", "status"]); expect(dotenvState.loadDotEnv).toHaveBeenCalledOnce(); expect(dotenvState.state.profileAtDotenvLoad).toBe("rawdog"); expect(process.env.OPENCLAW_PROFILE).toBe("rawdog"); }); it("rejects --container combined with --profile", async () => { await expect( runCli(["node", "openclaw", "--container", "demo", "--profile", "rawdog", "status"]), ).rejects.toThrow("--container cannot be combined with --profile/--dev"); expect(dotenvState.loadDotEnv).not.toHaveBeenCalled(); expect(process.env.OPENCLAW_PROFILE).toBe("rawdog"); }); it("rejects --container combined with interleaved --profile", async () => { await expect( runCli(["node", "openclaw", "status", "--container", "demo", "--profile", "rawdog"]), ).rejects.toThrow("--container cannot be combined with --profile/--dev"); }); it("rejects --container combined with interleaved --dev", async () => { await expect( runCli(["node", "openclaw", "status", "--container", "demo", "--dev"]), ).rejects.toThrow("--container cannot be combined with --profile/--dev"); }); it("does not let dotenv change container target resolution", async () => { fileState.hasCliDotEnv = true; dotenvState.loadDotEnv.mockImplementationOnce(() => { process.env.OPENCLAW_CONTAINER = "demo"; dotenvState.state.profileAtDotenvLoad = process.env.OPENCLAW_PROFILE; dotenvState.state.containerAtDotenvLoad = process.env.OPENCLAW_CONTAINER; }); await runCli(["node", "openclaw", "status"]); expect(dotenvState.loadDotEnv).toHaveBeenCalledOnce(); expect(process.env.OPENCLAW_CONTAINER).toBe("demo"); expect(dotenvState.state.containerAtDotenvLoad).toBe("demo"); expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "status"]); expect(maybeRunCliInContainerMock).toHaveReturnedWith({ handled: false, argv: ["node", "openclaw", "status"], }); }); it("allows container mode when OPENCLAW_PROFILE is already set in env", async () => { process.env.OPENCLAW_PROFILE = "work"; await expect( runCli(["node", "openclaw", "--container", "demo", "status"]), ).resolves.toBeUndefined(); }); it.each([ ["OPENCLAW_GATEWAY_PORT", "19001"], ["OPENCLAW_GATEWAY_URL", "ws://127.0.0.1:18789"], ["OPENCLAW_GATEWAY_TOKEN", "demo-token"], ["OPENCLAW_GATEWAY_PASSWORD", "demo-password"], ])("allows container mode when %s is set in env", async (key, value) => { process.env[key] = value; await expect( runCli(["node", "openclaw", "--container", "demo", "status"]), ).resolves.toBeUndefined(); }); it("allows container mode when only OPENCLAW_STATE_DIR is set in env", async () => { process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-host-state"; await expect( runCli(["node", "openclaw", "--container", "demo", "status"]), ).resolves.toBeUndefined(); }); it("allows container mode when only OPENCLAW_CONFIG_PATH is set in env", async () => { process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-host-state/openclaw.json"; await expect( runCli(["node", "openclaw", "--container", "demo", "status"]), ).resolves.toBeUndefined(); }); });