| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| import { captureEnv } from "../../test-utils/env.js"; |
|
|
| const callGatewayStatusProbe = vi.fn(async (_opts?: unknown) => ({ ok: true as const })); |
| const loadGatewayTlsRuntime = vi.fn(async (_cfg?: unknown) => ({ |
| enabled: true, |
| required: true, |
| fingerprintSha256: "sha256:11:22:33:44", |
| })); |
| const findExtraGatewayServices = vi.fn(async (_env?: unknown, _opts?: unknown) => []); |
| const inspectPortUsage = vi.fn(async (port: number) => ({ |
| port, |
| status: "free" as const, |
| listeners: [], |
| hints: [], |
| })); |
| const readLastGatewayErrorLine = vi.fn(async (_env?: NodeJS.ProcessEnv) => null); |
| const auditGatewayServiceConfig = vi.fn(async (_opts?: unknown) => undefined); |
| const serviceIsLoaded = vi.fn(async (_opts?: unknown) => true); |
| const serviceReadRuntime = vi.fn(async (_env?: NodeJS.ProcessEnv) => ({ status: "running" })); |
| const serviceReadCommand = vi.fn< |
| (env?: NodeJS.ProcessEnv) => Promise<{ |
| programArguments: string[]; |
| environment?: Record<string, string>; |
| }> |
| >(async (_env?: NodeJS.ProcessEnv) => ({ |
| programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"], |
| environment: { |
| OPENCLAW_STATE_DIR: "/tmp/openclaw-daemon", |
| OPENCLAW_CONFIG_PATH: "/tmp/openclaw-daemon/openclaw.json", |
| }, |
| })); |
| const resolveGatewayBindHost = vi.fn( |
| async (_bindMode?: string, _customBindHost?: string) => "0.0.0.0", |
| ); |
| const pickPrimaryTailnetIPv4 = vi.fn(() => "100.64.0.9"); |
| const resolveGatewayPort = vi.fn((_cfg?: unknown, _env?: unknown) => 18789); |
| const resolveStateDir = vi.fn( |
| (env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-cli", |
| ); |
| const resolveConfigPath = vi.fn((env: NodeJS.ProcessEnv, stateDir: string) => { |
| return env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`; |
| }); |
| let daemonLoadedConfig: Record<string, unknown> = { |
| gateway: { |
| bind: "lan", |
| tls: { enabled: true }, |
| auth: { token: "daemon-token" }, |
| }, |
| }; |
| let cliLoadedConfig: Record<string, unknown> = { |
| gateway: { |
| bind: "loopback", |
| }, |
| }; |
|
|
| vi.mock("../../config/config.js", () => ({ |
| createConfigIO: ({ configPath }: { configPath: string }) => { |
| const isDaemon = configPath.includes("/openclaw-daemon/"); |
| return { |
| readConfigFileSnapshot: async () => ({ |
| path: configPath, |
| exists: true, |
| valid: true, |
| issues: [], |
| }), |
| loadConfig: () => (isDaemon ? daemonLoadedConfig : cliLoadedConfig), |
| }; |
| }, |
| resolveConfigPath: (env: NodeJS.ProcessEnv, stateDir: string) => resolveConfigPath(env, stateDir), |
| resolveGatewayPort: (cfg?: unknown, env?: unknown) => resolveGatewayPort(cfg, env), |
| resolveStateDir: (env: NodeJS.ProcessEnv) => resolveStateDir(env), |
| })); |
|
|
| vi.mock("../../daemon/diagnostics.js", () => ({ |
| readLastGatewayErrorLine: (env: NodeJS.ProcessEnv) => readLastGatewayErrorLine(env), |
| })); |
|
|
| vi.mock("../../daemon/inspect.js", () => ({ |
| findExtraGatewayServices: (env: unknown, opts?: unknown) => findExtraGatewayServices(env, opts), |
| })); |
|
|
| vi.mock("../../daemon/service-audit.js", () => ({ |
| auditGatewayServiceConfig: (opts: unknown) => auditGatewayServiceConfig(opts), |
| })); |
|
|
| vi.mock("../../daemon/service.js", () => ({ |
| resolveGatewayService: () => ({ |
| label: "LaunchAgent", |
| loadedText: "loaded", |
| notLoadedText: "not loaded", |
| isLoaded: serviceIsLoaded, |
| readCommand: serviceReadCommand, |
| readRuntime: serviceReadRuntime, |
| }), |
| })); |
|
|
| vi.mock("../../gateway/net.js", () => ({ |
| resolveGatewayBindHost: (bindMode: string, customBindHost?: string) => |
| resolveGatewayBindHost(bindMode, customBindHost), |
| })); |
|
|
| vi.mock("../../infra/ports.js", () => ({ |
| inspectPortUsage: (port: number) => inspectPortUsage(port), |
| formatPortDiagnostics: () => [], |
| })); |
|
|
| vi.mock("../../infra/tailnet.js", () => ({ |
| pickPrimaryTailnetIPv4: () => pickPrimaryTailnetIPv4(), |
| })); |
|
|
| vi.mock("../../infra/tls/gateway.js", () => ({ |
| loadGatewayTlsRuntime: (cfg: unknown) => loadGatewayTlsRuntime(cfg), |
| })); |
|
|
| vi.mock("./probe.js", () => ({ |
| probeGatewayStatus: (opts: unknown) => callGatewayStatusProbe(opts), |
| })); |
|
|
| const { gatherDaemonStatus } = await import("./status.gather.js"); |
|
|
| describe("gatherDaemonStatus", () => { |
| let envSnapshot: ReturnType<typeof captureEnv>; |
|
|
| beforeEach(() => { |
| envSnapshot = captureEnv([ |
| "OPENCLAW_STATE_DIR", |
| "OPENCLAW_CONFIG_PATH", |
| "OPENCLAW_GATEWAY_TOKEN", |
| "OPENCLAW_GATEWAY_PASSWORD", |
| "DAEMON_GATEWAY_TOKEN", |
| "DAEMON_GATEWAY_PASSWORD", |
| ]); |
| process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-cli"; |
| process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-cli/openclaw.json"; |
| delete process.env.OPENCLAW_GATEWAY_TOKEN; |
| delete process.env.OPENCLAW_GATEWAY_PASSWORD; |
| delete process.env.DAEMON_GATEWAY_TOKEN; |
| delete process.env.DAEMON_GATEWAY_PASSWORD; |
| callGatewayStatusProbe.mockClear(); |
| loadGatewayTlsRuntime.mockClear(); |
| daemonLoadedConfig = { |
| gateway: { |
| bind: "lan", |
| tls: { enabled: true }, |
| auth: { token: "daemon-token" }, |
| }, |
| }; |
| cliLoadedConfig = { |
| gateway: { |
| bind: "loopback", |
| }, |
| }; |
| }); |
|
|
| afterEach(() => { |
| envSnapshot.restore(); |
| }); |
|
|
| it("uses wss probe URL and forwards TLS fingerprint when daemon TLS is enabled", async () => { |
| const status = await gatherDaemonStatus({ |
| rpc: {}, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(loadGatewayTlsRuntime).toHaveBeenCalledTimes(1); |
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| url: "wss://127.0.0.1:19001", |
| tlsFingerprint: "sha256:11:22:33:44", |
| token: "daemon-token", |
| }), |
| ); |
| expect(status.gateway?.probeUrl).toBe("wss://127.0.0.1:19001"); |
| expect(status.rpc?.url).toBe("wss://127.0.0.1:19001"); |
| expect(status.rpc?.ok).toBe(true); |
| }); |
|
|
| it("does not force local TLS fingerprint when probe URL is explicitly overridden", async () => { |
| const status = await gatherDaemonStatus({ |
| rpc: { url: "wss://override.example:18790" }, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(loadGatewayTlsRuntime).not.toHaveBeenCalled(); |
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| url: "wss://override.example:18790", |
| tlsFingerprint: undefined, |
| }), |
| ); |
| expect(status.gateway?.probeUrl).toBe("wss://override.example:18790"); |
| expect(status.rpc?.url).toBe("wss://override.example:18790"); |
| }); |
|
|
| it("reuses command environment when reading runtime status", async () => { |
| serviceReadCommand.mockResolvedValueOnce({ |
| programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"], |
| environment: { |
| OPENCLAW_GATEWAY_PORT: "19001", |
| OPENCLAW_CONFIG_PATH: "/tmp/openclaw-daemon/openclaw.json", |
| OPENCLAW_STATE_DIR: "/tmp/openclaw-daemon", |
| } as Record<string, string>, |
| }); |
| serviceReadRuntime.mockImplementationOnce(async (env?: NodeJS.ProcessEnv) => ({ |
| status: env?.OPENCLAW_GATEWAY_PORT === "19001" ? "running" : "unknown", |
| detail: env?.OPENCLAW_GATEWAY_PORT ?? "missing-port", |
| })); |
|
|
| const status = await gatherDaemonStatus({ |
| rpc: {}, |
| probe: false, |
| deep: false, |
| }); |
|
|
| expect(serviceReadRuntime).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| OPENCLAW_GATEWAY_PORT: "19001", |
| }), |
| ); |
| expect(status.service.runtime).toMatchObject({ |
| status: "running", |
| detail: "19001", |
| }); |
| }); |
|
|
| it("resolves daemon gateway auth password SecretRef values before probing", async () => { |
| daemonLoadedConfig = { |
| gateway: { |
| bind: "lan", |
| tls: { enabled: true }, |
| auth: { |
| password: { source: "env", provider: "default", id: "DAEMON_GATEWAY_PASSWORD" }, |
| }, |
| }, |
| secrets: { |
| providers: { |
| default: { source: "env" }, |
| }, |
| }, |
| }; |
| process.env.DAEMON_GATEWAY_PASSWORD = "daemon-secretref-password"; |
|
|
| await gatherDaemonStatus({ |
| rpc: {}, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| password: "daemon-secretref-password", |
| }), |
| ); |
| }); |
|
|
| it("resolves daemon gateway auth token SecretRef values before probing", async () => { |
| daemonLoadedConfig = { |
| gateway: { |
| bind: "lan", |
| tls: { enabled: true }, |
| auth: { |
| mode: "token", |
| token: "${DAEMON_GATEWAY_TOKEN}", |
| }, |
| }, |
| secrets: { |
| providers: { |
| default: { source: "env" }, |
| }, |
| }, |
| }; |
| process.env.DAEMON_GATEWAY_TOKEN = "daemon-secretref-token"; |
|
|
| await gatherDaemonStatus({ |
| rpc: {}, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| token: "daemon-secretref-token", |
| }), |
| ); |
| }); |
|
|
| it("does not resolve daemon password SecretRef when token auth is configured", async () => { |
| daemonLoadedConfig = { |
| gateway: { |
| bind: "lan", |
| tls: { enabled: true }, |
| auth: { |
| mode: "token", |
| token: "daemon-token", |
| password: { source: "env", provider: "default", id: "MISSING_DAEMON_GATEWAY_PASSWORD" }, |
| }, |
| }, |
| secrets: { |
| providers: { |
| default: { source: "env" }, |
| }, |
| }, |
| }; |
|
|
| await gatherDaemonStatus({ |
| rpc: {}, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| token: "daemon-token", |
| password: undefined, |
| }), |
| ); |
| }); |
|
|
| it("keeps remote probe auth strict when remote token is missing", async () => { |
| daemonLoadedConfig = { |
| gateway: { |
| mode: "remote", |
| remote: { |
| url: "wss://gateway.example", |
| password: "remote-password", |
| }, |
| auth: { |
| mode: "token", |
| token: "local-token", |
| password: "local-password", |
| }, |
| }, |
| }; |
| process.env.OPENCLAW_GATEWAY_TOKEN = "env-token"; |
| process.env.OPENCLAW_GATEWAY_PASSWORD = "env-password"; |
|
|
| await gatherDaemonStatus({ |
| rpc: {}, |
| probe: true, |
| deep: false, |
| }); |
|
|
| expect(callGatewayStatusProbe).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| token: undefined, |
| password: "env-password", |
| }), |
| ); |
| }); |
|
|
| it("skips TLS runtime loading when probe is disabled", async () => { |
| const status = await gatherDaemonStatus({ |
| rpc: {}, |
| probe: false, |
| deep: false, |
| }); |
|
|
| expect(loadGatewayTlsRuntime).not.toHaveBeenCalled(); |
| expect(callGatewayStatusProbe).not.toHaveBeenCalled(); |
| expect(status.rpc).toBeUndefined(); |
| }); |
| }); |
|
|