openclaw / src /cli /daemon-cli /install.test-support.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw
History Blame Contribute Delete
10.4 kB
// Daemon install tests cover service install command behavior and plan handling.
import { afterEach, beforeEach, expect, vi } from "vitest";
import type { SecretInput } from "../../config/types.secrets.js";
import type { GatewayServiceCommandConfig } from "../../daemon/service.js";
import { mockSystemAccountHome } from "../../daemon/service.test-helpers.js";
import type { ResolvedGatewayAuth } from "../../gateway/auth.js";
import { captureFullEnv } from "../../test-utils/env.js";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
import { createInstallPlanFixture, nodeProbeOutput } from "./install.test-helpers.js";
import type { createDaemonInstallActionContext } from "./shared.js";
type DaemonActionResponse = Parameters<
ReturnType<typeof createDaemonInstallActionContext>["emit"]
>[0];
const resolveNodeStartupTlsEnvironmentMock = vi.hoisted(() => vi.fn());
const runExecMock = vi.hoisted(() => vi.fn());
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
const replaceConfigFileMock = vi.hoisted(() => vi.fn());
const resolveGatewayAuthMock = vi.hoisted(() => vi.fn<() => ResolvedGatewayAuth>());
const resolveGatewayBindHostMock = vi.hoisted(() => vi.fn(async () => "127.0.0.1"));
const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn());
const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token"));
const buildGatewayInstallPlanMock = vi.hoisted(() => vi.fn<typeof createInstallPlanFixture>());
const parsePortMock = vi.hoisted(() => vi.fn(() => null));
const isGatewayDaemonRuntimeMock = vi.hoisted(() => vi.fn(() => true));
const installDaemonServiceAndEmitMock = vi.hoisted(() => vi.fn(async (_params?: unknown) => {}));
const actionState = vi.hoisted(() => ({
warnings: [] as string[],
emitted: [] as DaemonActionResponse[],
failed: [] as Array<{ message: string; hints?: string[] }>,
}));
const service = vi.hoisted(() => ({
label: "Gateway",
loadedText: "loaded",
notLoadedText: "not loaded",
isLoaded: vi.fn(async () => false),
stage: vi.fn(async () => {}),
install: vi.fn(async () => {}),
uninstall: vi.fn(async () => {}),
restart: vi.fn(async () => {}),
stop: vi.fn(async () => {}),
readDefinitionMutationCapability: vi.fn(async () => ({ kind: "writable" as const })),
readCommand: vi.fn<() => Promise<GatewayServiceCommandConfig | null>>(async () => null),
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
}));
vi.mock("../../bootstrap/node-startup-env.js", () => ({
resolveNodeStartupTlsEnvironment: resolveNodeStartupTlsEnvironmentMock,
}));
vi.mock("../../process/exec.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../process/exec.js")>()),
runExec: runExecMock,
}));
vi.mock("../../config/io.js", () => ({
readConfigFileSnapshotForWrite: vi.fn(async () => ({
snapshot: await readConfigFileSnapshotMock(),
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
})),
}));
vi.mock("../../config/mutate.js", () => ({
replaceConfigFile: replaceConfigFileMock,
}));
vi.mock("../../config/paths.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../config/paths.js")>()),
resolveGatewayPort: resolveGatewayPortMock,
}));
vi.mock("../../gateway/auth.js", () => ({
resolveGatewayAuth: resolveGatewayAuthMock,
}));
vi.mock("../../gateway/net.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../gateway/net.js")>();
return {
...actual,
resolveGatewayBindHost: resolveGatewayBindHostMock,
};
});
vi.mock("../../secrets/resolve.js", () => ({
resolveSecretRefValues: resolveSecretRefValuesMock,
}));
vi.mock("../../commands/random-token.js", () => ({
randomToken: randomTokenMock,
}));
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", async (importOriginal) => ({
...(await importOriginal<typeof import("./shared.js")>()),
parsePort: parsePortMock,
createDaemonInstallActionContext: (jsonFlag: unknown) => {
const json = Boolean(jsonFlag);
return {
json,
stdout: process.stdout,
warnings: actionState.warnings,
emit: (payload: DaemonActionResponse) => {
actionState.emitted.push(payload);
},
fail: (message: string, hints?: string[]) => {
actionState.failed.push({ message, hints });
},
};
},
}));
vi.mock("../../commands/daemon-runtime.js", () => ({
DEFAULT_GATEWAY_DAEMON_RUNTIME: "node",
isGatewayDaemonRuntime: isGatewayDaemonRuntimeMock,
}));
vi.mock("../../daemon/service.js", () => ({
resolveGatewayService: () => service,
}));
vi.mock("./response.js", () => ({
buildDaemonServiceSnapshot: vi.fn(),
installDaemonServiceAndEmit: installDaemonServiceAndEmitMock,
}));
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../runtime.js", () => ({
defaultRuntime,
}));
function expectFirstInstallPlanCallOmitsToken() {
expect("token" in readFirstInstallPlanArg()).toBe(false);
}
function expectFields(value: unknown, expected: Record<string, unknown>): void {
if (!value || typeof value !== "object") {
throw new Error("expected fields object");
}
const record = value as Record<string, unknown>;
for (const [key, expectedValue] of Object.entries(expected)) {
expect(record[key], key).toEqual(expectedValue);
}
}
function readFirstInstallPlanArg(): Record<string, unknown> {
const [firstArg] = buildGatewayInstallPlanMock.mock.calls[0] ?? [];
if (!firstArg) {
throw new Error("Expected gateway install plan arg");
}
return firstArg as Record<string, unknown>;
}
function readFirstConfigWriteParams(): {
sourceConfig?: { gateway?: { mode?: string; auth?: { token?: string } } };
} {
const [params] = replaceConfigFileMock.mock.calls[0] ?? [];
if (!params || typeof params !== "object") {
throw new Error("expected first config write params");
}
return params as { sourceConfig?: { gateway?: { mode?: string; auth?: { token?: string } } } };
}
function readFirstNodeStartupTlsEnvironmentArg(): Record<string, unknown> {
const [params] = resolveNodeStartupTlsEnvironmentMock.mock.calls[0] ?? [];
if (!params || typeof params !== "object") {
throw new Error("expected node startup TLS environment params");
}
return params as Record<string, unknown>;
}
function expectLastEmittedResult(result: string): void {
expectFields(actionState.emitted.at(-1), { result });
}
function mockResolvedGatewayTokenSecretRef(
token: SecretInput = { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
) {
const config = { gateway: { mode: "local" as const, auth: { mode: "token" as const, token } } };
readConfigFileSnapshotMock.mockResolvedValue({
exists: true,
valid: true,
config,
sourceConfig: config,
});
resolveSecretRefValuesMock.mockResolvedValue(
new Map([["env:default:OPENCLAW_GATEWAY_TOKEN", "resolved-from-secretref"]]),
);
}
const { runDaemonInstall } = await import("./install.js");
const envSnapshot = captureFullEnv();
export function setupInstallTests() {
beforeEach(() => {
runExecMock.mockReset();
runExecMock.mockResolvedValue(nodeProbeOutput("26.8.1"));
resolveNodeStartupTlsEnvironmentMock.mockReset();
readConfigFileSnapshotMock.mockReset();
resolveGatewayPortMock.mockClear();
mockSystemAccountHome();
replaceConfigFileMock.mockReset();
resolveGatewayAuthMock.mockReset();
resolveGatewayBindHostMock.mockReset();
resolveSecretRefValuesMock.mockReset();
randomTokenMock.mockReset();
buildGatewayInstallPlanMock.mockReset();
parsePortMock.mockReset();
isGatewayDaemonRuntimeMock.mockReset();
installDaemonServiceAndEmitMock.mockReset();
service.isLoaded.mockReset();
service.stage.mockReset();
service.install.mockReset();
service.readDefinitionMutationCapability.mockReset();
service.readCommand.mockReset();
resetRuntimeCapture();
actionState.warnings.length = 0;
actionState.emitted.length = 0;
actionState.failed.length = 0;
readConfigFileSnapshotMock.mockResolvedValue({
exists: false,
valid: true,
config: {},
sourceConfig: { gateway: { mode: "local", auth: { mode: "token" } } },
});
resolveGatewayPortMock.mockReturnValue(18789);
delete process.env.OPENCLAW_NIX_MODE;
resolveGatewayAuthMock.mockReturnValue({
mode: "token",
token: undefined,
password: undefined,
allowTailscale: false,
});
resolveGatewayBindHostMock.mockResolvedValue("127.0.0.1");
resolveSecretRefValuesMock.mockResolvedValue(new Map());
randomTokenMock.mockReturnValue("generated-token");
buildGatewayInstallPlanMock.mockImplementation(createInstallPlanFixture);
parsePortMock.mockReturnValue(null);
isGatewayDaemonRuntimeMock.mockReturnValue(true);
installDaemonServiceAndEmitMock.mockResolvedValue(undefined);
service.isLoaded.mockResolvedValue(false);
service.stage.mockResolvedValue(undefined);
service.install.mockResolvedValue(undefined);
service.readDefinitionMutationCapability.mockResolvedValue({ kind: "writable" });
service.readCommand.mockResolvedValue(null);
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
NODE_EXTRA_CA_CERTS: undefined,
NODE_USE_SYSTEM_CA: undefined,
});
delete process.env.OPENCLAW_GATEWAY_TOKEN;
});
afterEach(() => {
vi.restoreAllMocks();
envSnapshot.restore();
});
}
export {
actionState,
buildGatewayInstallPlanMock,
expectFields,
expectFirstInstallPlanCallOmitsToken,
expectLastEmittedResult,
installDaemonServiceAndEmitMock,
isGatewayDaemonRuntimeMock,
mockResolvedGatewayTokenSecretRef,
randomTokenMock,
readConfigFileSnapshotMock,
readFirstConfigWriteParams,
readFirstInstallPlanArg,
readFirstNodeStartupTlsEnvironmentArg,
replaceConfigFileMock,
resolveGatewayAuthMock,
resolveGatewayBindHostMock,
resolveNodeStartupTlsEnvironmentMock,
resolveSecretRefValuesMock,
runDaemonInstall,
runExecMock,
service,
};