

























@@ -8,6 +8,12 @@ vi.mock("../config/config.js", () => ({
88readConfigFileSnapshot: vi.fn(),
99recoverConfigFromLastKnownGood: vi.fn(),
1010recoverConfigFromJsonRootSuffix: vi.fn(),
11+isPluginLocalInvalidConfigSnapshot: vi.fn((snapshot: ConfigFileSnapshot) => {
12+if (snapshot.valid || snapshot.legacyIssues.length > 0 || snapshot.issues.length === 0) {
13+return false;
14+}
15+return snapshot.issues.every((issue) => issue.path.startsWith("plugins.entries."));
16+}),
1117shouldAttemptLastKnownGoodRecovery: vi.fn((snapshot: ConfigFileSnapshot) => {
1218if (snapshot.valid) {
1319return false;
@@ -125,7 +131,7 @@ describe("gateway startup config recovery", () => {
125131expect(recoveryNotice.enqueueConfigRecoveryNotice).not.toHaveBeenCalled();
126132});
127133128-it("does not restore last-known-good for plugin-local startup invalidity", async () => {
134+it("continues startup in degraded mode for plugin-local startup invalidity", async () => {
129135const invalidSnapshot = buildTestConfigSnapshot({
130136path: configPath,
131137exists: true,
@@ -171,16 +177,82 @@ describe("gateway startup config recovery", () => {
171177minimalTestGateway: true,
172178 log,
173179}),
174-).rejects.toThrow(`Invalid config at ${configPath}.`);
180+).resolves.toEqual({
181+snapshot: expect.objectContaining({
182+valid: true,
183+issues: [],
184+warnings: invalidSnapshot.issues,
185+}),
186+wroteConfig: false,
187+degradedPluginConfig: true,
188+});
175189176190expect(configIo.recoverConfigFromLastKnownGood).not.toHaveBeenCalled();
177-expect(configIo.recoverConfigFromJsonRootSuffix).toHaveBeenCalledWith(invalidSnapshot);
191+expect(configIo.recoverConfigFromJsonRootSuffix).not.toHaveBeenCalled();
178192expect(log.warn).toHaveBeenCalledWith(
179-`gateway: last-known-good recovery skipped for plugin-local config invalidity: ${configPath}`,
193+`gateway: skipped plugin config validation issue at plugins.entries.feishu: plugin feishu: plugin requires OpenClaw >=2026.4.23, but this host is 2026.4.22; skipping load. Run "openclaw doctor --fix" to quarantine the plugin config.`,
180194);
181195expect(recoveryNotice.enqueueConfigRecoveryNotice).not.toHaveBeenCalled();
182196});
183197198+it("keeps mixed plugin and core startup invalidity fatal", async () => {
199+const invalidSnapshot = buildTestConfigSnapshot({
200+path: configPath,
201+exists: true,
202+raw: `${JSON.stringify({
203+ gateway: { mode: "invalid" },
204+ plugins: {
205+ entries: {
206+ feishu: { enabled: true },
207+ },
208+ },
209+ })}\n`,
210+parsed: {
211+gateway: { mode: "invalid" },
212+plugins: {
213+entries: {
214+feishu: { enabled: true },
215+},
216+},
217+},
218+valid: false,
219+config: {
220+gateway: { mode: "invalid" },
221+plugins: {
222+entries: {
223+feishu: { enabled: true },
224+},
225+},
226+} as unknown as OpenClawConfig,
227+issues: [
228+{
229+path: "gateway.mode",
230+message: "Expected 'local' or 'remote'",
231+},
232+{
233+path: "plugins.entries.feishu.config.token",
234+message: "invalid config: must be string",
235+},
236+],
237+legacyIssues: [],
238+});
239+vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
240+vi.mocked(configIo.recoverConfigFromLastKnownGood).mockResolvedValueOnce(false);
241+vi.mocked(configIo.recoverConfigFromJsonRootSuffix).mockResolvedValueOnce(false);
242+243+await expect(
244+loadGatewayStartupConfigSnapshot({
245+minimalTestGateway: true,
246+log: { info: vi.fn(), warn: vi.fn() },
247+}),
248+).rejects.toThrow(`Invalid config at ${configPath}.`);
249+250+expect(configIo.recoverConfigFromLastKnownGood).toHaveBeenCalledWith({
251+snapshot: invalidSnapshot,
252+reason: "startup-invalid-config",
253+});
254+});
255+184256it("skips providers with stale model api enum values during startup", async () => {
185257const config = {
186258gateway: { mode: "local" },
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。