mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(cli): replace stale service selectors on profile switch (#124790)
* fix(cli): replace stale service profile selectors Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * refactor(cli): own service identity checks with daemon constants Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * fix(update): preserve managed home selector ownership Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * test(update): keep selector coverage within limits --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
e84a45e28e
commit
67ea9c2281
@@ -1,6 +1,7 @@
|
||||
// Profile CLI tests cover profile selection, persistence, and command wiring.
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveGatewayPort } from "../config/paths.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
|
||||
|
||||
@@ -306,6 +307,98 @@ describe("applyCliProfileEnv", () => {
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join("/custom", "openclaw.json"));
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "default service to named profile", inheritedProfile: undefined, selected: "work" },
|
||||
{ name: "named service to different profile", inheritedProfile: "main", selected: "work" },
|
||||
{ name: "named service to dev", inheritedProfile: "main", selected: "dev" },
|
||||
])("replaces the complete service selector bundle: $name", ({ inheritedProfile, selected }) => {
|
||||
const inheritedStateDir = inheritedProfile
|
||||
? `/home/peter/.openclaw-${inheritedProfile}`
|
||||
: "/home/peter/.openclaw";
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_PROFILE: inheritedProfile,
|
||||
OPENCLAW_STATE_DIR: inheritedStateDir,
|
||||
OPENCLAW_CONFIG_PATH: path.join(inheritedStateDir, "openclaw.json"),
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
OPENCLAW_LAUNCHD_LABEL: inheritedProfile
|
||||
? `ai.openclaw.${inheritedProfile}`
|
||||
: "ai.openclaw.gateway",
|
||||
OPENCLAW_SYSTEMD_UNIT: inheritedProfile
|
||||
? `openclaw-gateway-${inheritedProfile}.service`
|
||||
: "openclaw-gateway.service",
|
||||
OPENCLAW_WINDOWS_TASK_NAME: inheritedProfile
|
||||
? `OpenClaw Gateway (${inheritedProfile})`
|
||||
: "OpenClaw Gateway",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
};
|
||||
|
||||
applyCliProfileEnv({ profile: selected, env, homedir: () => "/home/peter" });
|
||||
|
||||
expect(env.OPENCLAW_PROFILE).toBe(selected);
|
||||
expect(env.OPENCLAW_STATE_DIR).toBe(`/home/peter/.openclaw-${selected}`);
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe(selected === "dev" ? "19001" : undefined);
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBeUndefined();
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBeUndefined();
|
||||
expect(env.OPENCLAW_WINDOWS_TASK_NAME).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets selected config or profile derivation resolve the port after stale service removal", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_PROFILE: "main",
|
||||
OPENCLAW_STATE_DIR: "/home/peter/.openclaw-main",
|
||||
OPENCLAW_CONFIG_PATH: "/home/peter/.openclaw-main/openclaw.json",
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.main",
|
||||
OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway-main.service",
|
||||
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway (main)",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
};
|
||||
|
||||
applyCliProfileEnv({ profile: "work", env, homedir: () => "/home/peter" });
|
||||
|
||||
expect(resolveGatewayPort({ gateway: { port: 21999 } }, env)).toBe(21999);
|
||||
expect(resolveGatewayPort(undefined, env)).not.toBe(18789);
|
||||
});
|
||||
|
||||
it("supports legacy gateway services without a service kind", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_PROFILE: "main",
|
||||
OPENCLAW_STATE_DIR: "/home/peter/.openclaw-main",
|
||||
OPENCLAW_CONFIG_PATH: "/home/peter/.openclaw-main/openclaw.json",
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
};
|
||||
|
||||
applyCliProfileEnv({ profile: "work", env, homedir: () => "/home/peter" });
|
||||
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves node service selectors when selecting a CLI profile", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_PROFILE: "main",
|
||||
OPENCLAW_STATE_DIR: "/home/peter/.openclaw-main",
|
||||
OPENCLAW_CONFIG_PATH: "/home/peter/.openclaw-main/openclaw.json",
|
||||
OPENCLAW_GATEWAY_PORT: "19999",
|
||||
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node",
|
||||
OPENCLAW_SYSTEMD_UNIT: "openclaw-node.service",
|
||||
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Node",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "node",
|
||||
};
|
||||
|
||||
applyCliProfileEnv({ profile: "work", env, homedir: () => "/home/peter" });
|
||||
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19999");
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.node");
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-node.service");
|
||||
expect(env.OPENCLAW_WINDOWS_TASK_NAME).toBe("OpenClaw Node");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "the default profile without a profile marker",
|
||||
|
||||
+28
-14
@@ -3,6 +3,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
GATEWAY_SERVICE_SELECTOR_ENV_KEYS,
|
||||
isGatewayServiceEnv,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
@@ -91,6 +93,12 @@ export function applyCliProfileEnv(params: {
|
||||
const inheritedProfileStateDir = resolveProfileStateDir(inheritedProfile, profileEnv, homedir);
|
||||
const selectedProfileStateDir = resolveProfileStateDir(profile, profileEnv, homedir);
|
||||
const switchesInheritedProfile = inheritedProfileStateDir !== selectedProfileStateDir;
|
||||
const inheritedSystemdServiceName = resolveGatewaySystemdServiceName(inheritedProfile);
|
||||
const inheritedServiceSelectors = {
|
||||
OPENCLAW_LAUNCHD_LABEL: [resolveGatewayLaunchAgentLabel(inheritedProfile)],
|
||||
OPENCLAW_SYSTEMD_UNIT: [inheritedSystemdServiceName, `${inheritedSystemdServiceName}.service`],
|
||||
OPENCLAW_WINDOWS_TASK_NAME: [resolveGatewayWindowsTaskName(inheritedProfile)],
|
||||
};
|
||||
const switchesInheritedProfileState = Boolean(
|
||||
existingStateDir &&
|
||||
switchesInheritedProfile &&
|
||||
@@ -108,32 +116,38 @@ export function applyCliProfileEnv(params: {
|
||||
homedir,
|
||||
}) === path.join(inheritedProfileStateDir, "openclaw.json"),
|
||||
);
|
||||
const inheritedManagedServiceSelectors =
|
||||
switchesInheritedProfile &&
|
||||
isGatewayServiceEnv(env) &&
|
||||
switchesInheritedProfileState &&
|
||||
replacesInheritedProfileConfig;
|
||||
|
||||
if (inheritedManagedServiceSelectors) {
|
||||
for (const key of GATEWAY_SERVICE_SELECTOR_ENV_KEYS) {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
|
||||
// A service's canonical profile paths are inherited defaults, not custom overrides.
|
||||
// Switch them together so an explicit profile cannot mutate the service's profile.
|
||||
env.OPENCLAW_PROFILE = profile;
|
||||
|
||||
const retainedStateDir = inheritedManagedServiceSelectors ? undefined : existingStateDir;
|
||||
const stateDir =
|
||||
existingStateDir && !switchesInheritedProfileState ? existingStateDir : selectedProfileStateDir;
|
||||
if (!existingStateDir || switchesInheritedProfileState) {
|
||||
retainedStateDir && !switchesInheritedProfileState ? retainedStateDir : selectedProfileStateDir;
|
||||
if (!retainedStateDir || switchesInheritedProfileState) {
|
||||
env.OPENCLAW_STATE_DIR = stateDir;
|
||||
}
|
||||
|
||||
if (!existingConfigPath || replacesInheritedProfileConfig) {
|
||||
if (
|
||||
!inheritedManagedServiceSelectors &&
|
||||
(!existingConfigPath || replacesInheritedProfileConfig)
|
||||
) {
|
||||
env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
|
||||
}
|
||||
|
||||
if (switchesInheritedProfile) {
|
||||
const inheritedSystemdServiceName = resolveGatewaySystemdServiceName(inheritedProfile);
|
||||
const inheritedServiceIdentities = {
|
||||
OPENCLAW_LAUNCHD_LABEL: [resolveGatewayLaunchAgentLabel(inheritedProfile)],
|
||||
OPENCLAW_SYSTEMD_UNIT: [
|
||||
inheritedSystemdServiceName,
|
||||
`${inheritedSystemdServiceName}.service`,
|
||||
],
|
||||
OPENCLAW_WINDOWS_TASK_NAME: [resolveGatewayWindowsTaskName(inheritedProfile)],
|
||||
};
|
||||
for (const [key, inheritedValues] of Object.entries(inheritedServiceIdentities)) {
|
||||
if (switchesInheritedProfile && !inheritedManagedServiceSelectors) {
|
||||
for (const [key, inheritedValues] of Object.entries(inheritedServiceSelectors)) {
|
||||
const activeValue = normalizeOptionalString(env[key]);
|
||||
if (activeValue && inheritedValues.includes(activeValue)) {
|
||||
delete env[key];
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import path from "node:path";
|
||||
import { GATEWAY_SERVICE_SELECTOR_ENV_KEYS } from "../../daemon/constants.js";
|
||||
|
||||
const SERVICE_REFRESH_PATH_ENV_KEYS = [
|
||||
"OPENCLAW_HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
] as const;
|
||||
|
||||
const MANAGED_SERVICE_SELECTOR_ENV_KEYS = [
|
||||
...SERVICE_REFRESH_PATH_ENV_KEYS,
|
||||
"OPENCLAW_PROFILE",
|
||||
"OPENCLAW_GATEWAY_PORT",
|
||||
const MANAGED_UPDATE_SELECTOR_ENV_KEYS = [
|
||||
"OPENCLAW_HOME",
|
||||
...GATEWAY_SERVICE_SELECTOR_ENV_KEYS,
|
||||
] as const;
|
||||
|
||||
function applyManagedServiceSelectorEnv(params: {
|
||||
@@ -19,7 +18,7 @@ function applyManagedServiceSelectorEnv(params: {
|
||||
}): NodeJS.ProcessEnv {
|
||||
const resolved = { ...params.baseEnv };
|
||||
const selectorEnv = params.selectorEnv ?? params.serviceEnv;
|
||||
for (const key of MANAGED_SERVICE_SELECTOR_ENV_KEYS) {
|
||||
for (const key of MANAGED_UPDATE_SELECTOR_ENV_KEYS) {
|
||||
if (selectorEnv[key]?.trim()) {
|
||||
resolved[key] = params.serviceEnv[key];
|
||||
} else {
|
||||
|
||||
@@ -16,11 +16,7 @@ import { doctorCommand } from "../../commands/doctor.js";
|
||||
import { UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV } from "../../commands/doctor/shared/update-phase.js";
|
||||
import { resolveGatewayPort } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
GATEWAY_SERVICE_RUNTIME_PID_ENV,
|
||||
} from "../../daemon/constants.js";
|
||||
import { GATEWAY_SERVICE_RUNTIME_PID_ENV, isGatewayServiceEnv } from "../../daemon/constants.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
|
||||
import {
|
||||
@@ -601,11 +597,7 @@ export async function maybeRestartServiceAfterFailedMutableUpdate(params: {
|
||||
function isRunningInsideGatewayService(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): boolean {
|
||||
if (env.OPENCLAW_SERVICE_MARKER?.trim() !== GATEWAY_SERVICE_MARKER) {
|
||||
return false;
|
||||
}
|
||||
const serviceKind = env.OPENCLAW_SERVICE_KIND?.trim();
|
||||
return !serviceKind || serviceKind === GATEWAY_SERVICE_KIND;
|
||||
return isGatewayServiceEnv(env);
|
||||
}
|
||||
|
||||
export function shouldBlockMutableUpdateFromGatewayServiceEnv(params: {
|
||||
|
||||
@@ -263,11 +263,13 @@ describe("resolvePostInstallDoctorEnv", () => {
|
||||
OPENCLAW_STATE_DIR: "/wrong/state",
|
||||
OPENCLAW_CONFIG_PATH: "/wrong/openclaw.json",
|
||||
OPENCLAW_PROFILE: "wrong",
|
||||
OPENCLAW_SYSTEMD_UNIT: "wrong.service",
|
||||
},
|
||||
serviceEnv: {
|
||||
OPENCLAW_STATE_DIR: "daemon-state",
|
||||
OPENCLAW_CONFIG_PATH: "daemon-state/openclaw.json",
|
||||
OPENCLAW_PROFILE: "work",
|
||||
OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway-work.service",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -278,6 +280,7 @@ describe("resolvePostInstallDoctorEnv", () => {
|
||||
path.join("/srv/openclaw", "daemon-state", "openclaw.json"),
|
||||
);
|
||||
expect(env.OPENCLAW_PROFILE).toBe("work");
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway-work.service");
|
||||
});
|
||||
|
||||
it("keeps the caller env when no managed service env is available", () => {
|
||||
@@ -321,6 +324,7 @@ describe("resolveUpdatedInstallCommandEnv", () => {
|
||||
const env = resolveOwnedManagedUpdateEnv({
|
||||
processEnv: {
|
||||
HOME: "/home/operator",
|
||||
OPENCLAW_HOME: "/home/operator/openclaw-home",
|
||||
OPENCLAW_PROFILE: "personal",
|
||||
OPENCLAW_STATE_DIR: "/home/operator/.openclaw-personal",
|
||||
OPENCLAW_CONFIG_PATH: "/home/operator/.openclaw-personal/openclaw.json",
|
||||
@@ -328,6 +332,7 @@ describe("resolveUpdatedInstallCommandEnv", () => {
|
||||
},
|
||||
serviceEnv: {
|
||||
HOME: "/home/operator",
|
||||
OPENCLAW_HOME: "/home/operator/openclaw-home",
|
||||
OPENCLAW_PROFILE: "personal",
|
||||
OPENCLAW_STATE_DIR: "/home/operator/.openclaw-personal",
|
||||
OPENCLAW_CONFIG_PATH: "/home/operator/.openclaw-personal/openclaw.json",
|
||||
@@ -337,6 +342,7 @@ describe("resolveUpdatedInstallCommandEnv", () => {
|
||||
});
|
||||
|
||||
expect(env.HOME).toBe("/home/operator");
|
||||
expect(env.OPENCLAW_HOME).toBeUndefined();
|
||||
expect(env.OPENCLAW_PROFILE).toBeUndefined();
|
||||
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
|
||||
@@ -8,6 +8,24 @@ const GATEWAY_WINDOWS_TASK_NAME = "OpenClaw Gateway";
|
||||
export const GATEWAY_SERVICE_MARKER = "openclaw";
|
||||
export const GATEWAY_SERVICE_KIND = "gateway";
|
||||
export const GATEWAY_SERVICE_RUNTIME_PID_ENV = "OPENCLAW_GATEWAY_SERVICE_PID";
|
||||
export const GATEWAY_SERVICE_SELECTOR_ENV_KEYS = [
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
"OPENCLAW_PROFILE",
|
||||
"OPENCLAW_GATEWAY_PORT",
|
||||
"OPENCLAW_LAUNCHD_LABEL",
|
||||
"OPENCLAW_SYSTEMD_UNIT",
|
||||
"OPENCLAW_WINDOWS_TASK_NAME",
|
||||
] as const;
|
||||
|
||||
export function isGatewayServiceEnv(env: Record<string, string | undefined>): boolean {
|
||||
if (env.OPENCLAW_SERVICE_MARKER?.trim() !== GATEWAY_SERVICE_MARKER) {
|
||||
return false;
|
||||
}
|
||||
const serviceKind = env.OPENCLAW_SERVICE_KIND?.trim();
|
||||
return !serviceKind || serviceKind === GATEWAY_SERVICE_KIND;
|
||||
}
|
||||
|
||||
const NODE_LAUNCH_AGENT_LABEL = "ai.openclaw.node";
|
||||
const NODE_SYSTEMD_SERVICE_NAME = "openclaw-node";
|
||||
const NODE_WINDOWS_TASK_NAME = "OpenClaw Node";
|
||||
|
||||
Reference in New Issue
Block a user