mirror of https://github.com/openclaw/openclaw.git
fix(status): handle node-only hosts on current main (#56718)
* Status: handle node-only hosts * Status: address follow-up review nits * Changelog: note node-only status fix * Status: lazy-load node-only helper
This commit is contained in:
parent
571da81a35
commit
88ca0b2c3f
|
|
@ -180,6 +180,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Telegram/forum topics: keep native `/new` and `/reset` routed to the active topic by preserving the topic target on forum-thread command context. (#35963)
|
||||
- Status/port diagnostics: treat single-process dual-stack loopback gateway listeners as healthy in `openclaw status --all`, suppressing false “port already in use” conflict warnings. (#53398) Thanks @DanWebb1949.
|
||||
- Memory/builtin: keep memory-file indexing active in FTS-only mode (no embedding provider) so forced reindexes no longer swap in an empty index and wipe existing memory chunks. (#42714) Thanks @asamimei.
|
||||
- CLI/status: detect node-only hosts in `openclaw status` and `openclaw status --all`, show the configured remote gateway target instead of a false local `ECONNREFUSED`, and suppress contradictory local-gateway diagnosis output.
|
||||
|
||||
## 2026.3.24
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { buildChannelsTable } from "./status-all/channels.js";
|
|||
import { formatDurationPrecise, formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { pickGatewaySelfPresence } from "./status-all/gateway.js";
|
||||
import { buildStatusAllReportLines } from "./status-all/report-lines.js";
|
||||
import { resolveNodeOnlyGatewayInfo } from "./status.node-mode.js";
|
||||
import { readServiceStatusSummary } from "./status.service-summary.js";
|
||||
import { formatUpdateOneLiner } from "./status.update.js";
|
||||
|
||||
|
|
@ -147,6 +148,7 @@ export async function statusAllCommand(
|
|||
return {
|
||||
label: summary.label,
|
||||
installed: summary.installed,
|
||||
externallyManaged: summary.externallyManaged,
|
||||
managedByOpenClaw: summary.managedByOpenClaw,
|
||||
loaded: summary.loaded,
|
||||
loadedText: summary.loadedText,
|
||||
|
|
@ -158,6 +160,13 @@ export async function statusAllCommand(
|
|||
};
|
||||
const daemon = await readServiceSummary(resolveGatewayService());
|
||||
const nodeService = await readServiceSummary(resolveNodeService());
|
||||
const nodeOnlyGateway =
|
||||
daemon && nodeService
|
||||
? await resolveNodeOnlyGatewayInfo({
|
||||
daemon,
|
||||
node: nodeService,
|
||||
})
|
||||
: null;
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Scanning agents…");
|
||||
|
|
@ -171,6 +180,9 @@ export async function statusAllCommand(
|
|||
progress.tick();
|
||||
|
||||
const connectionDetailsForReport = (() => {
|
||||
if (nodeOnlyGateway) {
|
||||
return nodeOnlyGateway.connectionDetails;
|
||||
}
|
||||
if (!remoteUrlMissing) {
|
||||
return connection.message;
|
||||
}
|
||||
|
|
@ -195,14 +207,16 @@ export async function statusAllCommand(
|
|||
: {};
|
||||
|
||||
progress.setLabel("Querying gateway…");
|
||||
const health = gatewayReachable
|
||||
? await callGateway({
|
||||
config: cfg,
|
||||
method: "health",
|
||||
timeoutMs: Math.min(8000, opts?.timeoutMs ?? 10_000),
|
||||
...callOverrides,
|
||||
}).catch((err) => ({ error: String(err) }))
|
||||
: { error: gatewayProbe?.error ?? "gateway unreachable" };
|
||||
const health = nodeOnlyGateway
|
||||
? undefined
|
||||
: gatewayReachable
|
||||
? await callGateway({
|
||||
config: cfg,
|
||||
method: "health",
|
||||
timeoutMs: Math.min(8000, opts?.timeoutMs ?? 10_000),
|
||||
...callOverrides,
|
||||
}).catch((err) => ({ error: String(err) }))
|
||||
: { error: gatewayProbe?.error ?? "gateway unreachable" };
|
||||
|
||||
const channelsStatus = gatewayReachable
|
||||
? await callGateway({
|
||||
|
|
@ -261,6 +275,9 @@ export async function statusAllCommand(
|
|||
? `unreachable (${gatewayProbe.error})`
|
||||
: "unreachable";
|
||||
const gatewayAuth = gatewayReachable ? ` · auth ${formatGatewayAuthUsed(probeAuth)}` : "";
|
||||
const gatewayValue =
|
||||
nodeOnlyGateway?.gatewayValue ??
|
||||
`${gatewayMode}${remoteUrlMissing ? " (remote.url missing)" : ""} · ${gatewayTarget} (${connection.urlSource}) · ${gatewayStatus}${gatewayAuth}`;
|
||||
const gatewaySelfLine =
|
||||
gatewaySelf?.host || gatewaySelf?.ip || gatewaySelf?.version || gatewaySelf?.platform
|
||||
? [
|
||||
|
|
@ -303,7 +320,7 @@ export async function statusAllCommand(
|
|||
{ Item: "Update", Value: updateLine },
|
||||
{
|
||||
Item: "Gateway",
|
||||
Value: `${gatewayMode}${remoteUrlMissing ? " (remote.url missing)" : ""} · ${gatewayTarget} (${connection.urlSource}) · ${gatewayStatus}${gatewayAuth}`,
|
||||
Value: gatewayValue,
|
||||
},
|
||||
...(probeAuthResolution.warning
|
||||
? [{ Item: "Gateway auth warning", Value: probeAuthResolution.warning }]
|
||||
|
|
@ -368,6 +385,7 @@ export async function statusAllCommand(
|
|||
channelIssues,
|
||||
gatewayReachable,
|
||||
health,
|
||||
nodeOnlyGateway,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ function createBaseParams(
|
|||
channelIssues: [],
|
||||
gatewayReachable: false,
|
||||
health: null,
|
||||
nodeOnlyGateway: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -87,4 +88,35 @@ describe("status-all diagnosis port checks", () => {
|
|||
expect(output).toContain("! Port 18789");
|
||||
expect(output).toContain("Port 18789 is already in use.");
|
||||
});
|
||||
|
||||
it("avoids unreachable gateway diagnosis in node-only mode", async () => {
|
||||
const params = createBaseParams([]);
|
||||
params.connectionDetailsForReport = [
|
||||
"Node-only mode detected",
|
||||
"Local gateway: not expected on this machine",
|
||||
"Remote gateway target: gateway.example.com:19000",
|
||||
].join("\n");
|
||||
params.tailscale.backendState = "Running";
|
||||
params.health = undefined;
|
||||
params.nodeOnlyGateway = {
|
||||
gatewayTarget: "gateway.example.com:19000",
|
||||
gatewayValue: "node → gateway.example.com:19000 · no local gateway",
|
||||
connectionDetails: [
|
||||
"Node-only mode detected",
|
||||
"Local gateway: not expected on this machine",
|
||||
"Remote gateway target: gateway.example.com:19000",
|
||||
"Inspect the remote gateway host for live channel and health details.",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
await appendStatusAllDiagnosis(params);
|
||||
|
||||
const output = params.lines.join("\n");
|
||||
expect(output).toContain("Node-only mode detected");
|
||||
expect(output).toContain(
|
||||
"Channel issues skipped (node-only mode; query gateway.example.com:19000)",
|
||||
);
|
||||
expect(output).not.toContain("Channel issues skipped (gateway unreachable)");
|
||||
expect(output).not.toContain("Gateway health:");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
formatPluginCompatibilityNotice,
|
||||
type PluginCompatibilityNotice,
|
||||
} from "../../plugins/status.js";
|
||||
import type { NodeOnlyGatewayInfo } from "../status.node-mode.js";
|
||||
import { formatTimeAgo, redactSecrets } from "./format.js";
|
||||
import { readFileTailLines, summarizeLogTail } from "./gateway.js";
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ export async function appendStatusAllDiagnosis(params: {
|
|||
channelIssues: ChannelIssueLike[];
|
||||
gatewayReachable: boolean;
|
||||
health: unknown;
|
||||
nodeOnlyGateway: NodeOnlyGatewayInfo | null;
|
||||
}) {
|
||||
const { lines, muted, ok, warn, fail } = params;
|
||||
|
||||
|
|
@ -248,6 +250,11 @@ export async function appendStatusAllDiagnosis(params: {
|
|||
if (params.channelIssues.length > 12) {
|
||||
lines.push(` ${muted(`… +${params.channelIssues.length - 12} more`)}`);
|
||||
}
|
||||
} else if (params.nodeOnlyGateway) {
|
||||
emitCheck(
|
||||
`Channel issues skipped (node-only mode; query ${params.nodeOnlyGateway.gatewayTarget})`,
|
||||
"ok",
|
||||
);
|
||||
} else {
|
||||
emitCheck(
|
||||
`Channel issues skipped (gateway ${params.gatewayReachable ? "query failed" : "unreachable"})`,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ describe("buildStatusAllReportLines", () => {
|
|||
channelIssues: [],
|
||||
gatewayReachable: false,
|
||||
health: null,
|
||||
nodeOnlyGateway: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ let statusAllModulePromise: Promise<typeof import("./status-all.js")> | undefine
|
|||
let statusCommandTextRuntimePromise:
|
||||
| Promise<typeof import("./status.command.text-runtime.js")>
|
||||
| undefined;
|
||||
let statusNodeModeModulePromise: Promise<typeof import("./status.node-mode.js")> | undefined;
|
||||
|
||||
function loadProviderUsage() {
|
||||
providerUsagePromise ??= import("../infra/provider-usage.js");
|
||||
|
|
@ -53,6 +54,11 @@ function loadStatusCommandTextRuntime() {
|
|||
return statusCommandTextRuntimePromise;
|
||||
}
|
||||
|
||||
function loadStatusNodeModeModule() {
|
||||
statusNodeModeModulePromise ??= import("./status.node-mode.js");
|
||||
return statusNodeModeModulePromise;
|
||||
}
|
||||
|
||||
function resolvePairingRecoveryContext(params: {
|
||||
error?: string | null;
|
||||
closeReason?: string | null;
|
||||
|
|
@ -302,7 +308,21 @@ export async function statusCommand(
|
|||
}).httpUrl
|
||||
: "disabled";
|
||||
|
||||
const [daemon, nodeDaemon] = await Promise.all([
|
||||
getDaemonStatusSummary(),
|
||||
getNodeDaemonStatusSummary(),
|
||||
]);
|
||||
const nodeOnlyGateway = await loadStatusNodeModeModule().then(({ resolveNodeOnlyGatewayInfo }) =>
|
||||
resolveNodeOnlyGatewayInfo({
|
||||
daemon,
|
||||
node: nodeDaemon,
|
||||
}),
|
||||
);
|
||||
|
||||
const gatewayValue = (() => {
|
||||
if (nodeOnlyGateway) {
|
||||
return nodeOnlyGateway.gatewayValue;
|
||||
}
|
||||
const target = remoteUrlMissing
|
||||
? `fallback ${gatewayConnection.url}`
|
||||
: `${gatewayConnection.url}${gatewayConnection.urlSource ? ` (${gatewayConnection.urlSource})` : ""}`;
|
||||
|
|
@ -344,11 +364,6 @@ export async function statusCommand(
|
|||
const defSuffix = def ? ` · default ${def.id} active ${defActive}` : "";
|
||||
return `${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`;
|
||||
})();
|
||||
|
||||
const [daemon, nodeDaemon] = await Promise.all([
|
||||
getDaemonStatusSummary(),
|
||||
getNodeDaemonStatusSummary(),
|
||||
]);
|
||||
const daemonValue = (() => {
|
||||
if (daemon.installed === false) {
|
||||
return `${daemon.label} not installed`;
|
||||
|
|
@ -743,7 +758,9 @@ export async function statusCommand(
|
|||
runtime.log("Next steps:");
|
||||
runtime.log(` Need to share? ${formatCliCommand("openclaw status --all")}`);
|
||||
runtime.log(` Need to debug live? ${formatCliCommand("openclaw logs --follow")}`);
|
||||
if (gatewayReachable) {
|
||||
if (nodeOnlyGateway) {
|
||||
runtime.log(` Need node service? ${formatCliCommand("openclaw node status")}`);
|
||||
} else if (gatewayReachable) {
|
||||
runtime.log(` Need to test channels? ${formatCliCommand("openclaw status --deep")}`);
|
||||
} else {
|
||||
runtime.log(` Fix reachability first: ${formatCliCommand("openclaw gateway probe")}`);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { readServiceStatusSummary } from "./status.service-summary.js";
|
|||
type DaemonStatusSummary = {
|
||||
label: string;
|
||||
installed: boolean | null;
|
||||
loaded: boolean;
|
||||
managedByOpenClaw: boolean;
|
||||
externallyManaged: boolean;
|
||||
loadedText: string;
|
||||
|
|
@ -21,6 +22,7 @@ async function buildDaemonStatusSummary(
|
|||
return {
|
||||
label: summary.label,
|
||||
installed: summary.installed,
|
||||
loaded: summary.loaded,
|
||||
managedByOpenClaw: summary.managedByOpenClaw,
|
||||
externallyManaged: summary.externallyManaged,
|
||||
loadedText: summary.loadedText,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadNodeHostConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../node-host/config.js", () => ({
|
||||
loadNodeHostConfig: mocks.loadNodeHostConfig,
|
||||
}));
|
||||
|
||||
import { resolveNodeOnlyGatewayInfo } from "./status.node-mode.js";
|
||||
|
||||
describe("resolveNodeOnlyGatewayInfo", () => {
|
||||
beforeEach(() => {
|
||||
mocks.loadNodeHostConfig.mockReset();
|
||||
});
|
||||
|
||||
it("returns node-only gateway details when no local gateway is installed", async () => {
|
||||
mocks.loadNodeHostConfig.mockResolvedValueOnce({
|
||||
version: 1,
|
||||
nodeId: "node-1",
|
||||
gateway: { host: "gateway.example.com", port: 19000 },
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveNodeOnlyGatewayInfo({
|
||||
daemon: { installed: false },
|
||||
node: {
|
||||
installed: true,
|
||||
loaded: true,
|
||||
externallyManaged: false,
|
||||
runtimeShort: "running (pid 4321)",
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
gatewayTarget: "gateway.example.com:19000",
|
||||
gatewayValue: "node → gateway.example.com:19000 · no local gateway",
|
||||
connectionDetails: [
|
||||
"Node-only mode detected",
|
||||
"Local gateway: not expected on this machine",
|
||||
"Remote gateway target: gateway.example.com:19000",
|
||||
"Inspect the remote gateway host for live channel and health details.",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not claim node-only mode when the node service is installed but inactive", async () => {
|
||||
mocks.loadNodeHostConfig.mockResolvedValueOnce({
|
||||
version: 1,
|
||||
nodeId: "node-1",
|
||||
gateway: { host: "gateway.example.com", port: 19000 },
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveNodeOnlyGatewayInfo({
|
||||
daemon: { installed: false },
|
||||
node: {
|
||||
installed: true,
|
||||
loaded: false,
|
||||
externallyManaged: false,
|
||||
runtime: { status: "stopped" },
|
||||
runtimeShort: "stopped",
|
||||
},
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to an unknown gateway target when node-only config is missing", async () => {
|
||||
mocks.loadNodeHostConfig.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
resolveNodeOnlyGatewayInfo({
|
||||
daemon: { installed: false },
|
||||
node: {
|
||||
installed: true,
|
||||
loaded: true,
|
||||
externallyManaged: false,
|
||||
runtimeShort: "running (pid 4321)",
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
gatewayTarget: "(gateway address unknown)",
|
||||
gatewayValue: "node → (gateway address unknown) · no local gateway",
|
||||
connectionDetails: [
|
||||
"Node-only mode detected",
|
||||
"Local gateway: not expected on this machine",
|
||||
"Remote gateway target: (gateway address unknown)",
|
||||
"Inspect the remote gateway host for live channel and health details.",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { DEFAULT_GATEWAY_PORT } from "../config/paths.js";
|
||||
import { loadNodeHostConfig } from "../node-host/config.js";
|
||||
|
||||
type NodeOnlyServiceLike = {
|
||||
installed: boolean | null;
|
||||
loaded?: boolean | null;
|
||||
externallyManaged?: boolean;
|
||||
runtime?:
|
||||
| {
|
||||
status?: string;
|
||||
pid?: number;
|
||||
}
|
||||
| undefined;
|
||||
runtimeShort?: string | null;
|
||||
};
|
||||
|
||||
export type NodeOnlyGatewayInfo = {
|
||||
gatewayTarget: string;
|
||||
gatewayValue: string;
|
||||
connectionDetails: string;
|
||||
};
|
||||
|
||||
function resolveNodeGatewayTarget(gateway?: { host?: string; port?: number }): string {
|
||||
return gateway?.host
|
||||
? `${gateway.host}:${gateway.port ?? DEFAULT_GATEWAY_PORT}`
|
||||
: "(gateway address unknown)";
|
||||
}
|
||||
|
||||
function hasRunningRuntime(
|
||||
runtime:
|
||||
| {
|
||||
status?: string;
|
||||
pid?: number;
|
||||
}
|
||||
| undefined,
|
||||
): boolean {
|
||||
return runtime?.status === "running" || typeof runtime?.pid === "number";
|
||||
}
|
||||
|
||||
function isNodeServiceActive(node: NodeOnlyServiceLike): boolean {
|
||||
if (node.installed !== true) {
|
||||
return false;
|
||||
}
|
||||
if (node.externallyManaged === true) {
|
||||
return true;
|
||||
}
|
||||
if (node.loaded === true) {
|
||||
return true;
|
||||
}
|
||||
if (hasRunningRuntime(node.runtime)) {
|
||||
return true;
|
||||
}
|
||||
return typeof node.runtimeShort === "string" && node.runtimeShort.startsWith("running");
|
||||
}
|
||||
|
||||
export async function resolveNodeOnlyGatewayInfo(params: {
|
||||
daemon: Pick<NodeOnlyServiceLike, "installed">;
|
||||
node: NodeOnlyServiceLike;
|
||||
}): Promise<NodeOnlyGatewayInfo | null> {
|
||||
if (params.daemon.installed !== false || !isNodeServiceActive(params.node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gatewayTarget = resolveNodeGatewayTarget((await loadNodeHostConfig())?.gateway);
|
||||
return {
|
||||
gatewayTarget,
|
||||
gatewayValue: `node → ${gatewayTarget} · no local gateway`,
|
||||
connectionDetails: [
|
||||
"Node-only mode detected",
|
||||
"Local gateway: not expected on this machine",
|
||||
`Remote gateway target: ${gatewayTarget}`,
|
||||
"Inspect the remote gateway host for live channel and health details.",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
|
@ -196,6 +196,7 @@ const mocks = vi.hoisted(() => ({
|
|||
}),
|
||||
resolveMainSessionKey: vi.fn().mockReturnValue("agent:main:main"),
|
||||
resolveStorePath: vi.fn().mockReturnValue("/tmp/sessions.json"),
|
||||
loadNodeHostConfig: vi.fn().mockResolvedValue(null),
|
||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
||||
getWebAuthAgeMs: vi.fn().mockReturnValue(5000),
|
||||
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
|
||||
|
|
@ -212,6 +213,38 @@ const mocks = vi.hoisted(() => ({
|
|||
}),
|
||||
runSecurityAudit: vi.fn().mockResolvedValue(createDefaultSecurityAuditResult()),
|
||||
buildPluginCompatibilityNotices: vi.fn((): PluginCompatibilityNotice[] => []),
|
||||
resolveGatewayService: vi.fn().mockReturnValue({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 1234 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "gateway"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
}),
|
||||
}),
|
||||
resolveNodeService: vi.fn().mockReturnValue({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 4321 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "node-host"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.node.plist",
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/config-presence.js", async (importOriginal) => {
|
||||
|
|
@ -387,47 +420,18 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||
readBestEffortConfig: vi.fn(async () => mocks.loadConfig()),
|
||||
};
|
||||
});
|
||||
vi.mock("../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => ({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 1234 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "gateway"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
}),
|
||||
}),
|
||||
readGatewayServiceState: async () => ({
|
||||
installed: true,
|
||||
loaded: true,
|
||||
running: true,
|
||||
runtime: { status: "running", pid: 1234 },
|
||||
}),
|
||||
}));
|
||||
vi.mock("../daemon/service.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../daemon/service.js")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveGatewayService: mocks.resolveGatewayService,
|
||||
};
|
||||
});
|
||||
vi.mock("../daemon/node-service.js", () => ({
|
||||
resolveNodeService: () => ({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 4321 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "node-host"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.node.plist",
|
||||
}),
|
||||
}),
|
||||
resolveNodeService: mocks.resolveNodeService,
|
||||
}));
|
||||
vi.mock("../node-host/config.js", () => ({
|
||||
loadNodeHostConfig: mocks.loadNodeHostConfig,
|
||||
}));
|
||||
vi.mock("../security/audit.js", () => ({
|
||||
runSecurityAudit: mocks.runSecurityAudit,
|
||||
|
|
@ -466,6 +470,8 @@ describe("statusCommand", () => {
|
|||
mocks.resolveMainSessionKey.mockReturnValue("agent:main:main");
|
||||
mocks.resolveStorePath.mockReset();
|
||||
mocks.resolveStorePath.mockReturnValue("/tmp/sessions.json");
|
||||
mocks.loadNodeHostConfig.mockReset();
|
||||
mocks.loadNodeHostConfig.mockResolvedValue(null);
|
||||
mocks.probeGateway.mockReset();
|
||||
mocks.probeGateway.mockResolvedValue(createDefaultProbeGatewayResult());
|
||||
mocks.callGateway.mockReset();
|
||||
|
|
@ -483,6 +489,40 @@ describe("statusCommand", () => {
|
|||
mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
|
||||
mocks.runSecurityAudit.mockReset();
|
||||
mocks.runSecurityAudit.mockResolvedValue(createDefaultSecurityAuditResult());
|
||||
mocks.resolveGatewayService.mockReset();
|
||||
mocks.resolveGatewayService.mockReturnValue({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 1234 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "gateway"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
}),
|
||||
});
|
||||
mocks.resolveNodeService.mockReset();
|
||||
mocks.resolveNodeService.mockReturnValue({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 4321 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "node-host"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.node.plist",
|
||||
}),
|
||||
});
|
||||
runtimeLogMock.mockClear();
|
||||
(runtime.error as Mock<(...args: unknown[]) => void>).mockClear();
|
||||
});
|
||||
|
|
@ -584,6 +624,33 @@ describe("statusCommand", () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
it("shows node-only gateway info when no local gateway service is installed", async () => {
|
||||
mocks.resolveGatewayService.mockReturnValueOnce({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
stage: async () => {},
|
||||
install: async () => {},
|
||||
uninstall: async () => {},
|
||||
stop: async () => {},
|
||||
restart: async () => ({ outcome: "completed" as const }),
|
||||
isLoaded: async () => false,
|
||||
readRuntime: async () => undefined,
|
||||
readCommand: async () => null,
|
||||
});
|
||||
mocks.loadNodeHostConfig.mockResolvedValueOnce({
|
||||
version: 1,
|
||||
nodeId: "node-1",
|
||||
gateway: { host: "gateway.example.com", port: 19000 },
|
||||
});
|
||||
|
||||
const joined = await runStatusAndGetJoinedLogs();
|
||||
expect(joined).toContain("node → gateway.example.com:19000 · no local gateway");
|
||||
expect(joined).not.toContain("Gateway: local · ws://127.0.0.1:18789");
|
||||
expect(joined).toContain("openclaw --profile isolated node status");
|
||||
expect(joined).not.toContain("Fix reachability first");
|
||||
});
|
||||
|
||||
it("shows gateway auth when reachable", async () => {
|
||||
mocks.loadConfig.mockReturnValue({
|
||||
session: {},
|
||||
|
|
|
|||
Loading…
Reference in New Issue