





















@@ -1,5 +1,6 @@
11import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22import type { OpenClawConfig } from "../../../config/config.js";
3+import type { PluginInstallRecord } from "../../../config/types.plugins.js";
34import type { PluginManifestRecord } from "../../../plugins/manifest-registry.js";
45import * as manifestRegistry from "../../../plugins/manifest-registry.js";
56import {
@@ -8,6 +9,18 @@ import {
89scanStalePluginConfig,
910} from "./stale-plugin-config.js";
101112+const installedPluginIndexMocks = vi.hoisted(() => ({
13+loadInstalledPluginIndexInstallRecordsSync: vi.fn<() => Record<string, PluginInstallRecord>>(
14+() => ({}),
15+),
16+}));
17+18+vi.mock("../../../plugins/installed-plugin-index-records.js", async (importOriginal) => ({
19+ ...(await importOriginal<typeof import("../../../plugins/installed-plugin-index-records.js")>()),
20+loadInstalledPluginIndexInstallRecordsSync:
21+installedPluginIndexMocks.loadInstalledPluginIndexInstallRecordsSync,
22+}));
23+1124function manifest(id: string): PluginManifestRecord {
1225return {
1326 id,
@@ -25,6 +38,8 @@ function manifest(id: string): PluginManifestRecord {
25382639describe("doctor stale plugin config helpers", () => {
2740beforeEach(() => {
41+installedPluginIndexMocks.loadInstalledPluginIndexInstallRecordsSync.mockReset();
42+installedPluginIndexMocks.loadInstalledPluginIndexInstallRecordsSync.mockReturnValue({});
2843vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
2944plugins: [manifest("discord"), manifest("voice-call"), manifest("openai")],
3045diagnostics: [],
@@ -99,6 +114,113 @@ describe("doctor stale plugin config helpers", () => {
99114]);
100115});
101116117+it("removes stale third-party channel config and dependent channel refs", () => {
118+const result = maybeRepairStalePluginConfig({
119+plugins: {
120+allow: ["discord", "openclaw-weixin"],
121+entries: {
122+discord: { enabled: true },
123+"openclaw-weixin": { enabled: true },
124+},
125+},
126+channels: {
127+"openclaw-weixin": {
128+enabled: true,
129+token: "stale",
130+},
131+telegram: {
132+botToken: "keep",
133+},
134+modelByChannel: {
135+openai: {
136+"openclaw-weixin": "openai/gpt-5.4",
137+telegram: "openai/gpt-5.4",
138+},
139+},
140+},
141+agents: {
142+defaults: {
143+heartbeat: {
144+target: "openclaw-weixin",
145+every: "30m",
146+},
147+},
148+list: [
149+{
150+id: "pi",
151+heartbeat: {
152+target: "openclaw-weixin",
153+},
154+},
155+{
156+id: "ops",
157+heartbeat: {
158+target: "telegram",
159+},
160+},
161+],
162+},
163+} as OpenClawConfig);
164+165+expect(result.changes).toEqual([
166+"- plugins.allow: removed 1 stale plugin id (openclaw-weixin)",
167+"- plugins.entries: removed 1 stale plugin entry (openclaw-weixin)",
168+"- channels: removed 1 stale channel config (openclaw-weixin)",
169+"- agents heartbeat: removed 2 stale heartbeat targets (openclaw-weixin)",
170+"- channels.modelByChannel: removed 1 stale channel model override (openclaw-weixin)",
171+]);
172+expect(result.config.plugins?.allow).toEqual(["discord"]);
173+expect(result.config.plugins?.entries).toEqual({
174+discord: { enabled: true },
175+});
176+expect(result.config.channels?.["openclaw-weixin"]).toBeUndefined();
177+expect(result.config.channels?.telegram).toEqual({ botToken: "keep" });
178+expect(result.config.channels?.modelByChannel).toEqual({
179+openai: {
180+telegram: "openai/gpt-5.4",
181+},
182+});
183+expect(result.config.agents?.defaults?.heartbeat).toEqual({ every: "30m" });
184+expect(result.config.agents?.list?.[0]?.heartbeat).toEqual({});
185+expect(result.config.agents?.list?.[1]?.heartbeat).toEqual({ target: "telegram" });
186+});
187+188+it("does not remove unknown channel config without stale plugin evidence", () => {
189+const cfg = {
190+channels: {
191+telegrm: {
192+botToken: "typo",
193+},
194+},
195+} as OpenClawConfig;
196+197+expect(scanStalePluginConfig(cfg)).toEqual([]);
198+expect(maybeRepairStalePluginConfig(cfg)).toEqual({ config: cfg, changes: [] });
199+});
200+201+it("uses missing persisted install records as stale channel evidence", () => {
202+installedPluginIndexMocks.loadInstalledPluginIndexInstallRecordsSync.mockReturnValue({
203+"openclaw-weixin": {
204+source: "npm",
205+resolvedName: "@tencent-weixin/openclaw-weixin",
206+installedAt: "2026-04-12T00:00:00.000Z",
207+},
208+});
209+210+const result = maybeRepairStalePluginConfig({
211+channels: {
212+"openclaw-weixin": {
213+enabled: true,
214+},
215+},
216+} as OpenClawConfig);
217+218+expect(result.changes).toEqual([
219+"- channels: removed 1 stale channel config (openclaw-weixin)",
220+]);
221+expect(result.config.channels?.["openclaw-weixin"]).toBeUndefined();
222+});
223+102224it("does not auto-repair stale refs while plugin discovery has errors", () => {
103225vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
104226plugins: [],
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。