fix: honor selected account in setup status

This commit is contained in:
Tak Hoffman
2026-04-03 11:48:58 -05:00
parent 5edefc4d5b
commit 51f6bc4940
19 changed files with 124 additions and 58 deletions

View File

@@ -166,9 +166,9 @@ export const blueBubblesSetupWizard: ChannelSetupWizard = {
configuredScore: 1,
unconfiguredScore: 0,
includeStatusLine: true,
resolveConfigured: ({ cfg }) =>
listBlueBubblesAccountIds(cfg).some((accountId) => {
const account = resolveBlueBubblesAccount({ cfg, accountId });
resolveConfigured: ({ cfg, accountId }) =>
(accountId ? [accountId] : listBlueBubblesAccountIds(cfg)).some((resolvedAccountId) => {
const account = resolveBlueBubblesAccount({ cfg, accountId: resolvedAccountId });
return account.configured;
}),
}),

View File

@@ -108,9 +108,9 @@ export function createDiscordSetupWizardBase(handlers: {
unconfiguredHint: "needs token",
configuredScore: 2,
unconfiguredScore: 1,
resolveConfigured: ({ cfg }) =>
listDiscordSetupAccountIds(cfg).some((accountId) => {
const account = inspectDiscordSetupAccount({ cfg, accountId });
resolveConfigured: ({ cfg, accountId }) =>
(accountId ? [accountId] : listDiscordSetupAccountIds(cfg)).some((resolvedAccountId) => {
const account = inspectDiscordSetupAccount({ cfg, accountId: resolvedAccountId });
return account.configured;
}),
}),

View File

@@ -92,10 +92,14 @@ export const googlechatSetupWizard: ChannelSetupWizard = {
configuredHint: "configured",
unconfiguredHint: "needs auth",
includeStatusLine: true,
resolveConfigured: ({ cfg }) =>
listGoogleChatAccountIds(cfg).some(
(accountId) => resolveGoogleChatAccount({ cfg, accountId }).credentialSource !== "none",
),
resolveConfigured: ({ cfg, accountId }) =>
accountId
? resolveGoogleChatAccount({ cfg, accountId }).credentialSource !== "none"
: listGoogleChatAccountIds(cfg).some(
(resolvedAccountId) =>
resolveGoogleChatAccount({ cfg, accountId: resolvedAccountId }).credentialSource !==
"none",
),
}),
introNote: {
title: "Google Chat setup",

View File

@@ -2,6 +2,7 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPluginSetupWizardConfigure,
createPluginSetupWizardStatus,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
@@ -30,6 +31,7 @@ vi.mock("./monitor.js", async () => {
});
const googlechatConfigure = createPluginSetupWizardConfigure(googlechatPlugin);
const googlechatStatus = createPluginSetupWizardStatus(googlechatPlugin);
function buildAccount(): ResolvedGoogleChatAccount {
return {
@@ -186,6 +188,29 @@ describe("googlechat setup", () => {
).toBe("allowlist");
});
it("reports configured state for the selected account instead of any account", async () => {
const status = await googlechatStatus({
cfg: {
channels: {
googlechat: {
accounts: {
default: {
serviceAccount: { client_email: "default@example.com" },
},
alerts: {},
},
},
},
} as OpenClawConfig,
accountOverrides: {
googlechat: "alerts",
},
options: {},
});
expect(status.configured).toBe(false);
});
it("reports account-scoped config keys for named accounts", () => {
expect(googlechatPlugin.setupWizard?.dmPolicy?.resolveConfigKeys?.({}, "alerts")).toEqual({
policyKey: "channels.googlechat.accounts.alerts.dm.policy",

View File

@@ -90,8 +90,10 @@ export const lineSetupWizard: ChannelSetupWizard = {
configuredScore: 1,
unconfiguredScore: 0,
includeStatusLine: true,
resolveConfigured: ({ cfg }) =>
listLineAccountIds(cfg).some((accountId) => isLineConfigured(cfg, accountId)),
resolveConfigured: ({ cfg, accountId }) =>
accountId
? isLineConfigured(cfg, accountId)
: listLineAccountIds(cfg).some((resolvedAccountId) => isLineConfigured(cfg, resolvedAccountId)),
resolveExtraStatusLines: ({ cfg }) => [`Accounts: ${listLineAccountIds(cfg).length || 0}`],
}),
introNote: {

View File

@@ -30,10 +30,12 @@ export const mattermostSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "needs setup",
configuredScore: 2,
unconfiguredScore: 1,
resolveConfigured: ({ cfg }) =>
listMattermostAccountIds(cfg).some((accountId) =>
isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId)),
),
resolveConfigured: ({ cfg, accountId }) =>
accountId
? isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId))
: listMattermostAccountIds(cfg).some((resolvedAccountId) =>
isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, resolvedAccountId)),
),
}),
introNote: {
title: "Mattermost bot token",

View File

@@ -33,11 +33,19 @@ export const nextcloudTalkSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "self-hosted chat",
configuredScore: 1,
unconfiguredScore: 5,
resolveConfigured: ({ cfg }) =>
listNextcloudTalkAccountIds(cfg as CoreConfig).some((accountId) => {
resolveConfigured: ({ cfg, accountId }) => {
if (accountId) {
const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
return Boolean(account.secret && account.baseUrl);
}),
}
return listNextcloudTalkAccountIds(cfg as CoreConfig).some((resolvedAccountId) => {
const account = resolveNextcloudTalkAccount({
cfg: cfg as CoreConfig,
accountId: resolvedAccountId,
});
return Boolean(account.secret && account.baseUrl);
});
},
}),
introNote: {
title: "Nextcloud Talk bot setup",

View File

@@ -28,10 +28,12 @@ export const signalSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "signal-cli missing",
configuredScore: 1,
unconfiguredScore: 0,
resolveConfigured: ({ cfg }) =>
listSignalAccountIds(cfg).some(
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
),
resolveConfigured: ({ cfg, accountId }) =>
accountId
? resolveSignalAccount({ cfg, accountId }).configured
: listSignalAccountIds(cfg).some(
(resolvedAccountId) => resolveSignalAccount({ cfg, accountId: resolvedAccountId }).configured,
),
resolveBinaryPath: ({ cfg }) => cfg.channels?.signal?.cliPath ?? "signal-cli",
detectBinary,
}),

