mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(status): trust live channel credential state
This commit is contained in:
82
src/commands/status-all/channels.test.ts
Normal file
82
src/commands/status-all/channels.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildChannelsTable } from "./channels.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveInspectedChannelAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
const discordPlugin = {
|
||||
id: "discord",
|
||||
meta: { label: "Discord" },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("../../channels/account-inspection.js", () => ({
|
||||
resolveInspectedChannelAccount: mocks.resolveInspectedChannelAccount,
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/read-only.js", () => ({
|
||||
listReadOnlyChannelPluginsForConfig: () => [discordPlugin],
|
||||
}));
|
||||
|
||||
describe("buildChannelsTable", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveInspectedChannelAccount.mockResolvedValue({
|
||||
account: {
|
||||
tokenStatus: "configured_unavailable",
|
||||
tokenSource: "secretref",
|
||||
},
|
||||
enabled: true,
|
||||
configured: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a live gateway-backed account OK when local status cannot resolve the token", async () => {
|
||||
const table = await buildChannelsTable(
|
||||
{ channels: { discord: { enabled: true } } },
|
||||
{
|
||||
liveChannelStatus: {
|
||||
channelAccounts: {
|
||||
discord: [
|
||||
{
|
||||
accountId: "default",
|
||||
running: true,
|
||||
connected: true,
|
||||
tokenStatus: "available",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(table.rows).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "discord",
|
||||
state: "ok",
|
||||
detail: expect.not.stringContaining("unavailable"),
|
||||
}),
|
||||
);
|
||||
expect(table.details[0]?.rows[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
Status: "OK",
|
||||
Notes: expect.stringContaining("credential available in gateway runtime"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when a configured token is unavailable and there is no live account proof", async () => {
|
||||
const table = await buildChannelsTable({ channels: { discord: { enabled: true } } });
|
||||
|
||||
expect(table.rows).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "discord",
|
||||
state: "warn",
|
||||
detail: expect.stringContaining("unavailable"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,63 @@ type ResolvedChannelAccountRowParams = {
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
function getLiveChannelAccounts(params: {
|
||||
liveChannelStatus: unknown;
|
||||
channelId: string;
|
||||
}): Array<Record<string, unknown>> {
|
||||
const payload = asRecord(params.liveChannelStatus);
|
||||
const accountsByChannel = asRecord(payload.channelAccounts);
|
||||
const raw = accountsByChannel[params.channelId];
|
||||
return Array.isArray(raw) ? raw.map(asRecord) : [];
|
||||
}
|
||||
|
||||
function getLiveAccountId(account: Record<string, unknown>): string {
|
||||
return (
|
||||
normalizeOptionalString(account.accountId) ??
|
||||
normalizeOptionalString(account.id) ??
|
||||
normalizeOptionalString(account.name) ??
|
||||
"default"
|
||||
);
|
||||
}
|
||||
|
||||
function findLiveChannelAccount(params: {
|
||||
liveAccounts: Array<Record<string, unknown>>;
|
||||
accountId: string;
|
||||
}): Record<string, unknown> | null {
|
||||
return (
|
||||
params.liveAccounts.find((account) => getLiveAccountId(account) === params.accountId) ??
|
||||
(params.accountId === "default" && params.liveAccounts.length === 1
|
||||
? (params.liveAccounts[0] ?? null)
|
||||
: null)
|
||||
);
|
||||
}
|
||||
|
||||
function hasLiveCredentialAvailable(params: {
|
||||
liveAccounts: Array<Record<string, unknown>>;
|
||||
accountId: string;
|
||||
}): boolean {
|
||||
const account = findLiveChannelAccount(params);
|
||||
if (!account) {
|
||||
return false;
|
||||
}
|
||||
if (hasConfiguredUnavailableCredentialStatus(account)) {
|
||||
return false;
|
||||
}
|
||||
return account.running === true || account.connected === true;
|
||||
}
|
||||
|
||||
function markConfiguredUnavailableCredentialStatusesAvailable(
|
||||
account: unknown,
|
||||
): Record<string, unknown> {
|
||||
const record = { ...asRecord(account) };
|
||||
for (const key of ["tokenStatus", "botTokenStatus", "appTokenStatus", "signingSecretStatus"]) {
|
||||
if (record[key] === "configured_unavailable") {
|
||||
record[key] = "available";
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function existsSyncMaybe(p: string | undefined): boolean | null {
|
||||
const path = normalizeOptionalString(p) ?? "";
|
||||
if (!path) {
|
||||
@@ -87,6 +144,7 @@ const buildAccountNotes = (params: {
|
||||
plugin: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
entry: ChannelAccountRow;
|
||||
liveCredentialAvailable?: boolean;
|
||||
}) => {
|
||||
const { plugin, cfg, entry } = params;
|
||||
const notes: string[] = [];
|
||||
@@ -112,7 +170,9 @@ const buildAccountNotes = (params: {
|
||||
) {
|
||||
notes.push(`signing:${snapshot.signingSecretSource}`);
|
||||
}
|
||||
if (hasConfiguredUnavailableCredentialStatus(entry.account)) {
|
||||
if (params.liveCredentialAvailable) {
|
||||
notes.push("credential available in gateway runtime");
|
||||
} else if (hasConfiguredUnavailableCredentialStatus(entry.account)) {
|
||||
notes.push("secret unavailable in this command path");
|
||||
}
|
||||
if (snapshot.baseUrl) {
|
||||
@@ -192,6 +252,7 @@ export async function buildChannelsTable(
|
||||
showSecrets?: boolean;
|
||||
sourceConfig?: OpenClawConfig;
|
||||
includeSetupFallbackPlugins?: boolean;
|
||||
liveChannelStatus?: unknown;
|
||||
},
|
||||
): Promise<{
|
||||
rows: ChannelRow[];
|
||||
@@ -234,12 +295,27 @@ export async function buildChannelsTable(
|
||||
}),
|
||||
);
|
||||
}
|
||||
const liveAccounts = getLiveChannelAccounts({
|
||||
liveChannelStatus: opts?.liveChannelStatus,
|
||||
channelId: plugin.id,
|
||||
});
|
||||
|
||||
const anyEnabled = accounts.some((a) => a.enabled);
|
||||
const enabledAccounts = accounts.filter((a) => a.enabled);
|
||||
const configuredAccounts = enabledAccounts.filter((a) => a.configured);
|
||||
const unavailableConfiguredAccounts = enabledAccounts.filter((a) =>
|
||||
hasConfiguredUnavailableCredentialStatus(a.account),
|
||||
const unavailableConfiguredAccounts = enabledAccounts.filter(
|
||||
(a) =>
|
||||
hasConfiguredUnavailableCredentialStatus(a.account) &&
|
||||
!hasLiveCredentialAvailable({ liveAccounts, accountId: a.accountId }),
|
||||
);
|
||||
const accountsForTokenSummary = accounts.map((entry) =>
|
||||
hasConfiguredUnavailableCredentialStatus(entry.account) &&
|
||||
hasLiveCredentialAvailable({ liveAccounts, accountId: entry.accountId })
|
||||
? {
|
||||
...entry,
|
||||
account: markConfiguredUnavailableCredentialStatusesAvailable(entry.account),
|
||||
}
|
||||
: entry,
|
||||
);
|
||||
const defaultEntry = accounts.find((a) => a.accountId === defaultAccountId) ?? accounts[0];
|
||||
|
||||
@@ -256,7 +332,7 @@ export async function buildChannelsTable(
|
||||
const link = resolveLinkFields(summary);
|
||||
const missingPaths = collectMissingPaths(enabledAccounts);
|
||||
const tokenSummary = summarizeTokenConfig({
|
||||
accounts,
|
||||
accounts: accountsForTokenSummary,
|
||||
showSecrets,
|
||||
});
|
||||
|
||||
@@ -383,14 +459,19 @@ export async function buildChannelsTable(
|
||||
title: `${label} accounts`,
|
||||
columns: ["Account", "Status", "Notes"],
|
||||
rows: configuredAccounts.map((entry) => {
|
||||
const notes = buildAccountNotes({ plugin, cfg, entry });
|
||||
const liveCredentialAvailable = hasLiveCredentialAvailable({
|
||||
liveAccounts,
|
||||
accountId: entry.accountId,
|
||||
});
|
||||
const notes = buildAccountNotes({ plugin, cfg, entry, liveCredentialAvailable });
|
||||
return {
|
||||
Account: formatAccountLabel({
|
||||
accountId: entry.accountId,
|
||||
name: entry.snapshot.name,
|
||||
}),
|
||||
Status:
|
||||
entry.enabled && !hasConfiguredUnavailableCredentialStatus(entry.account)
|
||||
entry.enabled &&
|
||||
(!hasConfiguredUnavailableCredentialStatus(entry.account) || liveCredentialAvailable)
|
||||
? "OK"
|
||||
: "WARN",
|
||||
Notes: notes.join(" · "),
|
||||
|
||||
@@ -251,6 +251,7 @@ export async function collectStatusScanOverview(params: {
|
||||
showSecrets: params.showSecrets,
|
||||
sourceConfig,
|
||||
includeSetupFallbackPlugins: params.includeChannelSetupRuntimeFallback !== false,
|
||||
liveChannelStatus: channelsStatus,
|
||||
});
|
||||
params.progress?.tick();
|
||||
return { channelsStatus, channelIssues, channels };
|
||||
|
||||
Reference in New Issue
Block a user