Doctor: warn on partial missing-default account coverage

This commit is contained in:
Gustavo Madeira Santana
2026-02-26 04:01:17 -05:00
parent 231fa3a07f
commit 50b5771808
3 changed files with 103 additions and 9 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { runDoctorConfigWithInput } from "./doctor-config-flow.test-utils.js";
const { noteSpy } = vi.hoisted(() => ({
noteSpy: vi.fn(),
}));
vi.mock("../terminal/note.js", () => ({
note: noteSpy,
}));
vi.mock("./doctor-legacy-config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./doctor-legacy-config.js")>();
return {
...actual,
normalizeLegacyConfigValues: (cfg: unknown) => ({
config: cfg,
changes: [],
}),
};
});
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
describe("doctor missing default account binding warning", () => {
it("emits a doctor warning when named accounts have no valid account-scoped bindings", async () => {
await withEnvAsync(
{
TELEGRAM_BOT_TOKEN: undefined,
TELEGRAM_BOT_TOKEN_FILE: undefined,
},
async () => {
await runDoctorConfigWithInput({
config: {
channels: {
telegram: {
accounts: {
alerts: {},
work: {},
},
},
},
bindings: [{ agentId: "ops", match: { channel: "telegram" } }],
},
run: loadAndMaybeMigrateDoctorConfig,
});
},
);
expect(noteSpy).toHaveBeenCalledWith(
expect.stringContaining("channels.telegram: accounts.default is missing"),
"Doctor warnings",
);
});
});

View File

@@ -37,6 +37,25 @@ describe("collectMissingDefaultAccountBindingWarnings", () => {
expect(collectMissingDefaultAccountBindingWarnings(cfg)).toEqual([]);
});
it("warns when bindings cover only a subset of configured accounts", () => {
const cfg: OpenClawConfig = {
channels: {
telegram: {
accounts: {
alerts: { botToken: "a" },
work: { botToken: "w" },
},
},
},
bindings: [{ agentId: "ops", match: { channel: "telegram", accountId: "alerts" } }],
};
const warnings = collectMissingDefaultAccountBindingWarnings(cfg);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain("subset");
expect(warnings[0]).toContain("Uncovered accounts: work");
});
it("does not warn when wildcard account binding exists", () => {
const cfg: OpenClawConfig = {
channels: {

View File

@@ -249,33 +249,52 @@ export function collectMissingDefaultAccountBindingWarnings(cfg: OpenClawConfig)
const accountIdSet = new Set(normalizedAccountIds);
const channelPattern = normalizeBindingChannelKey(channelKey);
const hasValidBinding = bindings.some((binding) => {
let hasWildcardBinding = false;
const coveredAccountIds = new Set<string>();
for (const binding of bindings) {
const bindingRecord = asObjectRecord(binding);
if (!bindingRecord) {
return false;
continue;
}
const match = asObjectRecord(bindingRecord.match);
if (!match) {
return false;
continue;
}
const matchChannel =
typeof match.channel === "string" ? normalizeBindingChannelKey(match.channel) : "";
if (!matchChannel || matchChannel !== channelPattern) {
return false;
continue;
}
const rawAccountId = typeof match.accountId === "string" ? match.accountId.trim() : "";
if (!rawAccountId) {
return false;
continue;
}
if (rawAccountId === "*") {
return true;
hasWildcardBinding = true;
continue;
}
return accountIdSet.has(normalizeAccountId(rawAccountId));
});
const normalizedBindingAccountId = normalizeAccountId(rawAccountId);
if (accountIdSet.has(normalizedBindingAccountId)) {
coveredAccountIds.add(normalizedBindingAccountId);
}
}
if (hasValidBinding) {
if (hasWildcardBinding) {
continue;
}
const uncoveredAccountIds = normalizedAccountIds.filter(
(accountId) => !coveredAccountIds.has(accountId),
);
if (uncoveredAccountIds.length === 0) {
continue;
}
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.`,
);
continue;
}