fix: preserve wrapper env during gateway reinstall

This commit is contained in:
Peter Steinberger
2026-04-27 03:07:34 +01:00
parent 9f9bd41f40
commit 0b3f13b337
2 changed files with 78 additions and 16 deletions

View File

@@ -30,13 +30,22 @@ const resolveGatewayAuthMock = vi.hoisted(() =>
);
const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn());
const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token"));
const buildGatewayInstallPlanMock = vi.hoisted(() =>
vi.fn(async () => ({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {},
})),
);
const createInstallPlanFixture = vi.hoisted(() => {
return async (params?: { wrapperPath?: string; env?: Record<string, string | undefined> }) => {
const environment: Record<string, string | undefined> = {};
if (params?.wrapperPath || params?.env?.OPENCLAW_WRAPPER) {
environment.OPENCLAW_WRAPPER = params.wrapperPath ?? params.env?.OPENCLAW_WRAPPER;
}
return {
programArguments: params?.wrapperPath
? [params.wrapperPath, "gateway", "run"]
: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment,
};
};
});
const buildGatewayInstallPlanMock = vi.hoisted(() => vi.fn(createInstallPlanFixture));
const parsePortMock = vi.hoisted(() => vi.fn(() => null));
const isGatewayDaemonRuntimeMock = vi.hoisted(() => vi.fn(() => true));
const installDaemonServiceAndEmitMock = vi.hoisted(() => vi.fn(async () => {}));
@@ -108,6 +117,11 @@ vi.mock("../../commands/daemon-install-helpers.js", () => ({
buildGatewayInstallPlan: buildGatewayInstallPlanMock,
}));
vi.mock("../../daemon/program-args.js", () => ({
OPENCLAW_WRAPPER_ENV_KEY: "OPENCLAW_WRAPPER",
resolveOpenClawWrapperPath: async (value: string | undefined) => value?.trim() || undefined,
}));
vi.mock("./shared.js", () => ({
parsePort: parsePortMock,
createDaemonInstallActionContext: (jsonFlag: unknown) => {
@@ -188,6 +202,7 @@ describe("runDaemonInstall", () => {
installDaemonServiceAndEmitMock.mockReset();
service.isLoaded.mockReset();
service.stage.mockReset();
service.readCommand.mockReset();
resetRuntimeCapture();
actionState.warnings.length = 0;
actionState.emitted.length = 0;
@@ -211,11 +226,7 @@ describe("runDaemonInstall", () => {
});
resolveSecretRefValuesMock.mockResolvedValue(new Map());
randomTokenMock.mockReturnValue("generated-token");
buildGatewayInstallPlanMock.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {},
});
buildGatewayInstallPlanMock.mockImplementation(createInstallPlanFixture);
parsePortMock.mockReturnValue(null);
isGatewayDaemonRuntimeMock.mockReturnValue(true);
installDaemonServiceAndEmitMock.mockResolvedValue(undefined);
@@ -402,6 +413,50 @@ describe("runDaemonInstall", () => {
expect(actionState.emitted.at(-1)).toMatchObject({ result: "already-installed" });
});
it("preserves wrapper env from an installed but unloaded service during forced reinstall", async () => {
service.isLoaded.mockResolvedValue(false);
service.readCommand.mockResolvedValue({
programArguments: ["/usr/local/bin/openclaw-doppler", "gateway", "run"],
environment: {
OPENCLAW_WRAPPER: "/usr/local/bin/openclaw-doppler",
},
} as never);
await runDaemonInstall({ json: true, force: true });
expect(service.readCommand).toHaveBeenCalledTimes(1);
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
wrapperPath: "/usr/local/bin/openclaw-doppler",
existingEnvironment: expect.objectContaining({
OPENCLAW_WRAPPER: "/usr/local/bin/openclaw-doppler",
}),
env: expect.objectContaining({
OPENCLAW_WRAPPER: "/usr/local/bin/openclaw-doppler",
}),
}),
);
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
it("reinstalls when wrapper command matches but wrapper env is missing", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments: ["/usr/local/bin/openclaw-doppler", "gateway", "run"],
environment: {},
} as never);
await runDaemonInstall({
json: true,
wrapper: "/usr/local/bin/openclaw-doppler",
});
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
expect(actionState.warnings).toContain(
"Gateway service OPENCLAW_WRAPPER differs from the current wrapper install plan; refreshing the install.",
);
});
it("reinstalls when the embedded gateway token differs from the install plan", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({

View File

@@ -135,10 +135,8 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
return;
}
}
if (loaded) {
existingServiceCommand = await service.readCommand(process.env).catch(() => null);
existingServiceEnv = existingServiceCommand?.environment;
}
existingServiceCommand = await service.readCommand(process.env).catch(() => null);
existingServiceEnv = existingServiceCommand?.environment;
const installEnv = mergeInstallInvocationEnv({
env: process.env,
existingServiceEnv,
@@ -294,6 +292,15 @@ async function getGatewayServiceAutoRefreshMessage(params: {
) {
return "Gateway service command differs from the current wrapper install plan; refreshing the install.";
}
const plannedWrapperPath = normalizeOptionalString(
plannedInstall.environment[OPENCLAW_WRAPPER_ENV_KEY],
);
const currentWrapperPath = normalizeOptionalString(
currentCommand.environment?.[OPENCLAW_WRAPPER_ENV_KEY],
);
if (plannedWrapperPath !== currentWrapperPath) {
return `Gateway service ${OPENCLAW_WRAPPER_ENV_KEY} differs from the current wrapper install plan; refreshing the install.`;
}
}
const currentExecPath = currentCommand.programArguments[0]?.trim();
if (!currentExecPath) {