mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:24:31 +00:00
fix(telegram): warn when accounts.default is missing in multi-account setup (#32544)
Merged via squash.
Prepared head SHA: 7ebc3f65b2
Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { runDoctorConfigWithInput } from "./doctor-config-flow.test-utils.js";
|
||||
@@ -23,6 +23,10 @@ import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
|
||||
const noteSpy = vi.mocked(note);
|
||||
|
||||
describe("doctor missing default account binding warning", () => {
|
||||
beforeEach(() => {
|
||||
noteSpy.mockClear();
|
||||
});
|
||||
|
||||
it("emits a doctor warning when named accounts have no valid account-scoped bindings", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
@@ -52,4 +56,67 @@ describe("doctor missing default account binding warning", () => {
|
||||
"Doctor warnings",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits a warning when multiple accounts have no explicit default", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
TELEGRAM_BOT_TOKEN: undefined,
|
||||
TELEGRAM_BOT_TOKEN_FILE: undefined,
|
||||
},
|
||||
async () => {
|
||||
await runDoctorConfigWithInput({
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
alerts: {},
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"channels.telegram: multiple accounts are configured but no explicit default is set",
|
||||
),
|
||||
"Doctor warnings",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits a warning when defaultAccount does not match configured accounts", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
TELEGRAM_BOT_TOKEN: undefined,
|
||||
TELEGRAM_BOT_TOKEN_FILE: undefined,
|
||||
},
|
||||
async () => {
|
||||
await runDoctorConfigWithInput({
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "missing",
|
||||
accounts: {
|
||||
alerts: {},
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'channels.telegram: defaultAccount is set to "missing" but does not match configured accounts',
|
||||
),
|
||||
"Doctor warnings",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { collectMissingExplicitDefaultAccountWarnings } from "./doctor-config-flow.js";
|
||||
|
||||
describe("collectMissingExplicitDefaultAccountWarnings", () => {
|
||||
it("warns when multiple named accounts are configured without default selection", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
alerts: { botToken: "a" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const warnings = collectMissingExplicitDefaultAccountWarnings(cfg);
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining("channels.telegram: multiple accounts are configured"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not warn for a single named account without default", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(collectMissingExplicitDefaultAccountWarnings(cfg)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn when accounts.default exists", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: { botToken: "d" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(collectMissingExplicitDefaultAccountWarnings(cfg)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn when defaultAccount points to a configured account", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
alerts: { botToken: "a" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(collectMissingExplicitDefaultAccountWarnings(cfg)).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes defaultAccount before validating configured account ids", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "Router D",
|
||||
accounts: {
|
||||
"router-d": { botToken: "r" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(collectMissingExplicitDefaultAccountWarnings(cfg)).toEqual([]);
|
||||
});
|
||||
|
||||
it("warns when defaultAccount is invalid for configured accounts", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "missing",
|
||||
accounts: {
|
||||
alerts: { botToken: "a" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const warnings = collectMissingExplicitDefaultAccountWarnings(cfg);
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('channels.telegram: defaultAccount is set to "missing"'),
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns across channels that support account maps", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
alerts: { botToken: "a" },
|
||||
work: { botToken: "w" },
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
accounts: {
|
||||
a: { botToken: "x" },
|
||||
b: { botToken: "y" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const warnings = collectMissingExplicitDefaultAccountWarnings(cfg);
|
||||
expect(warnings).toHaveLength(2);
|
||||
expect(warnings.some((line) => line.includes("channels.telegram"))).toBe(true);
|
||||
expect(warnings.some((line) => line.includes("channels.slack"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,16 @@ import {
|
||||
normalizeTrustedSafeBinDirs,
|
||||
} from "../infra/exec-safe-bin-trust.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import {
|
||||
formatChannelAccountsDefaultPath,
|
||||
formatSetExplicitDefaultInstruction,
|
||||
formatSetExplicitDefaultToConfiguredInstruction,
|
||||
} from "../routing/default-account-warnings.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import {
|
||||
isDiscordMutableAllowEntry,
|
||||
isGoogleChatMutableAllowEntry,
|
||||
@@ -215,15 +224,21 @@ function normalizeBindingChannelKey(raw?: string | null): string {
|
||||
return (raw ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig): string[] {
|
||||
type ChannelMissingDefaultAccountContext = {
|
||||
channelKey: string;
|
||||
channel: Record<string, unknown>;
|
||||
normalizedAccountIds: string[];
|
||||
};
|
||||
|
||||
function collectChannelsMissingDefaultAccount(
|
||||
cfg: OpenClawConfig,
|
||||
): ChannelMissingDefaultAccountContext[] {
|
||||
const channels = asObjectRecord(cfg.channels);
|
||||
if (!channels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const contexts: ChannelMissingDefaultAccountContext[] = [];
|
||||
for (const [channelKey, rawChannel] of Object.entries(channels)) {
|
||||
const channel = asObjectRecord(rawChannel);
|
||||
if (!channel) {
|
||||
@@ -240,10 +255,20 @@ export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig)
|
||||
.map((accountId) => normalizeAccountId(accountId))
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
).toSorted((a, b) => a.localeCompare(b));
|
||||
if (normalizedAccountIds.length === 0 || normalizedAccountIds.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
continue;
|
||||
}
|
||||
contexts.push({ channelKey, channel, normalizedAccountIds });
|
||||
}
|
||||
return contexts;
|
||||
}
|
||||
|
||||
export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig): string[] {
|
||||
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const { channelKey, normalizedAccountIds } of collectChannelsMissingDefaultAccount(cfg)) {
|
||||
const accountIdSet = new Set(normalizedAccountIds);
|
||||
const channelPattern = normalizeBindingChannelKey(channelKey);
|
||||
|
||||
@@ -291,13 +316,43 @@ export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig)
|
||||
}
|
||||
if (coveredAccountIds.size > 0) {
|
||||
warnings.push(
|
||||
`- channels.${channelKey}: accounts.default is missing and account bindings only cover a subset of configured accounts. Uncovered accounts: ${uncoveredAccountIds.join(", ")}. Add bindings[].match.accountId for uncovered accounts (or "*"), or add channels.${channelKey}.accounts.default.`,
|
||||
`- channels.${channelKey}: accounts.default is missing and account bindings only cover a subset of configured accounts. Uncovered accounts: ${uncoveredAccountIds.join(", ")}. Add bindings[].match.accountId for uncovered accounts (or "*"), or add ${formatChannelAccountsDefaultPath(channelKey)}.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
warnings.push(
|
||||
`- channels.${channelKey}: accounts.default is missing and no valid account-scoped binding exists for configured accounts (${normalizedAccountIds.join(", ")}). Channel-only bindings (no accountId) match only default. Add bindings[].match.accountId for one of these accounts (or "*"), or add channels.${channelKey}.accounts.default.`,
|
||||
`- channels.${channelKey}: accounts.default is missing and no valid account-scoped binding exists for configured accounts (${normalizedAccountIds.join(", ")}). Channel-only bindings (no accountId) match only default. Add bindings[].match.accountId for one of these accounts (or "*"), or add ${formatChannelAccountsDefaultPath(channelKey)}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export function collectMissingExplicitDefaultAccountWarnings(cfg: OpenClawConfig): string[] {
|
||||
const warnings: string[] = [];
|
||||
for (const { channelKey, channel, normalizedAccountIds } of collectChannelsMissingDefaultAccount(
|
||||
cfg,
|
||||
)) {
|
||||
if (normalizedAccountIds.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const preferredDefault = normalizeOptionalAccountId(
|
||||
typeof channel.defaultAccount === "string" ? channel.defaultAccount : undefined,
|
||||
);
|
||||
if (preferredDefault) {
|
||||
if (normalizedAccountIds.includes(preferredDefault)) {
|
||||
continue;
|
||||
}
|
||||
warnings.push(
|
||||
`- channels.${channelKey}: defaultAccount is set to "${preferredDefault}" but does not match configured accounts (${normalizedAccountIds.join(", ")}). ${formatSetExplicitDefaultToConfiguredInstruction({ channelKey })} to avoid fallback routing.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
warnings.push(
|
||||
`- channels.${channelKey}: multiple accounts are configured but no explicit default is set. ${formatSetExplicitDefaultInstruction(channelKey)} to avoid fallback routing.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1812,6 +1867,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
if (missingDefaultAccountBindingWarnings.length > 0) {
|
||||
note(missingDefaultAccountBindingWarnings.join("\n"), "Doctor warnings");
|
||||
}
|
||||
const missingExplicitDefaultWarnings = collectMissingExplicitDefaultAccountWarnings(candidate);
|
||||
if (missingExplicitDefaultWarnings.length > 0) {
|
||||
note(missingExplicitDefaultWarnings.join("\n"), "Doctor warnings");
|
||||
}
|
||||
|
||||
if (shouldRepair) {
|
||||
const repair = await maybeRepairTelegramAllowFromUsernames(candidate);
|
||||
|
||||
Reference in New Issue
Block a user