























@@ -4,6 +4,8 @@ import path from "node:path";
44import { Command } from "commander";
55import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
66import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js";
7+import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js";
8+import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
79import { createCliRuntimeCapture, mockRuntimeModule } from "./test-runtime-capture.js";
810911/**
@@ -21,6 +23,7 @@ const mockWriteConfigFile = vi.fn<
2123>(async () => {});
2224const mockResolveSecretRefValue = vi.fn();
2325const mockReadBestEffortRuntimeConfigSchema = vi.fn();
26+const mockLoadPluginMetadataSnapshot = vi.fn(() => createPluginMetadataSnapshot());
24272528vi.mock("../config/config.js", async (importOriginal) => {
2629const actual = await importOriginal<typeof import("../config/config.js")>();
@@ -46,6 +49,14 @@ vi.mock("../config/runtime-schema.js", () => ({
4649readBestEffortRuntimeConfigSchema: () => mockReadBestEffortRuntimeConfigSchema(),
4750}));
485152+vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => {
53+const actual = await importOriginal<typeof import("../plugins/plugin-metadata-snapshot.js")>();
54+return {
55+ ...actual,
56+loadPluginMetadataSnapshot: (...args: unknown[]) => mockLoadPluginMetadataSnapshot(...args),
57+};
58+});
59+4960const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
5061const mockLog = defaultRuntime.log;
5162const mockError = defaultRuntime.error;
@@ -107,6 +118,98 @@ function withRuntimeDefaults(resolved: OpenClawConfig): OpenClawConfig {
107118};
108119}
109120121+function createPluginManifestRecord(
122+overrides: Partial<PluginManifestRecord> & Pick<PluginManifestRecord, "id">,
123+): PluginManifestRecord {
124+return {
125+channels: [],
126+cliBackends: [],
127+hooks: [],
128+manifestPath: `/tmp/${overrides.id}/openclaw.plugin.json`,
129+origin: "bundled",
130+providers: [],
131+rootDir: `/tmp/${overrides.id}`,
132+skills: [],
133+source: `/tmp/${overrides.id}/index.js`,
134+ ...overrides,
135+};
136+}
137+138+function createPluginMetadataSnapshot(
139+manifestRegistry: PluginManifestRegistry = { diagnostics: [], plugins: [] },
140+): PluginMetadataSnapshot {
141+const plugins = manifestRegistry.plugins;
142+return {
143+policyHash: "test-policy",
144+index: {
145+version: 1,
146+hostContractVersion: "test",
147+compatRegistryVersion: "test",
148+migrationVersion: 1,
149+policyHash: "test-policy",
150+generatedAtMs: 0,
151+installRecords: {},
152+plugins: [],
153+diagnostics: [],
154+},
155+registryDiagnostics: [],
156+ manifestRegistry,
157+ plugins,
158+diagnostics: manifestRegistry.diagnostics,
159+byPluginId: new Map(plugins.map((plugin) => [plugin.id, plugin])),
160+normalizePluginId: (pluginId: string) => pluginId.trim().toLowerCase(),
161+owners: {
162+channels: new Map(),
163+channelConfigs: new Map(),
164+providers: new Map(),
165+modelCatalogProviders: new Map(),
166+cliBackends: new Map(),
167+setupProviders: new Map(),
168+commandAliases: new Map(),
169+contracts: new Map(),
170+},
171+metrics: {
172+registrySnapshotMs: 0,
173+manifestRegistryMs: 0,
174+ownerMapsMs: 0,
175+totalMs: 0,
176+indexPluginCount: 0,
177+manifestPluginCount: plugins.length,
178+},
179+};
180+}
181+182+function setExternalFeishuSchema() {
183+mockLoadPluginMetadataSnapshot.mockReturnValue(
184+createPluginMetadataSnapshot({
185+diagnostics: [],
186+plugins: [
187+createPluginManifestRecord({
188+id: "openclaw-lark",
189+origin: "external",
190+channels: ["feishu"],
191+channelConfigs: {
192+feishu: {
193+schema: {
194+type: "object",
195+properties: {
196+appId: { type: "string" },
197+appSecret: { type: "string" },
198+replyMode: { type: "string", enum: ["thread", "direct"] },
199+footer: { type: "string" },
200+},
201+required: ["appId", "appSecret"],
202+additionalProperties: false,
203+},
204+uiHints: {},
205+},
206+},
207+}),
208+],
209+}),
210+);
211+}
212+110213function makeInvalidSnapshot(params: {
111214issues: ConfigFileSnapshot["issues"];
112215path?: string;
@@ -233,6 +336,7 @@ describe("config cli", () => {
233336beforeEach(() => {
234337vi.clearAllMocks();
235338resetRuntimeCapture();
339+mockLoadPluginMetadataSnapshot.mockReturnValue(createPluginMetadataSnapshot());
236340mockReadBestEffortRuntimeConfigSchema.mockResolvedValue({
237341schema: {
238342$schema: "http://json-schema.org/draft-07/schema#",
@@ -1268,6 +1372,35 @@ describe("config cli", () => {
12681372expectErrorIncludes("Dry run failed: config schema validation failed.");
12691373});
127013741375+it("dry-runs config patch channel fields against plugin-owned schemas", async () => {
1376+setExternalFeishuSchema();
1377+const resolved: OpenClawConfig = {
1378+channels: {
1379+feishu: {
1380+appId: "app-id",
1381+appSecret: "secret",
1382+},
1383+},
1384+};
1385+setSnapshot(resolved, resolved);
1386+const pathname = writeTempJson5File("openclaw-config-plugin-channel-schema", {
1387+channels: {
1388+feishu: {
1389+appId: "app-id",
1390+appSecret: "secret",
1391+replyMode: "thread",
1392+footer: "OpenClaw",
1393+},
1394+},
1395+});
1396+1397+await runConfigCommand(["config", "patch", "--file", pathname, "--dry-run"]);
1398+1399+expect(mockWriteConfigFile).not.toHaveBeenCalled();
1400+expect(mockError).not.toHaveBeenCalledWith(expect.stringContaining("replyMode"));
1401+expect(mockError).not.toHaveBeenCalledWith(expect.stringContaining("footer"));
1402+});
1403+12711404it("fails dry-run when unsupported mutable paths receive SecretRef objects in value/json mode", async () => {
12721405const resolved: OpenClawConfig = {
12731406gateway: { port: 18789 },
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。