






















@@ -9,6 +9,12 @@ import {
99resetPluginLoaderTestStateForTest,
1010useNoBundledPlugins,
1111} from "../../plugins/loader.test-fixtures.js";
12+import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
13+import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
14+import {
15+createChannelTestPluginBase,
16+createTestRegistry,
17+} from "../../test-utils/channel-plugins.js";
1218import {
1319listPluginLoaderModuleCandidateUrls,
1420listReadOnlyChannelPluginsForConfig,
@@ -468,8 +474,10 @@ function expectExternalChatSetupOnlyPluginLoaded(params: {
468474}
469475470476afterEach(() => {
477+vi.unstubAllEnvs();
471478moduleLoaderParams.length = 0;
472479resetPluginLoaderTestStateForTest();
480+resetPluginRuntimeStateForTest();
473481});
474482475483afterAll(() => {
@@ -544,6 +552,161 @@ describe("listReadOnlyChannelPluginsForConfig", () => {
544552).toBe(true);
545553});
546554555+it("reuses default read-only channel plugin resolution for the same config", () => {
556+const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin();
557+const cfg = {
558+channels: {
559+"external-chat": { token: "configured" },
560+},
561+plugins: {
562+load: { paths: [pluginDir] },
563+allow: ["external-chat"],
564+},
565+} as never;
566+567+const first = listReadOnlyChannelPluginsForConfig(cfg, {
568+includePersistedAuthState: false,
569+includeSetupFallbackPlugins: true,
570+});
571+const loaderCallCount = moduleLoaderParams.length;
572+expect(fs.existsSync(setupMarker)).toBe(true);
573+fs.rmSync(setupMarker, { force: true });
574+575+const second = listReadOnlyChannelPluginsForConfig(cfg, {
576+includePersistedAuthState: false,
577+includeSetupFallbackPlugins: true,
578+});
579+580+expect(pluginIds(first)).toContain("external-chat");
581+expect(pluginIds(second)).toContain("external-chat");
582+expect(moduleLoaderParams).toHaveLength(loaderCallCount);
583+expect(fs.existsSync(setupMarker)).toBe(false);
584+expect(fs.existsSync(fullMarker)).toBe(false);
585+});
586+587+it("refreshes cached read-only channel plugins when the active channel registry changes", () => {
588+const cfg = { channels: { "external-chat": { token: "configured" } } } as never;
589+const createRegistryPlugin = (blurb: string) => {
590+const base = createChannelTestPluginBase({ id: "external-chat" as never });
591+return {
592+ ...base,
593+meta: {
594+ ...base.meta,
595+ blurb,
596+},
597+};
598+};
599+600+setActivePluginRegistry(
601+createTestRegistry([
602+{ pluginId: "first-plugin", plugin: createRegistryPlugin("first"), source: "test" },
603+]),
604+);
605+const first = listReadOnlyChannelPluginsForConfig(cfg, {
606+includeSetupFallbackPlugins: true,
607+});
608+609+setActivePluginRegistry(
610+createTestRegistry([
611+{ pluginId: "second-plugin", plugin: createRegistryPlugin("second"), source: "test" },
612+]),
613+);
614+const second = listReadOnlyChannelPluginsForConfig(cfg, {
615+includeSetupFallbackPlugins: true,
616+});
617+618+expect(first.find((plugin) => plugin.id === "external-chat")?.meta.blurb).toBe("first");
619+expect(second.find((plugin) => plugin.id === "external-chat")?.meta.blurb).toBe("second");
620+});
621+622+it("refreshes cached read-only channel plugins when active registry channels mutate in place", () => {
623+const cfg = { channels: { "external-chat": { token: "configured" } } } as never;
624+const registry = createTestRegistry([]);
625+setActivePluginRegistry(registry);
626+627+const first = listReadOnlyChannelPluginsForConfig(cfg, {
628+includePersistedAuthState: false,
629+includeSetupFallbackPlugins: true,
630+});
631+632+const plugin = {
633+ ...createChannelTestPluginBase({ id: "external-chat" as never }),
634+meta: {
635+ ...createChannelTestPluginBase({ id: "external-chat" as never }).meta,
636+blurb: "mutated registry",
637+},
638+};
639+registry.channels.push({ pluginId: "mutated-plugin", plugin, source: "test" } as never);
640+const second = listReadOnlyChannelPluginsForConfig(cfg, {
641+includePersistedAuthState: false,
642+includeSetupFallbackPlugins: true,
643+});
644+645+expect(pluginIds(first)).not.toContain("external-chat");
646+expect(second.find((entry) => entry.id === "external-chat")?.meta.blurb).toBe(
647+"mutated registry",
648+);
649+});
650+651+it("refreshes cached read-only channel plugins when ambient env changes", () => {
652+const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({
653+manifestChannelConfig: true,
654+});
655+const cfg = {
656+plugins: {
657+load: { paths: [pluginDir] },
658+allow: ["external-chat"],
659+},
660+} as never;
661+662+const first = listReadOnlyChannelPluginsForConfig(cfg, {
663+includePersistedAuthState: false,
664+includeSetupFallbackPlugins: true,
665+});
666+667+vi.stubEnv("EXTERNAL_CHAT_TOKEN", "token-from-env");
668+const second = listReadOnlyChannelPluginsForConfig(cfg, {
669+includePersistedAuthState: false,
670+includeSetupFallbackPlugins: true,
671+});
672+673+expect(pluginIds(first)).not.toContain("external-chat");
674+expectExternalChatSetupOnlyPluginLoaded({ plugins: second, setupMarker, fullMarker });
675+});
676+677+it("clears cached read-only channel plugin resolution with plugin metadata lifecycle caches", () => {
678+const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin();
679+const cfg = {
680+channels: {
681+"external-chat": { token: "configured" },
682+},
683+plugins: {
684+load: { paths: [pluginDir] },
685+allow: ["external-chat"],
686+},
687+} as never;
688+689+const first = listReadOnlyChannelPluginsForConfig(cfg, {
690+includePersistedAuthState: false,
691+includeSetupFallbackPlugins: true,
692+});
693+expect(pluginIds(first)).toContain("external-chat");
694+expect(fs.existsSync(setupMarker)).toBe(true);
695+const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
696+const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
697+manifest.channels = ["other-chat"];
698+fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
699+700+clearPluginMetadataLifecycleCaches();
701+const second = listReadOnlyChannelPluginsForConfig(cfg, {
702+includePersistedAuthState: false,
703+includeSetupFallbackPlugins: true,
704+});
705+706+expect(pluginIds(second)).not.toContain("external-chat");
707+expect(fs.existsSync(fullMarker)).toBe(false);
708+});
709+547710it("matches setup-only plugins by manifest-owned channel ids when plugin id differs", () => {
548711const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({
549712pluginId: "external-chat-plugin",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。