






















@@ -308,6 +308,98 @@ describe("config cli", () => {
308308expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Dry run successful"));
309309});
310310311+it("rejects protected model map replacement unless explicitly requested", async () => {
312+const resolved: OpenClawConfig = {
313+agents: {
314+defaults: {
315+models: {
316+"openai/gpt-5.4": { alias: "GPT" },
317+"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
318+},
319+},
320+},
321+};
322+setSnapshot(resolved, resolved);
323+324+await expect(
325+runConfigCommand([
326+"config",
327+"set",
328+"agents.defaults.models",
329+'{"openai/gpt-5.4":{}}',
330+"--strict-json",
331+]),
332+).rejects.toThrow("__exit__:1");
333+334+expect(mockWriteConfigFile).not.toHaveBeenCalled();
335+expect(mockError).toHaveBeenCalledWith(
336+expect.stringContaining("Refusing to replace agents.defaults.models"),
337+);
338+});
339+340+it("merges protected model map values with --merge", async () => {
341+const resolved: OpenClawConfig = {
342+agents: {
343+defaults: {
344+models: {
345+"openai/gpt-5.4": { alias: "GPT" },
346+},
347+},
348+},
349+};
350+setSnapshot(resolved, resolved);
351+352+await runConfigCommand([
353+"config",
354+"set",
355+"agents.defaults.models",
356+'{"anthropic/claude-sonnet-4-6":{"alias":"Sonnet"}}',
357+"--strict-json",
358+"--merge",
359+]);
360+361+expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
362+const written = mockWriteConfigFile.mock.calls[0]?.[0];
363+expect(written.agents?.defaults?.models).toEqual({
364+"openai/gpt-5.4": { alias: "GPT" },
365+"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
366+});
367+});
368+369+it("merges provider model arrays by id with --merge", async () => {
370+const resolved = {
371+models: {
372+providers: {
373+ollama: {
374+api: "ollama",
375+models: [
376+{ id: "llama3.2", name: "Llama 3.2", contextWindow: 131072 },
377+{ id: "qwen3", name: "Qwen 3" },
378+],
379+},
380+},
381+},
382+} as unknown as OpenClawConfig;
383+setSnapshot(resolved, resolved);
384+385+await runConfigCommand([
386+"config",
387+"set",
388+"models.providers.ollama.models",
389+'[{"id":"llama3.2","name":"Llama 3.2 latest"},{"id":"gemma4","name":"Gemma 4"}]',
390+"--strict-json",
391+"--merge",
392+]);
393+394+expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
395+const written = mockWriteConfigFile.mock.calls[0]?.[0];
396+expect(written.models?.providers?.ollama?.models).toEqual([
397+{ id: "llama3.2", name: "Llama 3.2 latest", contextWindow: 131072 },
398+{ id: "qwen3", name: "Qwen 3" },
399+{ id: "gemma4", name: "Gemma 4" },
400+]);
401+});
402+311403it("writes agents.defaults.llm.idleTimeoutSeconds without disturbing sibling defaults", async () => {
312404const resolved: OpenClawConfig = {
313405agents: {
@@ -1256,6 +1348,57 @@ describe("config cli", () => {
12561348expect(written.gateway?.auth).toEqual({ mode: "token" });
12571349});
125813501351+it("batch-file nested leaf updates preserve agents defaults and list siblings", async () => {
1352+const resolved: OpenClawConfig = {
1353+agents: {
1354+defaults: {
1355+models: {
1356+"openai/gpt-5.4": { alias: "GPT" },
1357+},
1358+model: { primary: "openai/gpt-5.4" },
1359+},
1360+list: [{ id: "main" }, { id: "ops" }],
1361+},
1362+plugins: {
1363+entries: {
1364+"github-copilot": { enabled: true },
1365+},
1366+},
1367+};
1368+setSnapshot(resolved, resolved);
1369+1370+const pathname = path.join(
1371+os.tmpdir(),
1372+`openclaw-config-memory-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
1373+);
1374+fs.writeFileSync(
1375+pathname,
1376+JSON.stringify([
1377+{ path: "agents.defaults.memorySearch.enabled", value: true },
1378+{ path: "agents.defaults.memorySearch.provider", value: "gemini" },
1379+{ path: "agents.defaults.memorySearch.sources", value: ["memory"] },
1380+]),
1381+"utf8",
1382+);
1383+try {
1384+await runConfigCommand(["config", "set", "--batch-file", pathname]);
1385+} finally {
1386+fs.rmSync(pathname, { force: true });
1387+}
1388+1389+expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
1390+const written = mockWriteConfigFile.mock.calls[0]?.[0];
1391+expect(written.agents?.defaults?.models).toEqual(resolved.agents?.defaults?.models);
1392+expect(written.agents?.defaults?.model).toEqual(resolved.agents?.defaults?.model);
1393+expect(written.agents?.defaults?.memorySearch).toEqual({
1394+enabled: true,
1395+provider: "gemini",
1396+sources: ["memory"],
1397+});
1398+expect(written.agents?.list).toEqual(resolved.agents?.list);
1399+expect(written.plugins).toEqual(resolved.plugins);
1400+});
1401+12591402it("rejects malformed batch-file payloads", async () => {
12601403const pathname = path.join(
12611404os.tmpdir(),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。