import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { makeTempWorkspace } from "../../test-helpers/workspace.js"; import { captureEnv } from "../../test-utils/env.js"; const runtimeLogs: string[] = []; const runtimeErrors: string[] = []; const serviceMock = vi.hoisted(() => ({ label: "Gateway", loadedText: "loaded", notLoadedText: "not loaded", install: vi.fn(async (_opts?: { environment?: Record }) => {}), uninstall: vi.fn(async () => {}), stop: vi.fn(async () => {}), restart: vi.fn(async () => {}), isLoaded: vi.fn(async () => false), readCommand: vi.fn(async () => null), readRuntime: vi.fn(async () => ({ status: "stopped" as const })), })); vi.mock("../../daemon/service.js", () => ({ resolveGatewayService: () => serviceMock, })); vi.mock("../../runtime.js", () => ({ defaultRuntime: { log: (message: string) => runtimeLogs.push(message), error: (message: string) => runtimeErrors.push(message), exit: (code: number) => { throw new Error(`__exit__:${code}`); }, }, })); const { runDaemonInstall } = await import("./install.js"); const { clearConfigCache } = await import("../../config/config.js"); async function readJson(filePath: string): Promise> { return JSON.parse(await fs.readFile(filePath, "utf8")) as Record; } describe("runDaemonInstall integration", () => { let envSnapshot: ReturnType; let tempHome: string; let configPath: string; beforeAll(async () => { envSnapshot = captureEnv([ "HOME", "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_GATEWAY_TOKEN", "CLAWDBOT_GATEWAY_TOKEN", "OPENCLAW_GATEWAY_PASSWORD", "CLAWDBOT_GATEWAY_PASSWORD", ]); tempHome = await makeTempWorkspace("openclaw-daemon-install-int-"); configPath = path.join(tempHome, "openclaw.json"); process.env.HOME = tempHome; process.env.OPENCLAW_STATE_DIR = tempHome; process.env.OPENCLAW_CONFIG_PATH = configPath; }); afterAll(async () => { envSnapshot.restore(); await fs.rm(tempHome, { recursive: true, force: true }); }); beforeEach(async () => { runtimeLogs.length = 0; runtimeErrors.length = 0; vi.clearAllMocks(); delete process.env.OPENCLAW_GATEWAY_TOKEN; delete process.env.CLAWDBOT_GATEWAY_TOKEN; delete process.env.OPENCLAW_GATEWAY_PASSWORD; delete process.env.CLAWDBOT_GATEWAY_PASSWORD; serviceMock.isLoaded.mockResolvedValue(false); await fs.writeFile(configPath, JSON.stringify({}, null, 2)); clearConfigCache(); }); it("fails closed when token SecretRef is required but unresolved", async () => { await fs.writeFile( configPath, JSON.stringify( { secrets: { providers: { default: { source: "env" }, }, }, gateway: { auth: { mode: "token", token: { source: "env", provider: "default", id: "MISSING_GATEWAY_TOKEN", }, }, }, }, null, 2, ), ); clearConfigCache(); await expect(runDaemonInstall({ json: true })).rejects.toThrow("__exit__:1"); expect(serviceMock.install).not.toHaveBeenCalled(); const joined = runtimeLogs.join("\n"); expect(joined).toContain("SecretRef is configured but unresolved"); expect(joined).toContain("MISSING_GATEWAY_TOKEN"); }); it("auto-mints token when no source exists and persists the same token used for install env", async () => { await fs.writeFile( configPath, JSON.stringify( { gateway: { auth: { mode: "token", }, }, }, null, 2, ), ); clearConfigCache(); await runDaemonInstall({ json: true }); expect(serviceMock.install).toHaveBeenCalledTimes(1); const updated = await readJson(configPath); const gateway = (updated.gateway ?? {}) as { auth?: { token?: string } }; const persistedToken = gateway.auth?.token; expect(typeof persistedToken).toBe("string"); expect((persistedToken ?? "").length).toBeGreaterThan(0); const installEnv = serviceMock.install.mock.calls[0]?.[0]?.environment; expect(installEnv?.OPENCLAW_GATEWAY_TOKEN).toBe(persistedToken); }); });