mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(discord): avoid doctor runtime loading (#119391)
* fix(discord): publish doctor capability metadata Punchcard-Session: amber-workshop-river-yr * fix(doctor): read packaged channel metadata Punchcard-Session: amber-workshop-river-yr
This commit is contained in:
@@ -78,6 +78,12 @@
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": true,
|
||||
"nativeSkillsAutoEnabled": true
|
||||
},
|
||||
"doctorCapabilities": {
|
||||
"dmAllowFromMode": "topOnly",
|
||||
"groupModel": "route",
|
||||
"groupAllowFromFallbackToAllowFrom": false,
|
||||
"warnOnEmptyGroupSenderAllowlist": false
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
|
||||
@@ -571,6 +571,12 @@
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": true,
|
||||
"nativeSkillsAutoEnabled": true
|
||||
},
|
||||
"doctorCapabilities": {
|
||||
"dmAllowFromMode": "topOnly",
|
||||
"groupModel": "route",
|
||||
"groupAllowFromFallbackToAllowFrom": false,
|
||||
"warnOnEmptyGroupSenderAllowlist": false
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
|
||||
@@ -39,7 +39,10 @@ vi.mock("../infra/openclaw-root.js", () => ({
|
||||
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
import { listBundledChannelCatalogEntries } from "./bundled-channel-catalog-read.js";
|
||||
import {
|
||||
findBundledChannelCatalogMetadata,
|
||||
listBundledChannelCatalogEntries,
|
||||
} from "./bundled-channel-catalog-read.js";
|
||||
import { listBundledChannelIds } from "./plugins/bundled-ids.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -126,6 +129,12 @@ function seedGeneratedChannelCatalog(
|
||||
label: string;
|
||||
docsPath: string;
|
||||
blurb: string;
|
||||
doctorCapabilities?: {
|
||||
dmAllowFromMode?: "topOnly" | "nestedOnly";
|
||||
groupModel?: "sender" | "route" | "hybrid";
|
||||
groupAllowFromFallbackToAllowFrom?: boolean;
|
||||
warnOnEmptyGroupSenderAllowlist?: boolean;
|
||||
};
|
||||
},
|
||||
): void {
|
||||
const { packageName, ...channel } = params;
|
||||
@@ -222,6 +231,31 @@ describe("listBundledChannelCatalogEntries", () => {
|
||||
expect(ids.has("telegram")).toBe(true);
|
||||
});
|
||||
|
||||
it("finds doctor capabilities from the generated catalog when the package is excluded", () => {
|
||||
const root = seedRoot("bcr-generated-doctor-");
|
||||
useBundledPluginsDir(undefined);
|
||||
seedGeneratedChannelCatalog(root, {
|
||||
packageName: "@openclaw/discord",
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
docsPath: "/channels/discord",
|
||||
blurb: "downloadable channel",
|
||||
doctorCapabilities: {
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findBundledChannelCatalogMetadata("Discord")?.doctorCapabilities).toEqual({
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled package metadata when generated catalog entries are stale", () => {
|
||||
const root = seedRoot("bcr-package-wins-");
|
||||
const extensionsRoot = path.join(root, "dist", "extensions");
|
||||
|
||||
@@ -157,3 +157,16 @@ export function listBundledChannelCatalogEntries(): BundledChannelCatalogEntry[]
|
||||
(left, right) => left.order - right.order || left.id.localeCompare(right.id),
|
||||
);
|
||||
}
|
||||
|
||||
/** Finds bundled or generated channel metadata by id or alias. */
|
||||
export function findBundledChannelCatalogMetadata(
|
||||
channelId: string,
|
||||
): PluginPackageChannel | undefined {
|
||||
const normalized = normalizeOptionalLowercaseString(channelId);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return listBundledChannelCatalogEntries().find(
|
||||
(entry) => entry.id === normalized || entry.aliases.includes(normalized),
|
||||
)?.channel;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Packaged doctor capability tests cover generated catalog lookup without plugin runtime loading.
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
cleanupTempDirs,
|
||||
makeTempRepoRoot,
|
||||
writeJsonFile,
|
||||
} from "../../../test/helpers/temp-repo.js";
|
||||
|
||||
const packageRootMock = vi.hoisted(() => ({ value: "" }));
|
||||
const channelPluginMocks = vi.hoisted(() => ({
|
||||
getBundledChannelPlugin: vi.fn(() => undefined),
|
||||
getChannelPlugin: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/bundled.js", () => ({
|
||||
getBundledChannelPlugin: channelPluginMocks.getBundledChannelPlugin,
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: channelPluginMocks.getChannelPlugin,
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/bundled-dir.js", () => ({
|
||||
resolveBundledPluginsDir: () => undefined,
|
||||
resolveSourceCheckoutDependencyDiagnostic: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/openclaw-root.js", () => ({
|
||||
resolveOpenClawPackageRootSync: () => packageRootMock.value,
|
||||
resolveOpenClawPackageRoot: async () => packageRootMock.value,
|
||||
}));
|
||||
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import { getDoctorChannelCapabilities } from "./channel-capabilities.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
const root = makeTempRepoRoot(tempDirs, "doctor-channel-packaged-");
|
||||
packageRootMock.value = root;
|
||||
writeJsonFile(path.join(root, "package.json"), { name: "openclaw" });
|
||||
writeJsonFile(path.join(root, "dist", "channel-catalog.json"), {
|
||||
entries: [
|
||||
{
|
||||
name: "@openclaw/discord",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
doctorCapabilities: {
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
channelPluginMocks.getBundledChannelPlugin.mockReset().mockReturnValue(undefined);
|
||||
channelPluginMocks.getChannelPlugin.mockReset().mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
packageRootMock.value = "";
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
cleanupTempDirs(tempDirs);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("doctor channel capabilities in a packaged install", () => {
|
||||
it("reads Discord semantics from the generated catalog without loading a plugin", () => {
|
||||
expect(getDoctorChannelCapabilities("discord")).toEqual({
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
});
|
||||
expect(channelPluginMocks.getChannelPlugin).not.toHaveBeenCalled();
|
||||
expect(channelPluginMocks.getBundledChannelPlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,17 @@ describe("doctor channel capabilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns Discord route semantics without loading its channel plugin", () => {
|
||||
expect(getDoctorChannelCapabilities("discord")).toEqual({
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
});
|
||||
expect(channelPluginMocks.getChannelPlugin).not.toHaveBeenCalled();
|
||||
expect(channelPluginMocks.getBundledChannelPlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns capability overrides from matrix plugin metadata", () => {
|
||||
expect(getDoctorChannelCapabilities("matrix")).toEqual({
|
||||
dmAllowFromMode: "nestedOnly",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { findBundledChannelCatalogMetadata } from "../../channels/bundled-channel-catalog-read.js";
|
||||
// Doctor capability lookup for channel-specific policy and migration behavior.
|
||||
import { getBundledChannelPlugin } from "../../channels/plugins/bundled.js";
|
||||
import type { ChannelDmAllowFromMode } from "../../channels/plugins/dm-access.js";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { normalizeAnyChannelId } from "../../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { findBundledPackageChannelMetadata } from "../../plugins/bundled-package-channel-metadata.js";
|
||||
import type { PluginPackageChannelDoctorCapabilities } from "../../plugins/manifest.js";
|
||||
|
||||
type DoctorGroupModel = "sender" | "route" | "hybrid";
|
||||
@@ -39,10 +39,10 @@ function mergeDoctorChannelCapabilities(
|
||||
};
|
||||
}
|
||||
|
||||
function getManifestDoctorCapabilities(
|
||||
function getCatalogDoctorCapabilities(
|
||||
channelId: string,
|
||||
): PluginPackageChannelDoctorCapabilities | undefined {
|
||||
return findBundledPackageChannelMetadata(channelId)?.doctorCapabilities;
|
||||
return findBundledChannelCatalogMetadata(channelId)?.doctorCapabilities;
|
||||
}
|
||||
|
||||
/** Resolve doctor behavior capabilities from channel metadata, plugin runtime, or defaults. */
|
||||
@@ -51,9 +51,9 @@ export function getDoctorChannelCapabilities(channelName?: string): DoctorChanne
|
||||
return DEFAULT_DOCTOR_CHANNEL_CAPABILITIES;
|
||||
}
|
||||
|
||||
const manifestCapabilities = getManifestDoctorCapabilities(channelName);
|
||||
if (manifestCapabilities) {
|
||||
return mergeDoctorChannelCapabilities(manifestCapabilities);
|
||||
const catalogCapabilities = getCatalogDoctorCapabilities(channelName);
|
||||
if (catalogCapabilities) {
|
||||
return mergeDoctorChannelCapabilities(catalogCapabilities);
|
||||
}
|
||||
|
||||
const channelId = normalizeAnyChannelId(channelName);
|
||||
@@ -65,7 +65,7 @@ export function getDoctorChannelCapabilities(channelName?: string): DoctorChanne
|
||||
if (pluginDoctor) {
|
||||
return mergeDoctorChannelCapabilities(pluginDoctor);
|
||||
}
|
||||
return mergeDoctorChannelCapabilities(getManifestDoctorCapabilities(channelId));
|
||||
return mergeDoctorChannelCapabilities(getCatalogDoctorCapabilities(channelId));
|
||||
}
|
||||
|
||||
type DoctorChannelAccountIds = {
|
||||
|
||||
@@ -10,7 +10,7 @@ vi.mock("./bundled-dir.js", () => ({
|
||||
}));
|
||||
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { findBundledPackageChannelMetadata } from "./bundled-package-channel-metadata.js";
|
||||
import { listBundledPackageChannelMetadata } from "./bundled-package-channel-metadata.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -72,7 +72,7 @@ describe("bundled package channel metadata", () => {
|
||||
);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
const matrix = findBundledPackageChannelMetadata("matrix");
|
||||
const matrix = listBundledPackageChannelMetadata().find((channel) => channel.id === "matrix");
|
||||
|
||||
expect(matrix?.doctorCapabilities).toEqual({
|
||||
dmAllowFromMode: "nestedOnly",
|
||||
@@ -107,7 +107,9 @@ describe("bundled package channel metadata", () => {
|
||||
"export default {};\n",
|
||||
"utf8",
|
||||
);
|
||||
expect(findBundledPackageChannelMetadata("matrix")?.label).toBe("Before");
|
||||
expect(
|
||||
listBundledPackageChannelMetadata().find((channel) => channel.id === "matrix")?.label,
|
||||
).toBe("Before");
|
||||
|
||||
writeJsonFile(packagePath, {
|
||||
name: "@openclaw/matrix",
|
||||
@@ -120,6 +122,8 @@ describe("bundled package channel metadata", () => {
|
||||
});
|
||||
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
expect(findBundledPackageChannelMetadata("matrix")?.label).toBe("After");
|
||||
expect(
|
||||
listBundledPackageChannelMetadata().find((channel) => channel.id === "matrix")?.label,
|
||||
).toBe("After");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,3 @@ import type { PluginPackageChannel } from "./manifest.js";
|
||||
export function listBundledPackageChannelMetadata(): readonly PluginPackageChannel[] {
|
||||
return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel);
|
||||
}
|
||||
|
||||
/** Finds bundled package channel metadata by id or alias. */
|
||||
export function findBundledPackageChannelMetadata(
|
||||
channelId: string,
|
||||
): PluginPackageChannel | undefined {
|
||||
return listBundledPackageChannelMetadata().find(
|
||||
(channel) => channel.id === channelId || channel.aliases?.includes(channelId),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user