mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 13:00:48 +00:00
410 lines
12 KiB
TypeScript
410 lines
12 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { captureEnv } from "../../test-utils/env.js";
|
|
import type { GatewayRestartSnapshot } from "./restart-health.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 inspectGatewayRestart = vi.fn<(opts?: unknown) => Promise<GatewayRestartSnapshot>>(
|
|
async (_opts?: unknown) => ({
|
|
runtime: { status: "running", pid: 1234 },
|
|
portUsage: { port: 19001, status: "busy", listeners: [], hints: [] },
|
|
healthy: true,
|
|
staleGatewayPids: [],
|
|
}),
|
|
);
|
|
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),
|
|
}));
|
|
|
|
vi.mock("./restart-health.js", () => ({
|
|
inspectGatewayRestart: (opts: unknown) => inspectGatewayRestart(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();
|
|
inspectGatewayRestart.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"; // pragma: allowlist secret
|
|
|
|
await gatherDaemonStatus({
|
|
rpc: {},
|
|
probe: true,
|
|
deep: false,
|
|
});
|
|
|
|
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
password: "daemon-secretref-password", // pragma: allowlist secret
|
|
}),
|
|
);
|
|
});
|
|
|
|
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", // pragma: allowlist secret
|
|
},
|
|
auth: {
|
|
mode: "token",
|
|
token: "local-token",
|
|
password: "local-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
};
|
|
process.env.OPENCLAW_GATEWAY_TOKEN = "env-token";
|
|
process.env.OPENCLAW_GATEWAY_PASSWORD = "env-password"; // pragma: allowlist secret
|
|
|
|
await gatherDaemonStatus({
|
|
rpc: {},
|
|
probe: true,
|
|
deep: false,
|
|
});
|
|
|
|
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
token: undefined,
|
|
password: "env-password", // pragma: allowlist secret
|
|
}),
|
|
);
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
it("surfaces stale gateway listener pids from restart health inspection", async () => {
|
|
inspectGatewayRestart.mockResolvedValueOnce({
|
|
runtime: { status: "running", pid: 8000 },
|
|
portUsage: {
|
|
port: 19001,
|
|
status: "busy",
|
|
listeners: [{ pid: 9000, ppid: 8999, commandLine: "openclaw-gateway" }],
|
|
hints: [],
|
|
},
|
|
healthy: false,
|
|
staleGatewayPids: [9000],
|
|
});
|
|
|
|
const status = await gatherDaemonStatus({
|
|
rpc: {},
|
|
probe: true,
|
|
deep: false,
|
|
});
|
|
|
|
expect(inspectGatewayRestart).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
port: 19001,
|
|
}),
|
|
);
|
|
expect(status.health).toEqual({
|
|
healthy: false,
|
|
staleGatewayPids: [9000],
|
|
});
|
|
});
|
|
});
|