Gateway: isolate dev reset from default profile state

This commit is contained in:
Tristan Manchester 2026-02-22 06:48:13 +01:00
parent 97683071b5
commit 902396c8b6
8 changed files with 390 additions and 27 deletions

View File

@ -1,6 +1,6 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempSecretFiles } from "../../test-utils/secret-file-fixture.js";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({
@ -22,6 +22,10 @@ const configState = vi.hoisted(() => ({
cfg: {} as Record<string, unknown>,
snapshot: { exists: false } as Record<string, unknown>,
}));
const resolveStateDir = vi.fn<(env?: NodeJS.ProcessEnv) => string>(() => "/tmp");
const resolveConfigPath = vi.fn((_env: NodeJS.ProcessEnv, stateDir: string) => {
return `${stateDir}/openclaw.json`;
});
const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
@ -29,7 +33,8 @@ vi.mock("../../config/config.js", () => ({
getConfigPath: () => "/tmp/openclaw-test-missing-config.json",
loadConfig: () => configState.cfg,
readConfigFileSnapshot: async () => configState.snapshot,
resolveStateDir: () => "/tmp",
resolveConfigPath: (env: NodeJS.ProcessEnv, stateDir: string) => resolveConfigPath(env, stateDir),
resolveStateDir: (env?: NodeJS.ProcessEnv) => resolveStateDir(env),
resolveGatewayPort: () => 18789,
}));
@ -137,6 +142,16 @@ describe("gateway run option collisions", () => {
waitForPortBindable.mockClear();
ensureDevGatewayConfig.mockClear();
runGatewayLoop.mockClear();
resolveStateDir.mockReset();
resolveStateDir.mockReturnValue("/tmp");
resolveConfigPath.mockReset();
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
return `${stateDir}/openclaw.json`;
});
});
afterEach(() => {
vi.unstubAllEnvs();
});
async function runGatewayCli(argv: string[]) {
@ -154,6 +169,10 @@ describe("gateway run option collisions", () => {
);
}
async function expectGatewayExit(argv: string[]) {
await expect(runGatewayCli(argv)).rejects.toThrow("__exit__:1");
}
it("forwards parent-captured options to `gateway run` subcommand", async () => {
await runGatewayCli([
"gateway",
@ -303,4 +322,44 @@ describe("gateway run option collisions", () => {
expect(runtimeErrors).toContain("Use either --password or --password-file.");
});
it("hard-stops --dev --reset when target resolves to default profile paths", async () => {
vi.stubEnv("HOME", "/Users/test");
resolveStateDir.mockReturnValue("/Users/test/.openclaw");
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
return `${stateDir}/openclaw.json`;
});
await expectGatewayExit(["gateway", "run", "--dev", "--reset"]);
expect(ensureDevGatewayConfig).not.toHaveBeenCalled();
expect(runtimeErrors.join("\n")).toContain(
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
);
expect(runtimeErrors.join("\n")).toContain("/Users/test/.openclaw");
});
it("allows --dev --reset when target resolves to dev profile paths", async () => {
vi.stubEnv("HOME", "/Users/test");
resolveStateDir.mockReturnValue("/Users/test/.openclaw-dev");
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
return `${stateDir}/openclaw.json`;
});
await runGatewayCli(["gateway", "run", "--dev", "--reset", "--allow-unconfigured"]);
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
});
it("allows --dev --reset for explicit non-default custom state/config paths", async () => {
vi.stubEnv("HOME", "/Users/test");
vi.stubEnv("OPENCLAW_STATE_DIR", "/tmp/custom-dev");
vi.stubEnv("OPENCLAW_CONFIG_PATH", "/tmp/custom-dev/openclaw.json");
resolveStateDir.mockReturnValue("/tmp/custom-dev");
resolveConfigPath.mockReturnValue("/tmp/custom-dev/openclaw.json");
await runGatewayCli(["gateway", "run", "--dev", "--reset", "--allow-unconfigured"]);
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
});
});

View File

