| |
| 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, |
| }; |
|
|