mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
perf(doctor): fast-path bundled channel compat migrations
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import type { LegacyConfigRule } from "../../config/legacy.shared.js";
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "../../plugins/public-surface-loader.js";
|
||||
|
||||
type BundledChannelDoctorCompatibilityMutation = {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
};
|
||||
|
||||
type BundledChannelDoctorContractApi = {
|
||||
legacyConfigRules?: readonly LegacyConfigRule[];
|
||||
normalizeCompatibilityConfig?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
}) => BundledChannelDoctorCompatibilityMutation;
|
||||
};
|
||||
|
||||
function loadBundledChannelPublicArtifact(
|
||||
|
||||
@@ -60,7 +60,55 @@ vi.mock("../config/validation.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/bootstrap-registry.js", () => ({
|
||||
getBootstrapChannelPlugin: vi.fn(() => undefined),
|
||||
getBootstrapChannelPlugin: vi.fn((channelId: string) => {
|
||||
if (channelId !== "discord") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
doctor: {
|
||||
normalizeCompatibilityConfig: ({
|
||||
cfg,
|
||||
}: {
|
||||
cfg: { channels?: { discord?: Record<string, unknown> } };
|
||||
}) => {
|
||||
const discord = cfg.channels?.discord;
|
||||
if (!discord) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
if (
|
||||
!("streamMode" in discord) &&
|
||||
typeof discord.streaming !== "boolean" &&
|
||||
typeof discord.streaming !== "string"
|
||||
) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
const next = structuredClone(cfg);
|
||||
const nextDiscord = next.channels?.discord;
|
||||
if (!nextDiscord) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
const nextStreaming =
|
||||
nextDiscord.streaming && typeof nextDiscord.streaming === "object"
|
||||
? { ...(nextDiscord.streaming as Record<string, unknown>) }
|
||||
: {};
|
||||
if (!("mode" in nextStreaming)) {
|
||||
nextStreaming.mode =
|
||||
nextDiscord.streamMode === "block"
|
||||
? "partial"
|
||||
: nextDiscord.streaming === false
|
||||
? "off"
|
||||
: "partial";
|
||||
}
|
||||
delete nextDiscord.streamMode;
|
||||
nextDiscord.streaming = nextStreaming;
|
||||
return {
|
||||
config: next,
|
||||
changes: ["Discord allowlist ids normalized to strings."],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/doctor-contract-registry.js", () => {
|
||||
@@ -535,6 +583,43 @@ vi.mock("./doctor-config-preflight.js", async () => {
|
||||
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir, "openclaw.json");
|
||||
}
|
||||
|
||||
function normalizeDiscordStreamingCompat(cfg: Record<string, unknown>): Record<string, unknown> {
|
||||
const channels =
|
||||
cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
|
||||
? (cfg.channels as Record<string, unknown>)
|
||||
: null;
|
||||
const discord =
|
||||
channels?.discord && typeof channels.discord === "object" && !Array.isArray(channels.discord)
|
||||
? (channels.discord as Record<string, unknown>)
|
||||
: null;
|
||||
if (
|
||||
!discord ||
|
||||
(!("streamMode" in discord) &&
|
||||
typeof discord.streaming !== "boolean" &&
|
||||
typeof discord.streaming !== "string")
|
||||
) {
|
||||
return cfg;
|
||||
}
|
||||
const next = structuredClone(cfg);
|
||||
const nextDiscord = ((next.channels as Record<string, unknown> | undefined)?.discord ??
|
||||
{}) as Record<string, unknown>;
|
||||
const nextStreaming =
|
||||
nextDiscord.streaming && typeof nextDiscord.streaming === "object"
|
||||
? { ...(nextDiscord.streaming as Record<string, unknown>) }
|
||||
: {};
|
||||
if (!("mode" in nextStreaming)) {
|
||||
nextStreaming.mode =
|
||||
nextDiscord.streamMode === "block"
|
||||
? "partial"
|
||||
: nextDiscord.streaming === false
|
||||
? "off"
|
||||
: "partial";
|
||||
}
|
||||
delete nextDiscord.streamMode;
|
||||
nextDiscord.streaming = nextStreaming;
|
||||
return next;
|
||||
}
|
||||
|
||||
return {
|
||||
runDoctorConfigPreflight: vi.fn(async () => {
|
||||
const injected = getDoctorConfigInputForTest();
|
||||
@@ -596,7 +681,7 @@ vi.mock("./doctor-config-preflight.js", async () => {
|
||||
}),
|
||||
);
|
||||
const compat = applyRuntimeLegacyConfigMigrations(parsed);
|
||||
const effectiveConfig = compat.next ?? parsed;
|
||||
const effectiveConfig = normalizeDiscordStreamingCompat(compat.next ?? parsed);
|
||||
return {
|
||||
snapshot: {
|
||||
exists,
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const applyPluginDoctorCompatibilityMigrations = vi.hoisted(() => vi.fn());
|
||||
const loadBundledChannelDoctorContractApi = vi.hoisted(() => vi.fn());
|
||||
const getBootstrapChannelPlugin = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../../plugins/doctor-contract-registry.js", () => ({
|
||||
applyPluginDoctorCompatibilityMigrations: (...args: unknown[]) =>
|
||||
applyPluginDoctorCompatibilityMigrations(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../../channels/plugins/doctor-contract-api.js", () => ({
|
||||
loadBundledChannelDoctorContractApi: (...args: unknown[]) =>
|
||||
loadBundledChannelDoctorContractApi(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../../channels/plugins/bootstrap-registry.js", () => ({
|
||||
getBootstrapChannelPlugin: (...args: unknown[]) => getBootstrapChannelPlugin(...args),
|
||||
}));
|
||||
|
||||
let applyChannelDoctorCompatibilityMigrations: typeof import("./channel-legacy-config-migrate.js").applyChannelDoctorCompatibilityMigrations;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -17,8 +28,59 @@ beforeAll(async () => {
|
||||
await import("./channel-legacy-config-migrate.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
applyPluginDoctorCompatibilityMigrations.mockReset();
|
||||
loadBundledChannelDoctorContractApi.mockReset();
|
||||
getBootstrapChannelPlugin.mockReset();
|
||||
});
|
||||
|
||||
describe("bundled channel legacy config migrations", () => {
|
||||
it("prefers bundled channel doctor contract normalizers before plugin registry fallback", () => {
|
||||
loadBundledChannelDoctorContractApi.mockImplementation((channelId: string) =>
|
||||
channelId === "slack"
|
||||
? {
|
||||
normalizeCompatibilityConfig: ({
|
||||
cfg,
|
||||
}: {
|
||||
cfg: { channels?: { slack?: Record<string, unknown> } };
|
||||
}) => ({
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
normalizedByBundledContract: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
changes: ["Normalized channels.slack via bundled doctor contract."],
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
getBootstrapChannelPlugin.mockReturnValue(undefined);
|
||||
|
||||
const result = applyChannelDoctorCompatibilityMigrations({
|
||||
channels: {
|
||||
slack: {
|
||||
streaming: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(applyPluginDoctorCompatibilityMigrations).not.toHaveBeenCalled();
|
||||
expect(loadBundledChannelDoctorContractApi).toHaveBeenCalledWith("slack");
|
||||
expect(result.next.channels?.slack).toMatchObject({
|
||||
streaming: true,
|
||||
normalizedByBundledContract: true,
|
||||
});
|
||||
expect(result.changes).toEqual(["Normalized channels.slack via bundled doctor contract."]);
|
||||
});
|
||||
|
||||
it("normalizes legacy private-network aliases exposed through bundled contract surfaces", () => {
|
||||
loadBundledChannelDoctorContractApi.mockReturnValue(undefined);
|
||||
getBootstrapChannelPlugin.mockReturnValue(undefined);
|
||||
applyPluginDoctorCompatibilityMigrations.mockReturnValueOnce({
|
||||
config: {
|
||||
channels: {
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { getBootstrapChannelPlugin } from "../../../channels/plugins/bootstrap-registry.js";
|
||||
import { loadBundledChannelDoctorContractApi } from "../../../channels/plugins/doctor-contract-api.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.js";
|
||||
import { applyPluginDoctorCompatibilityMigrations } from "../../../plugins/doctor-contract-registry.js";
|
||||
import { isRecord } from "./legacy-config-record-shared.js";
|
||||
|
||||
type ChannelDoctorCompatibilityMutation = {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
};
|
||||
|
||||
type ChannelDoctorCompatibilityNormalizer = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
}) => ChannelDoctorCompatibilityMutation;
|
||||
|
||||
function collectRelevantDoctorChannelIds(raw: unknown): string[] {
|
||||
const channels = isRecord(raw) && isRecord(raw.channels) ? raw.channels : null;
|
||||
if (!channels) {
|
||||
@@ -12,15 +23,49 @@ function collectRelevantDoctorChannelIds(raw: unknown): string[] {
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
function resolveBundledChannelCompatibilityNormalizer(
|
||||
channelId: string,
|
||||
): ChannelDoctorCompatibilityNormalizer | undefined {
|
||||
const contractNormalizer =
|
||||
loadBundledChannelDoctorContractApi(channelId)?.normalizeCompatibilityConfig;
|
||||
if (typeof contractNormalizer === "function") {
|
||||
return contractNormalizer;
|
||||
}
|
||||
return getBootstrapChannelPlugin(channelId)?.doctor?.normalizeCompatibilityConfig;
|
||||
}
|
||||
|
||||
export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, unknown>): {
|
||||
next: Record<string, unknown>;
|
||||
changes: string[];
|
||||
} {
|
||||
const compat = applyPluginDoctorCompatibilityMigrations(cfg as OpenClawConfig, {
|
||||
pluginIds: collectRelevantDoctorChannelIds(cfg),
|
||||
});
|
||||
let nextCfg = cfg as OpenClawConfig;
|
||||
const changes: string[] = [];
|
||||
const unresolvedChannelIds: string[] = [];
|
||||
|
||||
for (const channelId of collectRelevantDoctorChannelIds(cfg)) {
|
||||
const normalizeCompatibilityConfig = resolveBundledChannelCompatibilityNormalizer(channelId);
|
||||
if (!normalizeCompatibilityConfig) {
|
||||
unresolvedChannelIds.push(channelId);
|
||||
continue;
|
||||
}
|
||||
const mutation = normalizeCompatibilityConfig({ cfg: nextCfg });
|
||||
if (!mutation || mutation.changes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
nextCfg = mutation.config;
|
||||
changes.push(...mutation.changes);
|
||||
}
|
||||
|
||||
if (unresolvedChannelIds.length > 0) {
|
||||
const compat = applyPluginDoctorCompatibilityMigrations(nextCfg, {
|
||||
pluginIds: unresolvedChannelIds,
|
||||
});
|
||||
nextCfg = compat.config;
|
||||
changes.push(...compat.changes);
|
||||
}
|
||||
|
||||
return {
|
||||
next: compat.config as OpenClawConfig & Record<string, unknown>,
|
||||
changes: compat.changes,
|
||||
next: nextCfg as OpenClawConfig & Record<string, unknown>,
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user