mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(channels): answer the channel question, and stop calling ECONNREFUSED an auth failure (#126984)
Two defects in one command, both on the path a brand-new operator is on immediately after `openclaw onboard`. `channels status` never mentioned channels when none were configured. With the gateway up it printed `Gateway reachable.` and a tip about `status --deep`; without it, two blank lines where the channel list belongs. The operator asked for the status of their channels and got gateway reachability. Its siblings already handle this -- `channels list` prints `- no configured chat channels (run \`openclaw channels list --all\` to see installable channels)` and `openclaw status` prints `No channels configured` -- so `channels status` was the lone holdout. Both renderers now emit that same line, moved to a shared constant so the three surfaces cannot drift apart again. The second is worse because it sends the operator somewhere wrong. The fallback computed `gatewayAuthUnavailable = expectedError || isGatewaySecretRefUnavailableError(err)`, and `isExpectedCliError` returns true for `isGatewayTransportError` -- a plain ECONNREFUSED. So a gateway that simply was not running reported `Gateway auth unavailable; showing config-only status.`, contradicting the `Gateway not reachable at ws://... (ECONNREFUSED)` line printed three lines above it. Someone who runs `channels status` before starting the gateway went hunting for a token problem that did not exist. The flag now consults only the two genuinely auth-related predicates; `expectedError` keeps its separate job of selecting the canonical CLI failure output. `isGatewayCredentialsCliError` becomes exported for that check. The JSON shape is unchanged; only the truth of `gatewayAuthUnavailable` changes, and no test or documented contract depended on transport errors setting it. Production +27/-9.
This commit is contained in:
committed by
GitHub
parent
2acfc47b7f
commit
9be871245b
@@ -41,7 +41,7 @@ export class ExpectedCliError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isGatewayCredentialsCliError(
|
||||
export function isGatewayCredentialsCliError(
|
||||
error: unknown,
|
||||
): error is Error & { method: string; configPath: string } {
|
||||
// Keep the root failure renderer lean; importing gateway/call would pull the
|
||||
|
||||
@@ -203,6 +203,16 @@ function requireReadOnlyPluginListCall(): unknown[] {
|
||||
}
|
||||
|
||||
describe("config-only channels status output", () => {
|
||||
it("guides operators when no channels are configured", async () => {
|
||||
activeChannelPlugins.splice(0);
|
||||
|
||||
const output = await formatLocalStatusSummary({ channels: {} });
|
||||
|
||||
expect(output).toContain(
|
||||
"- no configured chat channels (run `openclaw channels list --all` to see installable channels)",
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizes channel and account display names in terminal output", async () => {
|
||||
const control = "\u001B]0;channels-status-injection\u0007";
|
||||
registerSingleTestPlugin(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Channels status command-flow tests cover gateway calls, config fallback, and timeout validation.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GatewaySecretRefUnavailableError } from "../gateway/credentials.js";
|
||||
import { GatewayTransportError } from "../gateway/transport-error.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
import { channelsStatusCommand } from "./channels/status.js";
|
||||
import { createCapturingTestRuntime } from "./test-runtime-config-helpers.js";
|
||||
@@ -214,6 +215,18 @@ function createTokenOnlyPlugin() {
|
||||
};
|
||||
}
|
||||
|
||||
function createGatewayTransportError(message = "Gateway not reachable (ECONNREFUSED).") {
|
||||
return new GatewayTransportError({
|
||||
kind: "closed",
|
||||
message,
|
||||
connectionDetails: {
|
||||
url: "ws://127.0.0.1:18997",
|
||||
urlSource: "local loopback",
|
||||
message: "Gateway target: ws://127.0.0.1:18997",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
beforeEach(() => {
|
||||
mocks.callGateway.mockReset();
|
||||
@@ -270,7 +283,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
});
|
||||
|
||||
it("keeps read-only fallback output when SecretRefs are unresolved", async () => {
|
||||
mocks.callGateway.mockRejectedValue(new Error("gateway closed"));
|
||||
mocks.callGateway.mockRejectedValue(createGatewayTransportError());
|
||||
mocks.requireValidConfig.mockResolvedValue({ secretResolved: false, channels: {} });
|
||||
mocks.resolveCommandConfigWithSecrets.mockResolvedValue({
|
||||
resolvedConfig: { secretResolved: false, channels: {} },
|
||||
@@ -284,6 +297,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
await channelsStatusCommand({ probe: false }, runtime as never);
|
||||
|
||||
expect(errors.join("\n")).toContain("Gateway not reachable");
|
||||
expect(errors.join("\n")).not.toContain("Gateway auth unavailable");
|
||||
expect(mocks.resolveCommandConfigWithSecrets).toHaveBeenCalledOnce();
|
||||
const configResolutionRequest = mocks.resolveCommandConfigWithSecrets.mock.calls[0]?.[0];
|
||||
expect(configResolutionRequest?.commandName).toBe("channels status");
|
||||
@@ -294,6 +308,8 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
const joined = logs.join("\n");
|
||||
expect(joined).toContain("Gateway not reachable; showing config-only status.");
|
||||
expect(joined).not.toContain("Gateway auth unavailable; showing config-only status.");
|
||||
expect(joined).toContain("configured, secret unavailable in this command path");
|
||||
expect(joined).toContain("token:config (unavailable)");
|
||||
});
|
||||
@@ -319,6 +335,10 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
expect(joined).toContain("Gateway auth unavailable; showing config-only status.");
|
||||
expect(joined).not.toContain("Gateway not reachable; showing config-only status.");
|
||||
expect(joined).toContain("configured, secret unavailable in this command path");
|
||||
|
||||
const { runtime: jsonRuntime, logs: jsonLogs } = createCapturingTestRuntime();
|
||||
await channelsStatusCommand({ json: true, probe: false }, jsonRuntime as never);
|
||||
expect(JSON.parse(jsonLogs.at(-1) ?? "{}").gatewayAuthUnavailable).toBe(true);
|
||||
});
|
||||
|
||||
it("renders missing gateway credentials canonically before config-only status", async () => {
|
||||
@@ -434,7 +454,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
|
||||
|
||||
it("keeps JSON fallback structured without rendering config-only text", async () => {
|
||||
mocks.callGateway.mockRejectedValue(
|
||||
new Error(
|
||||
createGatewayTransportError(
|
||||
[
|
||||
"gateway timeout after 3000ms",
|
||||
"Gateway target: wss://user:pass@gateway.example.com/socket?token=secret-token&keep=visible",
|
||||
|
||||
@@ -37,6 +37,14 @@ describe("channels command", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
it("guides operators when no channels are configured", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({ channelAccounts: {} });
|
||||
|
||||
expect(lines).toContain(
|
||||
"- no configured chat channels (run `openclaw channels list --all` to see installable channels)",
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces Signal runtime errors in channels status output", () => {
|
||||
const lines = formatGatewayChannelsStatusLines({
|
||||
channelLabels: {
|
||||
|
||||
@@ -19,7 +19,11 @@ import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-sna
|
||||
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
||||
import { listManifestInstalledChannelIds } from "../channel-setup/discovery.js";
|
||||
import { listTrustedChannelPluginCatalogEntries } from "../channel-setup/trusted-catalog.js";
|
||||
import { formatChannelAccountLabel, requireValidChannelConfig } from "./shared.js";
|
||||
import {
|
||||
formatChannelAccountLabel,
|
||||
NO_CONFIGURED_CHAT_CHANNELS_LINE,
|
||||
requireValidChannelConfig,
|
||||
} from "./shared.js";
|
||||
|
||||
export type ChannelsListOptions = {
|
||||
json?: boolean;
|
||||
@@ -341,11 +345,7 @@ export async function channelsListCommand(
|
||||
}
|
||||
if (accountLines.length === 0 && catalogOnlyLines.length === 0) {
|
||||
lines.push(
|
||||
theme.muted(
|
||||
showAll
|
||||
? "- no chat channels found"
|
||||
: "- no configured chat channels (run `openclaw channels list --all` to see installable channels)",
|
||||
),
|
||||
theme.muted(showAll ? "- no chat channels found" : NO_CONFIGURED_CHAT_CHANNELS_LINE),
|
||||
);
|
||||
} else {
|
||||
for (const line of accountLines) {
|
||||
|
||||
@@ -12,6 +12,9 @@ import { requireValidConfig, requireValidConfigFileSnapshot } from "../config-va
|
||||
|
||||
export type ChatChannel = ChannelId;
|
||||
|
||||
export const NO_CONFIGURED_CHAT_CHANNELS_LINE =
|
||||
"- no configured chat channels (run `openclaw channels list --all` to see installable channels)";
|
||||
|
||||
export { requireValidConfigFileSnapshot };
|
||||
|
||||
/** Load valid channel command config with read-only secret resolution applied. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
appendTokenSourceBits,
|
||||
buildChannelAccountLine,
|
||||
type ChatChannel,
|
||||
NO_CONFIGURED_CHAT_CHANNELS_LINE,
|
||||
} from "./shared.js";
|
||||
|
||||
type ChannelStatusPluginLabel = {
|
||||
@@ -74,6 +75,7 @@ export async function formatConfigChannelsStatusLines(
|
||||
includeSetupFallbackPlugins: true,
|
||||
}).filter((plugin) => !requestedChannel || plugin.id === requestedChannel);
|
||||
const visibleChannelIds = new Set<string>();
|
||||
const statusLinesStart = lines.length;
|
||||
for (const plugin of plugins) {
|
||||
visibleChannelIds.add(plugin.id);
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
@@ -127,6 +129,9 @@ export async function formatConfigChannelsStatusLines(
|
||||
lines.push(`- ${hint.label}: ${hint.repairHint}`);
|
||||
}
|
||||
}
|
||||
if (lines.length === statusLinesStart) {
|
||||
lines.push(theme.muted(NO_CONFIGURED_CHAT_CHANNELS_LINE));
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
appendTokenSourceBits,
|
||||
buildChannelAccountLine,
|
||||
type ChatChannel,
|
||||
NO_CONFIGURED_CHAT_CHANNELS_LINE,
|
||||
} from "./shared.js";
|
||||
import { formatConfigChannelsStatusLines } from "./status-config-format.js";
|
||||
import type { ChannelsStatusOptions } from "./status.js";
|
||||
@@ -193,12 +194,16 @@ export function formatGatewayChannelsStatusLines(payload: Record<string, unknown
|
||||
accountPayloads[channelId] = raw as Array<Record<string, unknown>>;
|
||||
}
|
||||
}
|
||||
const accountLinesStart = lines.length;
|
||||
for (const channelId of Object.keys(accountPayloads).toSorted()) {
|
||||
const accounts = accountPayloads[channelId];
|
||||
if (accounts && accounts.length > 0) {
|
||||
lines.push(...accountLines(channelId, accounts));
|
||||
}
|
||||
}
|
||||
if (lines.length === accountLinesStart) {
|
||||
lines.push(theme.muted(NO_CONFIGURED_CHAT_CHANNELS_LINE));
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
const issues = collectChannelStatusIssues(payload);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Implements `openclaw channels status` with gateway status and config-only fallback.
|
||||
import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { formatCliFailureLines, isExpectedCliError } from "../../cli/failure-output.js";
|
||||
import {
|
||||
formatCliFailureLines,
|
||||
isExpectedCliError,
|
||||
isGatewayCredentialsCliError,
|
||||
} from "../../cli/failure-output.js";
|
||||
import { parseTimeoutMsWithFallback } from "../../cli/parse-timeout.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
@@ -77,7 +81,8 @@ export async function channelsStatusCommand(
|
||||
} catch (err) {
|
||||
const safeError = formatChannelsStatusError(err);
|
||||
const expectedError = isExpectedCliError(err);
|
||||
const gatewayAuthUnavailable = expectedError || isGatewaySecretRefUnavailableError(err);
|
||||
const gatewayAuthUnavailable =
|
||||
isGatewayCredentialsCliError(err) || isGatewaySecretRefUnavailableError(err);
|
||||
const expectedErrorOutput = expectedError
|
||||
? formatCliFailureLines({ title: "", error: err }).join("\n")
|
||||
: undefined;
|
||||
|
||||
Reference in New Issue
Block a user