mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(doctor): resolve plugin account identities
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getDoctorChannelCapabilities,
|
||||
listDoctorChannelAccountIds,
|
||||
resolveDoctorChannelAccountIds,
|
||||
} from "./channel-capabilities.js";
|
||||
|
||||
const channelPluginMocks = vi.hoisted(() => ({
|
||||
@@ -74,6 +74,22 @@ describe("doctor channel capabilities", () => {
|
||||
throw new Error("missing generated bundled module");
|
||||
});
|
||||
|
||||
expect(listDoctorChannelAccountIds("telegram", {})).toBeUndefined();
|
||||
expect(resolveDoctorChannelAccountIds("telegram", {}, [])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves configured and runtime account ids through plugin semantics", () => {
|
||||
channelPluginMocks.getChannelPlugin.mockReturnValue({
|
||||
config: {
|
||||
listAccountIds: () => ["default", "Work"],
|
||||
resolveAccount: (_cfg: unknown, accountId?: string | null) => ({
|
||||
accountId: accountId === "Work" ? "work" : accountId,
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(resolveDoctorChannelAccountIds("signal", {}, ["Work"])).toEqual({
|
||||
configured: ["work"],
|
||||
runtime: ["default", "work"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,18 +68,45 @@ export function getDoctorChannelCapabilities(channelName?: string): DoctorChanne
|
||||
return mergeDoctorChannelCapabilities(getManifestDoctorCapabilities(channelId));
|
||||
}
|
||||
|
||||
/** Resolve the account ids a channel plugin would activate for the current config. */
|
||||
export function listDoctorChannelAccountIds(
|
||||
type DoctorChannelAccountIds = {
|
||||
configured: string[];
|
||||
runtime: string[];
|
||||
};
|
||||
|
||||
function readResolvedAccountId(account: unknown): string | undefined {
|
||||
if (!account || typeof account !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const accountId = (account as { accountId?: unknown }).accountId;
|
||||
return typeof accountId === "string" && accountId ? accountId : undefined;
|
||||
}
|
||||
|
||||
/** Resolve configured and runtime account ids through the channel plugin's own semantics. */
|
||||
export function resolveDoctorChannelAccountIds(
|
||||
channelName: string,
|
||||
cfg: OpenClawConfig,
|
||||
): string[] | undefined {
|
||||
configuredAccountIds: string[],
|
||||
): DoctorChannelAccountIds | undefined {
|
||||
const channelId = normalizeAnyChannelId(channelName);
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const plugin = getChannelPlugin(channelId) ?? getBundledChannelPlugin(channelId);
|
||||
return plugin?.config.listAccountIds(cfg);
|
||||
if (!plugin) {
|
||||
return undefined;
|
||||
}
|
||||
const resolveAccountIds = (accountIds: string[]): string[] | undefined => {
|
||||
const resolved = accountIds.map((accountId) =>
|
||||
readResolvedAccountId(plugin.config.resolveAccount(cfg, accountId)),
|
||||
);
|
||||
return resolved.every((accountId): accountId is string => accountId !== undefined)
|
||||
? resolved
|
||||
: undefined;
|
||||
};
|
||||
const configured = resolveAccountIds(configuredAccountIds);
|
||||
const runtime = resolveAccountIds(plugin.config.listAccountIds(cfg));
|
||||
return configured && runtime ? { configured, runtime } : undefined;
|
||||
} catch {
|
||||
// Keep doctor warnings conservative when a plugin cannot inspect its account set.
|
||||
return undefined;
|
||||
|
||||
@@ -9,17 +9,29 @@ vi.mock("../channel-capabilities.js", () => ({
|
||||
groupAllowFromFallbackToAllowFrom: channelName !== "imessage",
|
||||
warnOnEmptyGroupSenderAllowlist: channelName !== "discord",
|
||||
}),
|
||||
listDoctorChannelAccountIds: (
|
||||
resolveDoctorChannelAccountIds: (
|
||||
channelName: string,
|
||||
cfg: { channels?: Record<string, { accounts?: Record<string, unknown>; baseUrl?: string }> },
|
||||
cfg: {
|
||||
channels?: Record<
|
||||
string,
|
||||
{ accounts?: Record<string, unknown>; appId?: string; baseUrl?: string }
|
||||
>;
|
||||
},
|
||||
configuredAccountIds: string[],
|
||||
) => {
|
||||
const channel = cfg.channels?.[channelName];
|
||||
const ids = Object.keys(channel?.accounts ?? {});
|
||||
const runtimeIds =
|
||||
channelName === "matrix" ? ids.map((accountId) => accountId.toLowerCase()) : ids;
|
||||
return channelName === "qa-channel" && channel?.baseUrl
|
||||
? ["default", ...runtimeIds]
|
||||
: runtimeIds;
|
||||
const resolveAccountId = (accountId: string) =>
|
||||
channelName === "matrix" || channelName === "signal" ? accountId.toLowerCase() : accountId;
|
||||
const runtimeIds = [
|
||||
...(channelName === "qa-channel" && channel?.baseUrl ? ["default"] : []),
|
||||
...(channelName === "qqbot" && channel?.appId ? ["default"] : []),
|
||||
...ids,
|
||||
];
|
||||
return {
|
||||
configured: configuredAccountIds.map(resolveAccountId),
|
||||
runtime: runtimeIds.map(resolveAccountId),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -151,6 +163,28 @@ describe("doctor empty allowlist policy scan", () => {
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps parent warning for a distinct case-sensitive implicit default account", () => {
|
||||
const warnings = scanEmptyAllowlistPolicyWarnings(
|
||||
{
|
||||
channels: {
|
||||
qqbot: {
|
||||
appId: "top-level-app",
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: [],
|
||||
accounts: {
|
||||
Default: { groupAllowFrom: ["qqbot:group:named"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ doctorFixCommand: "openclaw doctor --fix" },
|
||||
);
|
||||
|
||||
expect(warnings).toContain(
|
||||
'- channels.qqbot.groupPolicy is "allowlist" but groupAllowFrom (and allowFrom) is empty — all group messages will be silently dropped. Add sender IDs to channels.qqbot.groupAllowFrom or channels.qqbot.allowFrom, or set groupPolicy to "open".',
|
||||
);
|
||||
});
|
||||
|
||||
it("allows provider-specific extra warnings without importing providers", () => {
|
||||
const warnings = scanEmptyAllowlistPolicyWarnings(
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Doctor scanner for empty allowlist policies across configured channels and accounts.
|
||||
import type { ChannelDoctorEmptyAllowlistAccountContext } from "../../../channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { normalizeAccountId } from "../../../routing/account-id.js";
|
||||
import {
|
||||
getDoctorChannelCapabilities,
|
||||
listDoctorChannelAccountIds,
|
||||
resolveDoctorChannelAccountIds,
|
||||
} from "../channel-capabilities.js";
|
||||
import type { DoctorAccountRecord, DoctorAllowFromList } from "../types.js";
|
||||
import { hasAllowFromEntries } from "./allowlist.js";
|
||||
@@ -102,13 +101,15 @@ export function scanEmptyAllowlistPolicyWarnings(
|
||||
Boolean(account && typeof account === "object" && !isDisabledRecord(account)),
|
||||
)
|
||||
: [];
|
||||
const configuredAccountIds = new Set(Object.keys(accounts ?? {}).map(normalizeAccountId));
|
||||
const runtimeAccountIds = listDoctorChannelAccountIds(channelName, cfg);
|
||||
const accountIds = resolveDoctorChannelAccountIds(
|
||||
channelName,
|
||||
cfg,
|
||||
Object.keys(accounts ?? {}),
|
||||
);
|
||||
const configuredAccountIds = new Set(accountIds?.configured);
|
||||
const hasImplicitActiveAccount =
|
||||
runtimeAccountIds === undefined ||
|
||||
runtimeAccountIds.some(
|
||||
(accountId) => !configuredAccountIds.has(normalizeAccountId(accountId)),
|
||||
);
|
||||
accountIds === undefined ||
|
||||
accountIds.runtime.some((accountId) => !configuredAccountIds.has(accountId));
|
||||
const suppressParentGroupAllowlistWarning =
|
||||
activeAccounts.length > 0 &&
|
||||
!hasImplicitActiveAccount &&
|
||||
|
||||
Reference in New Issue
Block a user