@ -1,12 +1,13 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { Command } from "commander";
import { readSecretFromFile } from "../../acp/secret-file.js";
import type { GatewayAuthMode, GatewayTailscaleMode } from "../../config/config.js";
import {
CONFIG_PATH,
loadConfig,
readConfigFileSnapshot,
resolveConfigPath,
resolveStateDir,
resolveGatewayPort,
} from "../../config/config.js";
@ -17,6 +18,7 @@ import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import { resolveRequiredHomeDir } from "../../infra/home-dir.js";
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js";
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
@ -25,6 +27,7 @@ import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { inheritOptionFromParent } from "../command-options.js";
import { forceFreePortAndWait, waitForPortBindable } from "../ports.js";
import { isValidProfileName } from "../profile-utils.js";
import { ensureDevGatewayConfig } from "./dev.js";
import { runGatewayLoop } from "./run-loop.js";
import {
@ -157,8 +160,40 @@ function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): Gate
return resolved;
}
function resolveDevResetPaths(env: NodeJS.ProcessEnv = process.env): {
stateDir: string;
configPath: string;
defaultStateDir: string;
defaultConfigPath: string;
expectedDevStateDir: string;
expectedDevConfigPath: string;
} {
const home = resolveRequiredHomeDir(env, os.homedir);
const stateDir = resolveStateDir(env);
const configPath = resolveConfigPath(env, stateDir);
const defaultStateDir = path.join(home, ".openclaw");
const expectedDevStateDir = path.join(home, ".openclaw-dev");
return {
stateDir,
configPath,
defaultStateDir,
defaultConfigPath: path.join(defaultStateDir, "openclaw.json"),
expectedDevStateDir,
expectedDevConfigPath: path.join(expectedDevStateDir, "openclaw.json"),
};
}
async function runGatewayCommand(opts: GatewayRunOpts) {
const isDevProfile = process.env.OPENCLAW_PROFILE?.trim().toLowerCase() === "dev";
const envProfile = process.env.OPENCLAW_PROFILE?.trim();
if (envProfile && !isValidProfileName(envProfile)) {
defaultRuntime.error(
'Invalid OPENCLAW_PROFILE (use letters, numbers, "_", "-" only, or unset the variable).',
);
defaultRuntime.exit(1);
return;
}
const isDevProfile = envProfile?.toLowerCase() === "dev";
const devMode = Boolean(opts.dev) || isDevProfile;
if (opts.reset && !devMode) {
defaultRuntime.error("Use --reset with --dev.");
@ -166,6 +201,37 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
return;
}
if (opts.reset && devMode) {
const paths = resolveDevResetPaths(process.env);
const resolvedStateDir = path.resolve(paths.stateDir);
const resolvedConfigPath = path.resolve(paths.configPath);
const stateIsDefault = resolvedStateDir === path.resolve(paths.defaultStateDir);
const configIsDefault = resolvedConfigPath === path.resolve(paths.defaultConfigPath);
const stateMatchesDev = resolvedStateDir === path.resolve(paths.expectedDevStateDir);
const configMatchesDev = resolvedConfigPath === path.resolve(paths.expectedDevConfigPath);
const hasStateOverride = Boolean(process.env.OPENCLAW_STATE_DIR?.trim());
const hasConfigOverride = Boolean(process.env.OPENCLAW_CONFIG_PATH?.trim());
const hasExplicitCustomTarget =
(hasStateOverride || hasConfigOverride) && !stateIsDefault && !configIsDefault;
if (!hasExplicitCustomTarget && (!stateMatchesDev || !configMatchesDev)) {
defaultRuntime.error(
[
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
`Resolved state dir: ${paths.stateDir}`,
`Resolved config path: ${paths.configPath}`,
`Expected dev state dir: ${paths.expectedDevStateDir}`,
`Expected dev config path: ${paths.expectedDevConfigPath}`,
"Retry with:",
" openclaw --dev gateway --dev --reset",
` OPENCLAW_STATE_DIR="${paths.expectedDevStateDir}" OPENCLAW_CONFIG_PATH="${paths.expectedDevConfigPath}" openclaw gateway --dev --reset`,
].join("\n"),
);
defaultRuntime.exit(1);
return;
}
}
setConsoleTimestampPrefix(true);
setVerbose(Boolean(opts.verbose));
if (opts.claudeCliLogs) {
@ -312,8 +378,10 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
const tokenRaw = toOptionString(opts.token);
const snapshot = await readConfigFileSnapshot().catch(() => null);
const configExists = snapshot?.exists ?? fs.existsSync(CONFIG_PATH);
const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl");
const stateDir = resolveStateDir(process.env);
const configPath = resolveConfigPath(process.env, stateDir);
const configExists = snapshot?.exists ?? fs.existsSync(configPath);
const configAuditPath = path.join(stateDir, "logs", "config-audit.jsonl");
const mode = cfg.gateway?.mode;
if (!opts.allowUnconfigured && mode !== "local") {
if (!configExists) {

View File

@ -1,7 +1,7 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { formatCliCommand } from "./command-format.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
import { applyCliProfileEnv, parseCliProfileArgs, resolveEffectiveCliProfile } from "./profile.js";
describe("parseCliProfileArgs", () => {
it("leaves gateway --dev for subcommands", () => {
@ -28,6 +28,46 @@ describe("parseCliProfileArgs", () => {
expect(res.argv).toEqual(["node", "openclaw", "gateway"]);
});
it("parses --profile after subcommand", () => {
const res = parseCliProfileArgs([
"node",
"openclaw",
"gateway",
"--profile",
"invest",
"--port",
"18795",
]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBe("invest");
expect(res.argv).toEqual(["node", "openclaw", "gateway", "--port", "18795"]);
});
it("parses --profile=NAME after subcommand", () => {
const res = parseCliProfileArgs(["node", "openclaw", "gateway", "--profile=work"]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBe("work");
expect(res.argv).toEqual(["node", "openclaw", "gateway"]);
});
it("keeps --dev and strips --profile when both appear after subcommand", () => {
const res = parseCliProfileArgs(["node", "openclaw", "gateway", "--dev", "--profile", "work"]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBe("work");
expect(res.argv).toEqual(["node", "openclaw", "gateway", "--dev"]);
});
it("rejects --dev before subcommand combined with --profile after subcommand", () => {
const res = parseCliProfileArgs(["node", "openclaw", "--dev", "gateway", "--profile", "work"]);
expect(res.ok).toBe(false);
});
it("parses --profile value and strips it", () => {
const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]);
if (!res.ok) {
@ -66,7 +106,7 @@ describe("applyCliProfileEnv", () => {
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
});
it("does not override explicit env values", () => {
it("does not override explicit OPENCLAW_STATE_DIR env value", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_STATE_DIR: "/custom",
OPENCLAW_GATEWAY_PORT: "19099",
@ -77,10 +117,42 @@ describe("applyCliProfileEnv", () => {
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_STATE_DIR).toBe("/custom");
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19099");
// OPENCLAW_GATEWAY_PORT is intentionally reset for profile isolation.
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join("/custom", "openclaw.json"));
});
it("clears inherited OPENCLAW_GATEWAY_PORT for non-dev profiles", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_GATEWAY_PORT: "18789",
};
applyCliProfileEnv({
profile: "work",
env,
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_GATEWAY_PORT).toBeUndefined();
});
it("clears inherited service env vars for profile isolation", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_GATEWAY_PORT: "18789",
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway",
OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway.service",
OPENCLAW_SERVICE_VERSION: "2026.1.0",
};
applyCliProfileEnv({
profile: "work",
env,
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_GATEWAY_PORT).toBeUndefined();
expect(env.OPENCLAW_LAUNCHD_LABEL).toBeUndefined();
expect(env.OPENCLAW_SYSTEMD_UNIT).toBeUndefined();
expect(env.OPENCLAW_SERVICE_VERSION).toBeUndefined();
expect(env.OPENCLAW_PROFILE).toBe("work");
});
it("uses OPENCLAW_HOME when deriving profile state dir", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_HOME: "/srv/openclaw-home",
@ -100,6 +172,40 @@ describe("applyCliProfileEnv", () => {
});
});
describe("resolveEffectiveCliProfile", () => {
it("prefers parsed profile over env profile", () => {
const res = resolveEffectiveCliProfile({
parsedProfile: "work",
envProfile: "dev",
});
expect(res).toEqual({ ok: true, profile: "work" });
});
it("falls back to env profile when parsed profile is absent", () => {
const res = resolveEffectiveCliProfile({
parsedProfile: null,
envProfile: "dev",
});
expect(res).toEqual({ ok: true, profile: "dev" });
});
it("treats blank env profile as unset", () => {
const res = resolveEffectiveCliProfile({
parsedProfile: null,
envProfile: " ",
});
expect(res).toEqual({ ok: true, profile: null });
});
it("rejects invalid env profile values", () => {
const res = resolveEffectiveCliProfile({
parsedProfile: null,
envProfile: "bad profile",
});
expect(res.ok).toBe(false);
});
});
describe("formatCliCommand", () => {
it.each([
{

View File

@ -7,6 +7,10 @@ export type CliProfileParseResult =
| { ok: true; profile: string | null; argv: string[] }
| { ok: false; error: string };
export type EffectiveCliProfileResult =
| { ok: true; profile: string | null }
| { ok: false; error: string };
function takeValue(
raw: string,
next: string | undefined,
@ -40,7 +44,7 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
continue;
}
if (sawCommand) {
if (sawCommand && arg !== "--profile" && !arg.startsWith("--profile=")) {
out.push(arg);
continue;
}
@ -88,6 +92,27 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
return { ok: true, profile, argv: out };
}
export function resolveEffectiveCliProfile(params: {
parsedProfile: string | null;
envProfile?: string;
}): EffectiveCliProfileResult {
if (params.parsedProfile) {
return { ok: true, profile: params.parsedProfile };
}
const envProfile = params.envProfile?.trim() || "";
if (!envProfile) {
return { ok: true, profile: null };
}
if (!isValidProfileName(envProfile)) {
return {
ok: false,
error:
'Invalid OPENCLAW_PROFILE (use letters, numbers, "_", "-" only, or unset the variable)',
};
}
return { ok: true, profile: envProfile };
}
function resolveProfileStateDir(
profile: string,
env: Record<string, string | undefined>,
@ -110,7 +135,12 @@ export function applyCliProfileEnv(params: {
}
// Convenience only: fill defaults, never override explicit env values.
// Exception: clear inherited service-scoped env vars for profile isolation.
env.OPENCLAW_PROFILE = profile;
delete env.OPENCLAW_GATEWAY_PORT;
delete env.OPENCLAW_LAUNCHD_LABEL;
delete env.OPENCLAW_SYSTEMD_UNIT;
delete env.OPENCLAW_SERVICE_VERSION;
const stateDir = env.OPENCLAW_STATE_DIR?.trim() || resolveProfileStateDir(profile, env, homedir);
if (!env.OPENCLAW_STATE_DIR?.trim()) {
@ -121,7 +151,7 @@ export function applyCliProfileEnv(params: {
env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
}
if (profile === "dev" && !env.OPENCLAW_GATEWAY_PORT?.trim()) {
if (profile === "dev") {
env.OPENCLAW_GATEWAY_PORT = "19001";
}
}

View File

@ -1,7 +1,9 @@
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
normalizeGatewayTokenInput,
openUrl,
resolveResetTargets,
resolveBrowserOpenCommand,
resolveControlUiLinks,
validateGatewayPasswordInput,
@ -153,3 +155,31 @@ describe("validateGatewayPasswordInput", () => {
expect(validateGatewayPasswordInput(" secret ")).toBeUndefined();
});
});
describe("resolveResetTargets", () => {
it("derives reset targets from the active OPENCLAW_STATE_DIR", () => {
const env = {
OPENCLAW_STATE_DIR: "/tmp/custom-openclaw",
} as NodeJS.ProcessEnv;
const targets = resolveResetTargets(env);
const expectedStateDir = path.resolve("/tmp/custom-openclaw");
expect(targets.stateDir).toBe(expectedStateDir);
expect(targets.configPath).toBe(path.join(expectedStateDir, "openclaw.json"));
expect(targets.credentialsPath).toBe(path.join(expectedStateDir, "credentials"));
expect(targets.sessionsDir).toBe(path.join(expectedStateDir, "agents", "main", "sessions"));
});
it("respects OPENCLAW_CONFIG_PATH override at runtime", () => {
const env = {
OPENCLAW_STATE_DIR: "/tmp/custom-openclaw",
OPENCLAW_CONFIG_PATH: "/tmp/alternate/openclaw.json",
} as NodeJS.ProcessEnv;
const targets = resolveResetTargets(env);
expect(targets.stateDir).toBe(path.resolve("/tmp/custom-openclaw"));
expect(targets.configPath).toBe(path.resolve("/tmp/alternate/openclaw.json"));
expect(targets.credentialsPath).toBe(path.resolve("/tmp/custom-openclaw/credentials"));
expect(targets.sessionsDir).toBe(path.resolve("/tmp/custom-openclaw/agents/main/sessions"));
});
});

View File

@ -5,7 +5,7 @@ import { inspect } from "node:util";
import { cancel, isCancel } from "@clack/prompts";
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/workspace.js";
import type { OpenClawConfig } from "../config/config.js";
import { CONFIG_PATH } from "../config/config.js";
import { resolveConfigPath, resolveStateDir } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
@ -17,13 +17,7 @@ import { isWSL } from "../infra/wsl.js";
import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { stylePromptTitle } from "../terminal/prompt-style.js";
import {
CONFIG_DIR,
resolveUserPath,
shortenHomeInString,
shortenHomePath,
sleep,
} from "../utils.js";
import { resolveUserPath, shortenHomeInString, shortenHomePath, sleep } from "../utils.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { VERSION } from "../version.js";
import type { NodeManagerChoice, OnboardMode, ResetScope } from "./onboard-types.js";
@ -329,13 +323,32 @@ export async function moveToTrash(pathname: string, runtime: RuntimeEnv): Promis
}
}
export function resolveResetTargets(
env: NodeJS.ProcessEnv = process.env,
agentId?: string,
): {
stateDir: string;
configPath: string;
credentialsPath: string;
sessionsDir: string;
} {
const stateDir = resolveStateDir(env);
return {
stateDir,
configPath: resolveConfigPath(env, stateDir),
credentialsPath: path.join(stateDir, "credentials"),
sessionsDir: resolveSessionTranscriptsDirForAgent(agentId, env),
};
}
export async function handleReset(scope: ResetScope, workspaceDir: string, runtime: RuntimeEnv) {
await moveToTrash(CONFIG_PATH, runtime);
const targets = resolveResetTargets(process.env);
await moveToTrash(targets.configPath, runtime);
if (scope === "config") {
return;
}
await moveToTrash(path.join(CONFIG_DIR, "credentials"), runtime);
await moveToTrash(resolveSessionTranscriptsDirForAgent(), runtime);
await moveToTrash(targets.credentialsPath, runtime);
await moveToTrash(targets.sessionsDir, runtime);
if (scope === "full") {
await moveToTrash(workspaceDir, runtime);
}

View File

@ -3,9 +3,11 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEFAULT_GATEWAY_PORT,
resolveDefaultConfigCandidates,
resolveConfigPathCandidate,
resolveConfigPath,
resolveGatewayPort,
resolveOAuthDir,
resolveOAuthPath,
resolveStateDir,
@ -150,3 +152,44 @@ describe("state + config path candidates", () => {
});
});
});
describe("resolveGatewayPort", () => {
it("returns DEFAULT_GATEWAY_PORT when no env or config port is set", () => {
expect(resolveGatewayPort(undefined, {} as NodeJS.ProcessEnv)).toBe(DEFAULT_GATEWAY_PORT);
});
it("prefers OPENCLAW_GATEWAY_PORT env var over config", () => {
const env = { OPENCLAW_GATEWAY_PORT: "18790" } as NodeJS.ProcessEnv;
const cfg = { gateway: { port: 19001 } } as Parameters<typeof resolveGatewayPort>[0];
expect(resolveGatewayPort(cfg, env)).toBe(18790);
});
it("prefers CLAWDBOT_GATEWAY_PORT env var over config", () => {
const env = { CLAWDBOT_GATEWAY_PORT: "18791" } as NodeJS.ProcessEnv;
const cfg = { gateway: { port: 19001 } } as Parameters<typeof resolveGatewayPort>[0];
expect(resolveGatewayPort(cfg, env)).toBe(18791);
});
it("falls back to config port when env var is absent", () => {
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
expect(resolveGatewayPort(cfg, {} as NodeJS.ProcessEnv)).toBe(18790);
});
it("falls back to DEFAULT_GATEWAY_PORT when env and config are both missing", () => {
expect(
resolveGatewayPort({} as Parameters<typeof resolveGatewayPort>[0], {} as NodeJS.ProcessEnv),
).toBe(DEFAULT_GATEWAY_PORT);
});
it("ignores invalid env ports and falls back to config", () => {
const env = { OPENCLAW_GATEWAY_PORT: "abc" } as NodeJS.ProcessEnv;
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
expect(resolveGatewayPort(cfg, env)).toBe(18790);
});
it("ignores zero env ports and falls back to config", () => {
const env = { OPENCLAW_GATEWAY_PORT: "0" } as NodeJS.ProcessEnv;
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
expect(resolveGatewayPort(cfg, env)).toBe(18790);
});
});

View File

@ -4,7 +4,11 @@ import { enableCompileCache } from "node:module";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { isRootHelpInvocation, isRootVersionInvocation } from "./cli/argv.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import {
applyCliProfileEnv,
parseCliProfileArgs,
resolveEffectiveCliProfile,
} from "./cli/profile.js";
import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js";
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
@ -112,7 +116,6 @@ if (
}
process.exit(code ?? 1);
});
child.once("error", (error) => {
console.error(
"[openclaw] Failed to respawn CLI:",
@ -173,10 +176,21 @@ if (
process.exit(2);
}
if (parsed.profile) {
applyCliProfileEnv({ profile: parsed.profile });
const effectiveProfile = resolveEffectiveCliProfile({
parsedProfile: parsed.profile,
envProfile: process.env.OPENCLAW_PROFILE,
});
if (!effectiveProfile.ok) {
console.error(`[openclaw] ${effectiveProfile.error}`);
process.exit(2);
}
if (effectiveProfile.profile) {
applyCliProfileEnv({ profile: effectiveProfile.profile });
// Keep Commander and ad-hoc argv checks consistent.
process.argv = parsed.argv;
if (parsed.profile) {
process.argv = parsed.argv;
}
}
if (!tryHandleRootVersionFastPath(process.argv) && !tryHandleRootHelpFastPath(process.argv)) {