import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const loadConfig = vi.fn(() => ({ gateway: { auth: { token: "config-token", }, }, })); const runtimeLogs: string[] = []; const defaultRuntime = { log: (message: string) => runtimeLogs.push(message), error: vi.fn(), exit: (code: number) => { throw new Error(`__exit__:${code}`); }, }; const service = { label: "TestService", loadedText: "loaded", notLoadedText: "not loaded", install: vi.fn(), uninstall: vi.fn(), stop: vi.fn(), isLoaded: vi.fn(), readCommand: vi.fn(), readRuntime: vi.fn(), restart: vi.fn(), }; vi.mock("../../config/config.js", () => ({ loadConfig: () => loadConfig(), readBestEffortConfig: async () => loadConfig(), })); vi.mock("../../runtime.js", () => ({ defaultRuntime, })); let runServiceRestart: typeof import("./lifecycle-core.js").runServiceRestart; let runServiceStop: typeof import("./lifecycle-core.js").runServiceStop; describe("runServiceRestart token drift", () => { beforeAll(async () => { ({ runServiceRestart, runServiceStop } = await import("./lifecycle-core.js")); }); beforeEach(() => { runtimeLogs.length = 0; loadConfig.mockReset(); loadConfig.mockReturnValue({ gateway: { auth: { token: "config-token", }, }, }); service.isLoaded.mockClear(); service.readCommand.mockClear(); service.restart.mockClear(); service.isLoaded.mockResolvedValue(true); service.readCommand.mockResolvedValue({ environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" }, }); service.restart.mockResolvedValue(undefined); vi.unstubAllEnvs(); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", ""); vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", ""); vi.stubEnv("OPENCLAW_GATEWAY_URL", ""); vi.stubEnv("CLAWDBOT_GATEWAY_URL", ""); }); it("emits drift warning when enabled", async () => { await runServiceRestart({ serviceNoun: "Gateway", service, renderStartHints: () => [], opts: { json: true }, checkTokenDrift: true, }); expect(loadConfig).toHaveBeenCalledTimes(1); const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{")); const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] }; expect(payload.warnings).toEqual( expect.arrayContaining([expect.stringContaining("gateway install --force")]), ); }); it("compares restart drift against config token even when caller env is set", async () => { loadConfig.mockReturnValue({ gateway: { auth: { token: "config-token", }, }, }); service.readCommand.mockResolvedValue({ environment: { OPENCLAW_GATEWAY_TOKEN: "env-token" }, }); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "env-token"); await runServiceRestart({ serviceNoun: "Gateway", service, renderStartHints: () => [], opts: { json: true }, checkTokenDrift: true, }); const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{")); const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] }; expect(payload.warnings).toEqual( expect.arrayContaining([expect.stringContaining("gateway install --force")]), ); }); it("skips drift warning when disabled", async () => { await runServiceRestart({ serviceNoun: "Node", service, renderStartHints: () => [], opts: { json: true }, }); expect(loadConfig).not.toHaveBeenCalled(); expect(service.readCommand).not.toHaveBeenCalled(); const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{")); const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] }; expect(payload.warnings).toBeUndefined(); }); it("emits stopped when an unmanaged process handles stop", async () => { service.isLoaded.mockResolvedValue(false); await runServiceStop({ serviceNoun: "Gateway", service, opts: { json: true }, onNotLoaded: async () => ({ result: "stopped", message: "Gateway stop signal sent to unmanaged process on port 18789: 4200.", }), }); const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{")); const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string }; expect(payload.result).toBe("stopped"); expect(payload.message).toContain("unmanaged process"); expect(service.stop).not.toHaveBeenCalled(); }); it("runs restart health checks after an unmanaged restart signal", async () => { const postRestartCheck = vi.fn(async () => {}); service.isLoaded.mockResolvedValue(false); await runServiceRestart({ serviceNoun: "Gateway", service, renderStartHints: () => [], opts: { json: true }, onNotLoaded: async () => ({ result: "restarted", message: "Gateway restart signal sent to unmanaged process on port 18789: 4200.", }), postRestartCheck, }); expect(postRestartCheck).toHaveBeenCalledTimes(1); expect(service.restart).not.toHaveBeenCalled(); expect(service.readCommand).not.toHaveBeenCalled(); const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{")); const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string }; expect(payload.result).toBe("restarted"); expect(payload.message).toContain("unmanaged process"); }); });