diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index e1c02160b67..d4d9e55580e 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -1,5 +1,14 @@ +import fs from "node:fs/promises"; +import path from "node:path"; import { describe, expect, it } from "vitest"; -import { validateConfigObject } from "./config.js"; +import { + getConfigValueAtPath, + parseConfigPath, + setConfigValueAtPath, + unsetConfigValueAtPath, +} from "./config-paths.js"; +import { readConfigFileSnapshot, validateConfigObject } from "./config.js"; +import { withTempHome } from "./test-helpers.js"; import { OpenClawSchema } from "./zod-schema.js"; describe("$schema key in config (#14998)", () => { @@ -167,3 +176,118 @@ describe("cron webhook schema", () => { expect(res.success).toBe(false); }); }); + +describe("broadcast", () => { + it("accepts a broadcast peer map with strategy", () => { + const res = validateConfigObject({ + agents: { + list: [{ id: "alfred" }, { id: "baerbel" }], + }, + broadcast: { + strategy: "parallel", + "120363403215116621@g.us": ["alfred", "baerbel"], + }, + }); + expect(res.ok).toBe(true); + }); + + it("rejects invalid broadcast strategy", () => { + const res = validateConfigObject({ + broadcast: { strategy: "nope" }, + }); + expect(res.ok).toBe(false); + }); + + it("rejects non-array broadcast entries", () => { + const res = validateConfigObject({ + broadcast: { "120363403215116621@g.us": 123 }, + }); + expect(res.ok).toBe(false); + }); +}); + +describe("model compat config schema", () => { + it("accepts full openai-completions compat fields", () => { + const res = validateConfigObject({ + models: { + providers: { + local: { + baseUrl: "http://127.0.0.1:1234/v1", + api: "openai-completions", + models: [ + { + id: "qwen3-32b", + name: "Qwen3 32B", + compat: { + supportsUsageInStreaming: true, + supportsStrictMode: false, + thinkingFormat: "qwen", + requiresToolResultName: true, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresMistralToolIds: false, + }, + }, + ], + }, + }, + }, + }); + + expect(res.ok).toBe(true); + }); +}); + +describe("config paths", () => { + it("rejects empty and blocked paths", () => { + expect(parseConfigPath("")).toEqual({ + ok: false, + error: "Invalid path. Use dot notation (e.g. foo.bar).", + }); + expect(parseConfigPath("__proto__.polluted").ok).toBe(false); + expect(parseConfigPath("constructor.polluted").ok).toBe(false); + expect(parseConfigPath("prototype.polluted").ok).toBe(false); + }); + + it("sets, gets, and unsets nested values", () => { + const root: Record = {}; + const parsed = parseConfigPath("foo.bar"); + if (!parsed.ok || !parsed.path) { + throw new Error("path parse failed"); + } + setConfigValueAtPath(root, parsed.path, 123); + expect(getConfigValueAtPath(root, parsed.path)).toBe(123); + expect(unsetConfigValueAtPath(root, parsed.path)).toBe(true); + expect(getConfigValueAtPath(root, parsed.path)).toBeUndefined(); + }); +}); + +describe("config strict validation", () => { + it("rejects unknown fields", async () => { + const res = validateConfigObject({ + agents: { list: [{ id: "pi" }] }, + customUnknownField: { nested: "value" }, + }); + expect(res.ok).toBe(false); + }); + + it("flags legacy config entries without auto-migrating", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".openclaw"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "openclaw.json"), + JSON.stringify({ + agents: { list: [{ id: "pi" }] }, + routing: { allowFrom: ["+15555550123"] }, + }), + "utf-8", + ); + + const snap = await readConfigFileSnapshot(); + + expect(snap.valid).toBe(false); + expect(snap.legacyIssues).not.toHaveLength(0); + }); + }); +}); diff --git a/src/config/config-paths.test.ts b/src/config/config-paths.test.ts deleted file mode 100644 index a4dc7192ecd..00000000000 --- a/src/config/config-paths.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getConfigValueAtPath, - parseConfigPath, - setConfigValueAtPath, - unsetConfigValueAtPath, -} from "./config-paths.js"; - -describe("config paths", () => { - it("rejects empty and blocked paths", () => { - expect(parseConfigPath("")).toEqual({ - ok: false, - error: "Invalid path. Use dot notation (e.g. foo.bar).", - }); - expect(parseConfigPath("__proto__.polluted").ok).toBe(false); - expect(parseConfigPath("constructor.polluted").ok).toBe(false); - expect(parseConfigPath("prototype.polluted").ok).toBe(false); - }); - - it("sets, gets, and unsets nested values", () => { - const root: Record = {}; - const parsed = parseConfigPath("foo.bar"); - if (!parsed.ok || !parsed.path) { - throw new Error("path parse failed"); - } - setConfigValueAtPath(root, parsed.path, 123); - expect(getConfigValueAtPath(root, parsed.path)).toBe(123); - expect(unsetConfigValueAtPath(root, parsed.path)).toBe(true); - expect(getConfigValueAtPath(root, parsed.path)).toBeUndefined(); - }); -}); diff --git a/src/config/config.broadcast.test.ts b/src/config/config.broadcast.test.ts deleted file mode 100644 index cab0cdf12b1..00000000000 --- a/src/config/config.broadcast.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { validateConfigObject } from "./config.js"; - -describe("broadcast", () => { - it("accepts a broadcast peer map with strategy", () => { - const res = validateConfigObject({ - agents: { - list: [{ id: "alfred" }, { id: "baerbel" }], - }, - broadcast: { - strategy: "parallel", - "120363403215116621@g.us": ["alfred", "baerbel"], - }, - }); - expect(res.ok).toBe(true); - }); - - it("rejects invalid broadcast strategy", () => { - const res = validateConfigObject({ - broadcast: { strategy: "nope" }, - }); - expect(res.ok).toBe(false); - }); - - it("rejects non-array broadcast entries", () => { - const res = validateConfigObject({ - broadcast: { "120363403215116621@g.us": 123 }, - }); - expect(res.ok).toBe(false); - }); -}); diff --git a/src/config/config.model-compat-schema.test.ts b/src/config/config.model-compat-schema.test.ts deleted file mode 100644 index 7039e44f34c..00000000000 --- a/src/config/config.model-compat-schema.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { validateConfigObject } from "./validation.js"; - -describe("model compat config schema", () => { - it("accepts full openai-completions compat fields", () => { - const res = validateConfigObject({ - models: { - providers: { - local: { - baseUrl: "http://127.0.0.1:1234/v1", - api: "openai-completions", - models: [ - { - id: "qwen3-32b", - name: "Qwen3 32B", - compat: { - supportsUsageInStreaming: true, - supportsStrictMode: false, - thinkingFormat: "qwen", - requiresToolResultName: true, - requiresAssistantAfterToolResult: false, - requiresThinkingAsText: false, - requiresMistralToolIds: false, - }, - }, - ], - }, - }, - }, - }); - - expect(res.ok).toBe(true); - }); -}); diff --git a/src/config/config.preservation-on-validation-failure.test.ts b/src/config/config.preservation-on-validation-failure.test.ts deleted file mode 100644 index b82b861d289..00000000000 --- a/src/config/config.preservation-on-validation-failure.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { readConfigFileSnapshot, validateConfigObject } from "./config.js"; -import { withTempHome } from "./test-helpers.js"; - -describe("config strict validation", () => { - it("rejects unknown fields", async () => { - const res = validateConfigObject({ - agents: { list: [{ id: "pi" }] }, - customUnknownField: { nested: "value" }, - }); - expect(res.ok).toBe(false); - }); - - it("flags legacy config entries without auto-migrating", async () => { - await withTempHome(async (home) => { - const configDir = path.join(home, ".openclaw"); - await fs.mkdir(configDir, { recursive: true }); - await fs.writeFile( - path.join(configDir, "openclaw.json"), - JSON.stringify({ - agents: { list: [{ id: "pi" }] }, - routing: { allowFrom: ["+15555550123"] }, - }), - "utf-8", - ); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(false); - expect(snap.legacyIssues).not.toHaveLength(0); - }); - }); -});