View File

@@ -166,9 +166,9 @@ export function createSlackSetupWizardBase(handlers: {
unconfiguredHint: "needs tokens",
configuredScore: 2,
unconfiguredScore: 1,
resolveConfigured: ({ cfg }) =>
listSlackAccountIds(cfg).some((accountId) => {
const account = inspectSlackAccount({ cfg, accountId });
resolveConfigured: ({ cfg, accountId }) =>
(accountId ? [accountId] : listSlackAccountIds(cfg)).some((resolvedAccountId) => {
const account = inspectSlackAccount({ cfg, accountId: resolvedAccountId });
return account.configured;
}),
}),

View File

@@ -122,11 +122,11 @@ export const telegramSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "recommended · newcomer-friendly",
configuredScore: 1,
unconfiguredScore: 10,
resolveConfigured: ({ cfg }) =>
listTelegramAccountIds(cfg).some((accountId) => {
const account = inspectTelegramAccount({ cfg, accountId });
return account.configured;
}),
resolveConfigured: ({ cfg, accountId }) =>
(accountId ? [accountId] : listTelegramAccountIds(cfg)).some((resolvedAccountId) => {
const account = inspectTelegramAccount({ cfg, accountId: resolvedAccountId });
return account.configured;
}),
}),
prepare: async ({ cfg, accountId, credentialValues }) => ({
cfg: ensureTelegramDefaultGroupMentionGate(cfg, accountId),

View File

@@ -19,20 +19,20 @@ export const whatsappSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "not linked",
configuredScore: 5,
unconfiguredScore: 4,
resolveConfigured: async ({ cfg }) => {
for (const accountId of listWhatsAppAccountIds(cfg)) {
if (await detectWhatsAppLinked(cfg, accountId)) {
resolveConfigured: async ({ cfg, accountId }) => {
for (const resolvedAccountId of accountId ? [accountId] : listWhatsAppAccountIds(cfg)) {
if (await detectWhatsAppLinked(cfg, resolvedAccountId)) {
return true;
}
}
return false;
},
resolveStatusLines: async ({ cfg, configured }) => {
resolveStatusLines: async ({ cfg, accountId, configured }) => {
const linkedAccountId = (
await Promise.all(
listWhatsAppAccountIds(cfg).map(async (accountId) => ({
accountId,
linked: await detectWhatsAppLinked(cfg, accountId),
(accountId ? [accountId] : listWhatsAppAccountIds(cfg)).map(async (resolvedAccountId) => ({
accountId: resolvedAccountId,
linked: await detectWhatsAppLinked(cfg, resolvedAccountId),
})),
)
).find((entry) => entry.linked)?.accountId;

View File

@@ -185,11 +185,11 @@ export const zaloSetupWizard: ChannelSetupWizard = {
configuredScore: 1,
unconfiguredScore: 10,
includeStatusLine: true,
resolveConfigured: ({ cfg }) =>
listZaloAccountIds(cfg).some((accountId) => {
resolveConfigured: ({ cfg, accountId }) =>
(accountId ? [accountId] : listZaloAccountIds(cfg)).some((resolvedAccountId) => {
const account = resolveZaloAccount({
cfg,
accountId,
accountId: resolvedAccountId,
allowUnresolvedSecretRef: true,
});
return (

View File

@@ -302,19 +302,22 @@ export const zalouserSetupWizard: ChannelSetupWizard = {
unconfiguredHint: "recommended · QR login",
configuredScore: 1,
unconfiguredScore: 15,
resolveConfigured: async ({ cfg }) => {
const ids = listZalouserAccountIds(cfg);
for (const accountId of ids) {
const account = resolveZalouserAccountSync({ cfg, accountId });
resolveConfigured: async ({ cfg, accountId }) => {
const ids = accountId ? [accountId] : listZalouserAccountIds(cfg);
for (const resolvedAccountId of ids) {
const account = resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId });
if (await checkZcaAuthenticated(account.profile)) {
return true;
}
}
return false;
},
resolveStatusLines: async ({ cfg, configured }) => {
resolveStatusLines: async ({ cfg, accountId, configured }) => {
void cfg;
return [`Zalo Personal: ${configured ? "logged in" : "needs QR login"}`];
const label = accountId && accountId !== DEFAULT_ACCOUNT_ID
? `Zalo Personal (${accountId})`
: "Zalo Personal";
return [`${label}: ${configured ? "logged in" : "needs QR login"}`];
},
},
prepare: async ({ cfg, accountId, prompter, options }) => {

View File

@@ -9,6 +9,7 @@ import type { ChannelSetupWizard } from "./setup-wizard.js";
describe("createDetectedBinaryStatus", () => {
it("builds status lines, hint, and score from binary detection", async () => {
const resolveConfigured = vi.fn(() => true);
const status = createDetectedBinaryStatus({
channelLabel: "Signal",
binaryLabel: "signal-cli",
@@ -18,12 +19,13 @@ describe("createDetectedBinaryStatus", () => {
unconfiguredHint: "signal-cli missing",
configuredScore: 1,
unconfiguredScore: 0,
resolveConfigured: () => true,
resolveConfigured,
resolveBinaryPath: () => "/usr/local/bin/signal-cli",
detectBinary: vi.fn(async () => true),
});
expect(await status.resolveConfigured({ cfg: {} })).toBe(true);
expect(await status.resolveConfigured({ cfg: {}, accountId: "work" })).toBe(true);
expect(resolveConfigured).toHaveBeenCalledWith({ cfg: {}, accountId: "work" });
expect(await status.resolveStatusLines?.({ cfg: {}, configured: true })).toEqual([
"Signal: configured",
"signal-cli: found (/usr/local/bin/signal-cli)",

View File

@@ -18,7 +18,10 @@ export function createDetectedBinaryStatus(params: {
unconfiguredHint: string;
configuredScore: number;
unconfiguredScore: number;
resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise<boolean>;
resolveConfigured: (params: {
cfg: OpenClawConfig;
accountId?: string;
}) => boolean | Promise<boolean>;
resolveBinaryPath: (params: { cfg: OpenClawConfig }) => string;
detectBinary?: (path: string) => Promise<boolean>;
}): ChannelSetupWizardStatus {

View File

@@ -172,6 +172,7 @@ export function createStandardChannelSetupStatus(params: {
resolveConfigured: ChannelSetupWizardStatus["resolveConfigured"];
resolveExtraStatusLines?: (params: {
cfg: OpenClawConfig;
accountId?: string;
configured: boolean;
}) => string[] | Promise<string[]>;
}): ChannelSetupWizardStatus {
@@ -190,13 +191,14 @@ export function createStandardChannelSetupStatus(params: {
};
if (params.includeStatusLine || params.resolveExtraStatusLines) {
status.resolveStatusLines = async ({ cfg, configured }) => {
status.resolveStatusLines = async ({ cfg, accountId, configured }) => {
const lines = params.includeStatusLine
? [
`${params.channelLabel}: ${configured ? params.configuredLabel : params.unconfiguredLabel}`,
]
: [];
const extraLines = (await params.resolveExtraStatusLines?.({ cfg, configured })) ?? [];
const extraLines =
(await params.resolveExtraStatusLines?.({ cfg, accountId, configured })) ?? [];
return [...lines, ...extraLines];
};
}

View File

@@ -23,7 +23,8 @@ describe("createDelegatedResolveConfigured", () => {
status: {
configuredLabel: "configured",
unconfiguredLabel: "needs setup",
resolveConfigured: async ({ cfg }) => Boolean(cfg.channels?.demo),
resolveConfigured: async ({ cfg, accountId }) =>
Boolean(cfg.channels?.[accountId ?? "demo"]),
},
credentials: [],
}),
@@ -32,7 +33,9 @@ describe("createDelegatedResolveConfigured", () => {
const resolveConfigured = createDelegatedResolveConfigured(loadWizard);
expect(await resolveConfigured({ cfg: {} })).toBe(false);
expect(await resolveConfigured({ cfg: { channels: { demo: {} } } })).toBe(true);
expect(await resolveConfigured({ cfg: { channels: { work: {} } }, accountId: "work" })).toBe(
true,
);
});
});

View File

@@ -16,8 +16,8 @@ type ResolveGroupAllowlistParams = Parameters<
>[0];
export function createDelegatedResolveConfigured(loadWizard: () => Promise<ChannelSetupWizard>) {
return async ({ cfg }: ResolveConfiguredParams) =>
await (await loadWizard()).status.resolveConfigured({ cfg });
return async ({ cfg, accountId }: ResolveConfiguredParams) =>
await (await loadWizard()).status.resolveConfigured({ cfg, accountId });
}
export function createDelegatedPrepare(loadWizard: () => Promise<ChannelSetupWizard>) {

View File

@@ -26,17 +26,23 @@ export type ChannelSetupWizardStatus = {
unconfiguredHint?: string;
configuredScore?: number;
unconfiguredScore?: number;
resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise<boolean>;
resolveConfigured: (params: {
cfg: OpenClawConfig;
accountId?: string;
}) => boolean | Promise<boolean>;
resolveStatusLines?: (params: {
cfg: OpenClawConfig;
accountId?: string;
configured: boolean;
}) => string[] | Promise<string[]>;
resolveSelectionHint?: (params: {
cfg: OpenClawConfig;
accountId?: string;
configured: boolean;
}) => string | undefined | Promise<string | undefined>;
resolveQuickstartScore?: (params: {
cfg: OpenClawConfig;
accountId?: string;
configured: boolean;
}) => number | undefined | Promise<number | undefined>;
};
@@ -283,9 +289,11 @@ async function buildStatus(
wizard: ChannelSetupWizard,
ctx: ChannelSetupStatusContext,
): Promise<ChannelSetupStatus> {
const configured = await wizard.status.resolveConfigured({ cfg: ctx.cfg });
const accountId = ctx.accountOverrides[plugin.id];
const configured = await wizard.status.resolveConfigured({ cfg: ctx.cfg, accountId });
const statusLines = (await wizard.status.resolveStatusLines?.({
cfg: ctx.cfg,
accountId,
configured,
})) ?? [
`${plugin.meta.label}: ${configured ? wizard.status.configuredLabel : wizard.status.unconfiguredLabel}`,
@@ -293,11 +301,13 @@ async function buildStatus(
const selectionHint =
(await wizard.status.resolveSelectionHint?.({
cfg: ctx.cfg,
accountId,
configured,
})) ?? (configured ? wizard.status.configuredHint : wizard.status.unconfiguredHint);
const quickstartScore =
(await wizard.status.resolveQuickstartScore?.({
cfg: ctx.cfg,
accountId,
configured,
})) ?? (configured ? wizard.status.configuredScore : wizard.status.unconfiguredScore);
return {