test: trim doctor command hotspots

This commit is contained in:
Peter Steinberger
2026-04-17 06:40:06 +01:00
parent 92859357bb
commit c66f16ac55
4 changed files with 144 additions and 212 deletions

View File

@@ -111,6 +111,133 @@ vi.mock("../channels/plugins/bootstrap-registry.js", () => ({
}),
}));
vi.mock("../channels/plugins/doctor-contract-api.js", () => ({
loadBundledChannelDoctorContractApi: vi.fn(() => undefined),
}));
vi.mock("../channels/plugins/setup-promotion-helpers.js", () => {
const commonSingleAccountKeys = new Set([
"name",
"token",
"tokenFile",
"botToken",
"appToken",
"account",
"signalNumber",
"authDir",
"cliPath",
"dbPath",
"httpUrl",
"httpHost",
"httpPort",
"webhookPath",
"webhookUrl",
"webhookSecret",
"service",
"region",
"homeserver",
"userId",
"accessToken",
"password",
"deviceName",
"url",
"code",
"dmPolicy",
"allowFrom",
"groupPolicy",
"groupAllowFrom",
"defaultTo",
]);
const fallbackSingleAccountKeys: Record<string, readonly string[]> = {
telegram: ["streaming"],
};
const namedAccountPromotionKeys: Record<string, readonly string[]> = {
telegram: ["botToken", "tokenFile"],
};
return {
resolveSingleAccountKeysToMove: ({
channelKey,
channel,
}: {
channelKey: string;
channel: Record<string, unknown>;
}) => {
const accounts =
channel.accounts && typeof channel.accounts === "object" && !Array.isArray(channel.accounts)
? (channel.accounts as Record<string, unknown>)
: {};
const hasNamedAccounts = Object.keys(accounts).filter(Boolean).length > 0;
const allowedNamedKeys = namedAccountPromotionKeys[channelKey];
return Object.entries(channel)
.filter(([key, value]) => {
if (key === "accounts" || key === "enabled" || value === undefined) {
return false;
}
const isKnownKey =
commonSingleAccountKeys.has(key) ||
(fallbackSingleAccountKeys[channelKey]?.includes(key) ?? false);
if (!isKnownKey) {
return false;
}
if (hasNamedAccounts && allowedNamedKeys && !allowedNamedKeys.includes(key)) {
return false;
}
return true;
})
.map(([key]) => key);
},
};
});
vi.mock("./doctor/shared/channel-legacy-config-migrate.js", () => ({
applyChannelDoctorCompatibilityMigrations: (cfg: Record<string, unknown>) => ({
next: cfg,
changes: [],
}),
}));
vi.mock("./doctor/channel-capabilities.js", () => {
const byChannel = {
googlechat: {
dmAllowFromMode: "nestedOnly",
groupModel: "route",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: false,
},
matrix: {
dmAllowFromMode: "nestedOnly",
groupModel: "sender",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: true,
},
msteams: {
dmAllowFromMode: "topOnly",
groupModel: "hybrid",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: true,
},
zalouser: {
dmAllowFromMode: "topOnly",
groupModel: "hybrid",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: false,
},
} as const;
const fallback = {
dmAllowFromMode: "topOnly",
groupModel: "sender",
groupAllowFromFallbackToAllowFrom: true,
warnOnEmptyGroupSenderAllowlist: true,
};
return {
getDoctorChannelCapabilities: (channelName?: string) =>
channelName && channelName in byChannel
? byChannel[channelName as keyof typeof byChannel]
: fallback,
};
});
vi.mock("../plugins/doctor-contract-registry.js", () => {
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)

View File

@@ -12,16 +12,6 @@ vi.mock("../plugins/setup-registry.js", () => ({
}),
}));
function asLegacyConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
function getLegacyProperty(value: unknown, key: string): unknown {
if (!value || typeof value !== "object") {
return undefined;
}
return (value as Record<string, unknown>)[key];
}
describe("normalizeCompatibilityConfigValues", () => {
let previousOauthDir: string | undefined;
let tempOauthDir = "";
@@ -90,201 +80,6 @@ describe("normalizeCompatibilityConfigValues", () => {
});
});
it("migrates Slack dm.policy/dm.allowFrom to dmPolicy/allowFrom aliases", () => {
const res = normalizeCompatibilityConfigValues({
channels: {
slack: {
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
},
},
});
expect(res.config.channels?.slack?.dmPolicy).toBe("open");
expect(res.config.channels?.slack?.allowFrom).toEqual(["*"]);
expect(res.config.channels?.slack?.dm).toEqual({
enabled: true,
});
expect(res.changes).toEqual([
"Moved channels.slack.dm.policy → channels.slack.dmPolicy.",
"Moved channels.slack.dm.allowFrom → channels.slack.allowFrom.",
]);
});
it("migrates Discord account dm.policy/dm.allowFrom to dmPolicy/allowFrom aliases", () => {
const res = normalizeCompatibilityConfigValues({
channels: {
discord: {
accounts: {
work: {
dm: { policy: "allowlist", allowFrom: ["123"], groupEnabled: true },
},
},
},
},
});
expect(res.config.channels?.discord?.accounts?.work?.dmPolicy).toBe("allowlist");
expect(res.config.channels?.discord?.accounts?.work?.allowFrom).toEqual(["123"]);
expect(res.config.channels?.discord?.accounts?.work?.dm).toEqual({
groupEnabled: true,
});
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.dm.policy → channels.discord.accounts.work.dmPolicy.",
"Moved channels.discord.accounts.work.dm.allowFrom → channels.discord.accounts.work.allowFrom.",
]);
});
it("migrates Discord streaming boolean alias into nested streaming.mode", () => {
const res = normalizeCompatibilityConfigValues(
asLegacyConfig({
channels: {
discord: {
streaming: true,
accounts: {
work: {
streaming: false,
},
},
},
},
}),
);
expect(res.config.channels?.discord?.streaming).toEqual({ mode: "partial" });
expect(getLegacyProperty(res.config.channels?.discord, "streamMode")).toBeUndefined();
expect(res.config.channels?.discord?.accounts?.work?.streaming).toEqual({ mode: "off" });
expect(
getLegacyProperty(res.config.channels?.discord?.accounts?.work, "streamMode"),
).toBeUndefined();
expect(res.changes).toEqual([
"Moved channels.discord.streaming (boolean) → channels.discord.streaming.mode (partial).",
"Moved channels.discord.accounts.work.streaming (boolean) → channels.discord.accounts.work.streaming.mode (off).",
]);
});
it("migrates Discord legacy streamMode into nested streaming.mode", () => {
const res = normalizeCompatibilityConfigValues(
asLegacyConfig({
channels: {
discord: {
streaming: false,
streamMode: "block",
},
},
}),
);
expect(res.config.channels?.discord?.streaming).toEqual({ mode: "block" });
expect(getLegacyProperty(res.config.channels?.discord, "streamMode")).toBeUndefined();
expect(res.changes).toEqual([
"Moved channels.discord.streamMode → channels.discord.streaming.mode (block).",
]);
});
it("migrates Telegram streamMode into nested streaming.mode", () => {
const res = normalizeCompatibilityConfigValues(
asLegacyConfig({
channels: {
telegram: {
streamMode: "block",
},
},
}),
);
expect(res.config.channels?.telegram?.streaming).toEqual({ mode: "block" });
expect(getLegacyProperty(res.config.channels?.telegram, "streamMode")).toBeUndefined();
expect(res.changes).toEqual([
"Moved channels.telegram.streamMode → channels.telegram.streaming.mode (block).",
]);
});
it("migrates Slack legacy streaming keys into nested streaming config", () => {
const res = normalizeCompatibilityConfigValues(
asLegacyConfig({
channels: {
slack: {
streaming: false,
streamMode: "status_final",
},
},
}),
);
expect(res.config.channels?.slack?.streaming).toEqual({
mode: "progress",
nativeTransport: false,
});
expect(getLegacyProperty(res.config.channels?.slack, "streamMode")).toBeUndefined();
expect(res.changes).toEqual([
"Moved channels.slack.streamMode → channels.slack.streaming.mode (progress).",
"Moved channels.slack.streaming (boolean) → channels.slack.streaming.nativeTransport.",
]);
});
it("preserves top-level Telegram allowlist fallback for existing named accounts", () => {
const res = normalizeCompatibilityConfigValues({
channels: {
telegram: {
enabled: true,
dmPolicy: "allowlist",
allowFrom: ["123"],
groupPolicy: "allowlist",
accounts: {
bot1: {
enabled: true,
botToken: "bot-1-token",
},
bot2: {
enabled: true,
botToken: "bot-2-token",
},
},
},
},
});
expect(res.config.channels?.telegram?.dmPolicy).toBe("allowlist");
expect(res.config.channels?.telegram?.allowFrom).toEqual(["123"]);
expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist");
expect(res.config.channels?.telegram?.accounts?.bot1?.botToken).toBe("bot-1-token");
expect(res.config.channels?.telegram?.accounts?.bot2?.botToken).toBe("bot-2-token");
expect(res.changes).not.toContain(
"Moved channels.telegram single-account top-level values into channels.telegram.accounts.default.",
);
});
it("keeps Telegram policy fallback top-level while still seeding default auth", () => {
const res = normalizeCompatibilityConfigValues({
channels: {
telegram: {
enabled: true,
botToken: "legacy-token",
dmPolicy: "allowlist",
allowFrom: ["123"],
groupPolicy: "allowlist",
accounts: {
bot1: {
enabled: true,
botToken: "bot-1-token",
},
},
},
},
});
expect(res.config.channels?.telegram?.accounts?.default).toMatchObject({
botToken: "legacy-token",
});
expect(res.config.channels?.telegram?.botToken).toBeUndefined();
expect(res.config.channels?.telegram?.dmPolicy).toBe("allowlist");
expect(res.config.channels?.telegram?.allowFrom).toEqual(["123"]);
expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist");
expect(res.changes).toContain(
"Moved channels.telegram single-account top-level values into channels.telegram.accounts.default.",
);
});
it("migrates browser ssrfPolicy allowPrivateNetwork to dangerouslyAllowPrivateNetwork", () => {
const res = normalizeCompatibilityConfigValues({
browser: {

View File

@@ -364,10 +364,22 @@ vi.mock("./doctor-memory-search.js", () => ({
}));
vi.mock("../plugins/doctor-contract-registry.js", () => ({
applyPluginDoctorCompatibilityMigrations: (config: unknown) => ({
config,
changes: [],
}),
collectRelevantDoctorPluginIds,
listPluginDoctorLegacyConfigRules,
}));
vi.mock("../channels/plugins/doctor-contract-api.js", () => ({
loadBundledChannelDoctorContractApi: vi.fn(() => undefined),
}));
vi.mock("../channels/plugins/bootstrap-registry.js", () => ({
getBootstrapChannelPlugin: vi.fn(() => undefined),
}));
vi.mock("./doctor-bundled-plugin-runtime-deps.js", () => ({
maybeRepairBundledPluginRuntimeDeps: vi.fn(async () => {}),
}));

View File

@@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { runPluginSetupConfigMigrations } from "../../../plugins/setup-registry.js";
import { collectChannelDoctorCompatibilityMutations } from "./channel-doctor.js";
import { applyChannelDoctorCompatibilityMigrations } from "./channel-legacy-config-migrate.js";
import {
normalizeLegacyBrowserConfig,
normalizeLegacyCrossContextMessageConfig,
@@ -48,12 +48,10 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
next = normalizeLegacyCrossContextMessageConfig(next, changes);
next = normalizeLegacyMediaProviderOptions(next, changes);
next = normalizeLegacyMistralModelMaxTokens(next, changes);
for (const mutation of collectChannelDoctorCompatibilityMutations(next)) {
if (mutation.changes.length === 0) {
continue;
}
next = mutation.config;
changes.push(...mutation.changes);
const channelMigrations = applyChannelDoctorCompatibilityMigrations(next);
if (channelMigrations.changes.length > 0) {
next = channelMigrations.next;
changes.push(...channelMigrations.changes);
}
return { config: next, changes };