openclaw / src /cli /daemon-cli /install.reinstall.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw
History Blame Contribute Delete
15.6 kB
import "./install.test-support.js";
import { describe, expect, it } from "vitest";
import { resolveTestNodeExecPath } from "../../test-utils/node-process.js";
import { createInstallPlanFixture, nodeProbeOutput } from "./install.test-helpers.js";
const {
actionState,
buildGatewayInstallPlanMock,
expectFields,
expectLastEmittedResult,
installDaemonServiceAndEmitMock,
readConfigFileSnapshotMock,
readFirstInstallPlanArg,
readFirstNodeStartupTlsEnvironmentArg,
replaceConfigFileMock,
resolveNodeStartupTlsEnvironmentMock,
runDaemonInstall,
runExecMock,
service,
setupInstallTests,
} = await import("./install.test-support.js");
describe("runDaemonInstall reinstall", () => {
setupInstallTests();
it.each([
{ mode: "local", installedOverride: false, plannedOverride: false },
{ mode: "remote", installedOverride: true, plannedOverride: true },
{ mode: "local", installedOverride: true, plannedOverride: false },
])(
"refreshes only changed service start mode with $mode primary",
async ({ mode, installedOverride, plannedOverride }) => {
service.isLoaded.mockResolvedValue(true);
readConfigFileSnapshotMock.mockResolvedValue({
valid: true,
sourceConfig: { gateway: { mode, auth: { mode: "token" } } },
});
const command = (override: boolean) =>
["openclaw", "gateway", "run"].concat(override ? ["--allow-unconfigured"] : []);
const environment = { NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt" };
resolveNodeStartupTlsEnvironmentMock.mockReturnValue(environment);
service.readCommand.mockResolvedValue({
programArguments: command(installedOverride),
environment,
});
buildGatewayInstallPlanMock.mockResolvedValue({
programArguments: command(plannedOverride),
environment,
workingDirectory: "/tmp",
});
await runDaemonInstall({ json: true });
expect(actionState.failed).toEqual([]);
if (installedOverride !== plannedOverride) {
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledOnce();
} else {
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
expect(actionState.emitted.at(-1)).toMatchObject({ result: "already-installed" });
}
},
);
it.each([
{ failure: "probe", message: "openclaw gateway install --force" },
{ failure: "no-replacement", message: "No supported Node runtime is available" },
{ failure: "sealed-definition", message: "SERVICE_DEFINITION_UNKNOWN" },
])(
"refuses runtime repair on $failure without claiming success",
async ({ failure, message }) => {
service.isLoaded.mockResolvedValue(true);
const oldNode = failure === "probe" ? resolveTestNodeExecPath() : "/opt/old/bin/node";
service.readCommand.mockResolvedValue({
programArguments: [oldNode, "/opt/openclaw/dist/index.js", "gateway"],
});
runExecMock.mockImplementation(async (file: string) => {
if (failure === "probe") {
throw new Error("runtime probe timed out");
}
return nodeProbeOutput(
failure === "sealed-definition" && file !== oldNode ? "26.8.1" : "22.23.1",
);
});
if (failure === "sealed-definition") {
service.readDefinitionMutationCapability.mockRejectedValue(new Error("sealed"));
}
await runDaemonInstall({ json: true });
expect(actionState.failed[0]?.message).toContain(message);
expect(actionState.emitted).toEqual([]);
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
},
);
it("reinstalls when the loaded service still embeds OPENCLAW_GATEWAY_TOKEN", async () => {
const programArguments = [
"/usr/bin/node",
"--max-old-space-size=24576",
"--require=/tmp/service-preload.js",
"/usr/local/bin/openclaw",
"gateway",
];
service.isLoaded.mockResolvedValue(true);
const managedDefinition = {
programArguments,
environment: {
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
},
};
const existingCommand = {
...managedDefinition,
environment: { NODE_OPTIONS: "--max-old-space-size=512" },
managedDefinition,
managedOverrides: { environment: { keys: ["NODE_OPTIONS"] } },
};
service.readCommand.mockResolvedValue(existingCommand as never);
await runDaemonInstall({ json: true });
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
for (const [options] of buildGatewayInstallPlanMock.mock.calls) {
expect(options).toEqual(expect.objectContaining({ existingCommand }));
}
expect(actionState.warnings).toContain(
"Gateway service OPENCLAW_GATEWAY_TOKEN differs from the current install plan; refreshing the install.",
);
});
it("returns already-installed when the embedded gateway token matches the install plan", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENCLAW_GATEWAY_TOKEN: "durable-token",
},
} as never);
buildGatewayInstallPlanMock.mockResolvedValueOnce({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {
OPENCLAW_GATEWAY_TOKEN: "durable-token",
},
});
await runDaemonInstall({ json: true });
expect(buildGatewayInstallPlanMock).toHaveBeenCalledTimes(1);
expect(replaceConfigFileMock).not.toHaveBeenCalled();
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
expectLastEmittedResult("already-installed");
});
it("preserves managed base wrapper, environment, and provenance during forced reinstall", async () => {
for (const key of ["OPENAI_API_KEY", "OPENCLAW_WRAPPER"]) {
delete process.env[key];
}
const environment = {
OPENAI_API_KEY: "managed-service-key",
OPENCLAW_WRAPPER: "/usr/local/bin/openclaw-doppler",
};
const environmentValueSources = {
OPENAI_API_KEY: "file",
OPENCLAW_WRAPPER: "inline",
};
service.isLoaded.mockResolvedValue(false);
service.readCommand.mockResolvedValue({
programArguments: ["/operator/drop-in-wrapper", "gateway", "run"],
environment: {
OPENAI_API_KEY: "operator-drop-in-key",
OPENCLAW_WRAPPER: "/operator/drop-in-wrapper",
},
environmentValueSources: { OPENAI_API_KEY: "inline" },
managedDefinition: {
programArguments: [environment.OPENCLAW_WRAPPER, "gateway", "run"],
environment,
environmentValueSources,
},
} as never);
await runDaemonInstall({ json: true, force: true });
expect(service.readCommand).toHaveBeenCalledTimes(1);
const installPlanArg = readFirstInstallPlanArg();
expectFields(installPlanArg, {
wrapperPath: environment.OPENCLAW_WRAPPER,
existingEnvironment: environment,
existingEnvironmentValueSources: environmentValueSources,
});
expectFields(installPlanArg.env, environment);
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
it("preserves generated-service CA trust without unsafe overrides during forced reinstall", async () => {
const extraCaCerts = "/opt/openclaw/corporate-ca.pem";
const programArguments = [
"/usr/bin/node",
"--max-old-space-size=24576",
"--require=/tmp/service-preload.js",
"/usr/local/bin/openclaw",
"gateway",
];
for (const key of [
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"HTTPS_PROXY",
"NODE_OPTIONS",
"BASH_ENV",
"LD_PRELOAD",
]) {
delete process.env[key];
}
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments,
environment: {
NODE_EXTRA_CA_CERTS: extraCaCerts,
NODE_TLS_REJECT_UNAUTHORIZED: "0",
HTTPS_PROXY: "https://attacker.invalid",
NODE_OPTIONS: "--require /tmp/untrusted.js",
BASH_ENV: "/tmp/untrusted.sh",
LD_PRELOAD: "/tmp/untrusted.so",
},
environmentValueSources: {
NODE_EXTRA_CA_CERTS: "file",
},
} as never);
buildGatewayInstallPlanMock.mockImplementationOnce(async (params) => {
const plan = await createInstallPlanFixture(params);
return {
...plan,
environment: {
...plan.environment,
NODE_EXTRA_CA_CERTS: params?.env?.NODE_EXTRA_CA_CERTS ?? "/etc/ssl/cert.pem",
},
};
});
installDaemonServiceAndEmitMock.mockImplementationOnce(async (params?: unknown) => {
await (params as { install: () => Promise<void> }).install();
});
await runDaemonInstall({ json: true, force: true });
const installPlanArg = readFirstInstallPlanArg();
expect(installPlanArg.existingCommand).toEqual(expect.objectContaining({ programArguments }));
const installEnv = installPlanArg.env as Record<string, string | undefined>;
expect(installEnv.NODE_EXTRA_CA_CERTS).toBe(extraCaCerts);
expect(installEnv.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
expect(installEnv.HTTPS_PROXY).toBeUndefined();
expect(installEnv.NODE_OPTIONS).toBeUndefined();
expect(installEnv.BASH_ENV).toBeUndefined();
expect(installEnv.LD_PRELOAD).toBeUndefined();
expectFields(installPlanArg.existingEnvironmentValueSources, {
NODE_EXTRA_CA_CERTS: "file",
});
const installCalls = service.install.mock.calls as unknown as Array<
[{ environment?: Record<string, string | undefined> }]
>;
expect(installCalls[0]?.[0].environment?.NODE_EXTRA_CA_CERTS).toBe(extraCaCerts);
});
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({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
},
} as never);
buildGatewayInstallPlanMock.mockResolvedValueOnce({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {
OPENCLAW_GATEWAY_TOKEN: "fresh-token",
},
});
await runDaemonInstall({ json: true });
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
expect(actionState.warnings).toContain(
"Gateway service OPENCLAW_GATEWAY_TOKEN differs from the current install plan; refreshing the install.",
);
});
it.each([
{ name: "an env file", source: "file", operatorOwned: false },
{ name: "an operator-only drop-in", source: "inline", operatorOwned: true },
])("does not reinstall when OPENCLAW_GATEWAY_TOKEN comes from $name", async (testCase) => {
service.isLoaded.mockResolvedValue(true);
const programArguments = ["openclaw", "gateway", "run"];
service.readCommand.mockResolvedValue({
programArguments,
environment: { OPENCLAW_GATEWAY_TOKEN: "operator-token" },
environmentValueSources: { OPENCLAW_GATEWAY_TOKEN: testCase.source },
...(testCase.operatorOwned && {
managedDefinition: { programArguments, environment: {} },
}),
} as never);
await runDaemonInstall({ json: true });
expect(buildGatewayInstallPlanMock).not.toHaveBeenCalled();
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
expectLastEmittedResult("already-installed");
});
it("reinstalls when an existing service is missing the nvm TLS CA bundle", async () => {
service.isLoaded.mockResolvedValue(true);
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
NODE_USE_SYSTEM_CA: undefined,
});
service.readCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {},
} as never);
await runDaemonInstall({ json: true });
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
it("reinstalls when the installed service still runs from nvm even if the installer runtime does not", async () => {
service.isLoaded.mockResolvedValue(true);
resolveNodeStartupTlsEnvironmentMock.mockImplementation(({ execPath }) => ({
NODE_EXTRA_CA_CERTS:
typeof execPath === "string" && execPath.includes("/.nvm/")
? "/etc/ssl/certs/ca-certificates.crt"
: undefined,
NODE_USE_SYSTEM_CA: undefined,
}));
service.readCommand.mockResolvedValue({
programArguments: ["/home/test/.nvm/versions/node/v22.19.0/bin/node", "dist/entry.js"],
environment: {},
} as never);
await runDaemonInstall({ json: true });
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
expectFields(readFirstNodeStartupTlsEnvironmentArg(), {
execPath: "/home/test/.nvm/versions/node/v22.19.0/bin/node",
});
});
it("reuses env-backed service secrets during forced reinstall when the current shell is missing them", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENAI_API_KEY: "service-openai-key",
},
} as never);
delete process.env.OPENAI_API_KEY;
process.env.NODE_OPTIONS = "--require /tmp/untrusted.js";
await runDaemonInstall({ json: true, force: true });
expectFields(readFirstInstallPlanArg().env, {
OPENAI_API_KEY: "service-openai-key",
});
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
it("does not reuse stale service control env during forced reinstall", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENCLAW_STATE_DIR: "/tmp/openclaw-doctor-manual",
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-doctor-manual/openclaw.json",
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
PATH: "/tmp/doctor-bin:/usr/bin",
NODE_OPTIONS: "--require /tmp/evil.js",
OPENAI_API_KEY: "service-openai-key",
},
} as never);
delete process.env.OPENAI_API_KEY;
await runDaemonInstall({ json: true, force: true });
expectFields(readFirstInstallPlanArg().env, {
OPENAI_API_KEY: "service-openai-key",
});
const env = readFirstInstallPlanArg().env as Record<string, string | undefined>;
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
expect(env.NODE_OPTIONS).toBeUndefined();
expect(env.PATH).not.toContain("/tmp/doctor-bin");
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
});