diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 23889d40b97..b01c978509d 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -295,6 +295,99 @@ describe("gateway.channelHealthCheckMinutes", () => { }); }); +describe("config identity/materialization regressions", () => { + it("keeps explicit responsePrefix and group mention patterns", () => { + const res = validateConfigObject({ + agents: { + list: [ + { + id: "main", + identity: { + name: "Samantha Sloth", + theme: "space lobster", + emoji: "🦞", + }, + groupChat: { mentionPatterns: ["@openclaw"] }, + }, + ], + }, + messages: { + responsePrefix: "✅", + }, + }); + + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.messages?.responsePrefix).toBe("✅"); + expect(res.config.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual(["@openclaw"]); + } + }); + + it("preserves empty responsePrefix when identity is present", () => { + const res = validateConfigObject({ + agents: { + list: [ + { + id: "main", + identity: { + name: "Samantha", + theme: "helpful sloth", + emoji: "🦥", + }, + }, + ], + }, + messages: { + responsePrefix: "", + }, + }); + + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.messages?.responsePrefix).toBe(""); + } + }); + + it("accepts blank model provider apiKey values", () => { + const res = validateConfigObject({ + models: { + mode: "merge", + providers: { + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + apiKey: "", + api: "anthropic-messages", + models: [ + { + id: "MiniMax-M2.7", + name: "MiniMax M2.7", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 8192, + }, + ], + }, + }, + }, + }); + + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.models?.providers?.minimax?.baseUrl).toBe( + "https://api.minimax.io/anthropic", + ); + expect(res.config.models?.providers?.minimax?.apiKey).toBe(""); + } + }); +}); + describe("cron webhook schema", () => { it("accepts cron.webhookToken and legacy cron.webhook", () => { const res = OpenClawSchema.safeParse({ @@ -345,6 +438,38 @@ describe("cron webhook schema", () => { }); expect(res.success).toBe(true); }); + + it("accepts channel textChunkLimit config without reviving legacy message limits", () => { + const res = OpenClawSchema.safeParse({ + messages: { + messagePrefix: "[openclaw]", + responsePrefix: "🦞", + }, + channels: { + whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 }, + telegram: { enabled: true, textChunkLimit: 3333 }, + discord: { + enabled: true, + textChunkLimit: 1999, + maxLinesPerMessage: 17, + }, + signal: { enabled: true, textChunkLimit: 2222 }, + imessage: { enabled: true, textChunkLimit: 1111 }, + }, + }); + + expect(res.success).toBe(true); + if (res.success) { + expect(res.data.channels?.whatsapp?.textChunkLimit).toBe(4444); + expect(res.data.channels?.telegram?.textChunkLimit).toBe(3333); + expect(res.data.channels?.discord?.textChunkLimit).toBe(1999); + expect(res.data.channels?.discord?.maxLinesPerMessage).toBe(17); + expect(res.data.channels?.signal?.textChunkLimit).toBe(2222); + expect(res.data.channels?.imessage?.textChunkLimit).toBe(1111); + const legacy = (res.data.messages as unknown as Record).textChunkLimit; + expect(legacy).toBeUndefined(); + } + }); }); describe("broadcast", () => { diff --git a/src/config/config.identity-defaults.test.ts b/src/config/config.identity-defaults.test.ts deleted file mode 100644 index c04fd376558..00000000000 --- a/src/config/config.identity-defaults.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; -import { validateConfigObject, validateConfigObjectRaw } from "./validation.js"; -import { OpenClawSchema } from "./zod-schema.js"; - -const defaultIdentity = { - name: "Samantha", - theme: "helpful sloth", - emoji: "🦥", -}; - -const configWithDefaultIdentity = (messages: Record) => ({ - agents: { - list: [ - { - id: "main", - identity: defaultIdentity, - }, - ], - }, - messages, -}); - -function expectValidConfig(raw: unknown) { - const result = validateConfigObject(raw); - expect(result.ok).toBe(true); - if (!result.ok) { - throw new Error(`expected config to validate: ${JSON.stringify(result.issues)}`); - } - return result.config; -} - -function expectValidRawConfig(raw: unknown) { - const result = validateConfigObjectRaw(raw); - expect(result.ok).toBe(true); - if (!result.ok) { - throw new Error(`expected raw config to validate: ${JSON.stringify(result.issues)}`); - } - return result.config; -} - -describe("config identity defaults", () => { - it("does not derive mention defaults and only sets ackReactionScope when identity is present", () => { - const cfg = expectValidConfig(configWithDefaultIdentity({})); - - expect(cfg.messages?.responsePrefix).toBeUndefined(); - expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined(); - expect(cfg.messages?.ackReaction).toBeUndefined(); - expect(cfg.messages?.ackReactionScope).toBe("group-mentions"); - }); - - it("keeps ackReaction unset and does not synthesize agent/session defaults when identity is missing", () => { - const cfg = expectValidConfig({ messages: {} }); - - expect(cfg.messages?.ackReaction).toBeUndefined(); - expect(cfg.messages?.ackReactionScope).toBe("group-mentions"); - expect(cfg.messages?.responsePrefix).toBeUndefined(); - expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined(); - expect(cfg.agents?.list).toBeUndefined(); - expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); - expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); - expect(cfg.session).toBeUndefined(); - }); - - it("does not override explicit values", () => { - const cfg = expectValidConfig({ - agents: { - list: [ - { - id: "main", - identity: { - name: "Samantha Sloth", - theme: "space lobster", - emoji: "🦞", - }, - groupChat: { mentionPatterns: ["@openclaw"] }, - }, - ], - }, - messages: { - responsePrefix: "✅", - }, - }); - - expect(cfg.messages?.responsePrefix).toBe("✅"); - expect(cfg.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual(["@openclaw"]); - }); - - it("supports provider textChunkLimit config", () => { - const result = OpenClawSchema.safeParse({ - messages: { - messagePrefix: "[openclaw]", - responsePrefix: "🦞", - }, - channels: { - whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 }, - telegram: { enabled: true, textChunkLimit: 3333 }, - discord: { - enabled: true, - textChunkLimit: 1999, - maxLinesPerMessage: 17, - }, - signal: { enabled: true, textChunkLimit: 2222 }, - imessage: { enabled: true, textChunkLimit: 1111 }, - }, - }); - expect(result.success).toBe(true); - if (!result.success) { - throw new Error(`expected schema parse success: ${JSON.stringify(result.error.issues)}`); - } - const cfg = result.data; - - expect(cfg.channels?.whatsapp?.textChunkLimit).toBe(4444); - expect(cfg.channels?.telegram?.textChunkLimit).toBe(3333); - expect(cfg.channels?.discord?.textChunkLimit).toBe(1999); - expect(cfg.channels?.discord?.maxLinesPerMessage).toBe(17); - expect(cfg.channels?.signal?.textChunkLimit).toBe(2222); - expect(cfg.channels?.imessage?.textChunkLimit).toBe(1111); - - const legacy = (cfg.messages as unknown as Record).textChunkLimit; - expect(legacy).toBeUndefined(); - }); - - it("accepts blank model provider apiKey values", () => { - const cfg = expectValidRawConfig({ - models: { - mode: "merge", - providers: { - minimax: { - baseUrl: "https://api.minimax.io/anthropic", - apiKey: "", - api: "anthropic-messages", - models: [ - { - id: "MiniMax-M2.7", - name: "MiniMax M2.7", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 8192, - }, - ], - }, - }, - }, - }); - - expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic"); - expect(cfg.models?.providers?.minimax?.apiKey).toBe(""); - }); - - it("accepts SecretRef values in model provider headers", () => { - const cfg = expectValidRawConfig({ - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - api: "openai-completions", - headers: { - Authorization: { - source: "env", - provider: "default", - id: "OPENAI_HEADER_TOKEN", - }, - }, - models: [], - }, - }, - }, - }); - - expect(cfg.models?.providers?.openai?.headers?.Authorization).toEqual({ - source: "env", - provider: "default", - id: "OPENAI_HEADER_TOKEN", - }); - }); - - it("respects empty responsePrefix to disable identity defaults", () => { - const cfg = expectValidConfig(configWithDefaultIdentity({ responsePrefix: "" })); - - expect(cfg.messages?.responsePrefix).toBe(""); - }); -}); diff --git a/src/config/config.secrets-schema.test.ts b/src/config/config.secrets-schema.test.ts index 322aa794c2d..5a66269c13c 100644 --- a/src/config/config.secrets-schema.test.ts +++ b/src/config/config.secrets-schema.test.ts @@ -171,6 +171,36 @@ describe("config secret refs schema", () => { expect(result.ok).toBe(true); }); + it("accepts model provider header SecretRef values", () => { + const result = validateConfigObjectRaw({ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + api: "openai-completions", + headers: { + Authorization: { + source: "env", + provider: "default", + id: "OPENAI_HEADER_TOKEN", + }, + }, + models: [], + }, + }, + }, + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.config.models?.providers?.openai?.headers?.Authorization).toEqual({ + source: "env", + provider: "default", + id: "OPENAI_HEADER_TOKEN", + }); + } + }); + it("rejects model provider request proxy url secret refs", () => { const result = validateConfigObjectRaw({ models: { diff --git a/src/config/defaults.test.ts b/src/config/defaults.test.ts index 9456b0e4e08..343b58d0bb7 100644 --- a/src/config/defaults.test.ts +++ b/src/config/defaults.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; const mocks = vi.hoisted(() => ({ applyProviderConfigDefaultsWithPlugin: vi.fn(), @@ -11,11 +12,14 @@ vi.mock("../plugins/provider-runtime.js", () => ({ })); let applyContextPruningDefaults: typeof import("./defaults.js").applyContextPruningDefaults; +let applyAgentDefaults: typeof import("./defaults.js").applyAgentDefaults; +let applyMessageDefaults: typeof import("./defaults.js").applyMessageDefaults; describe("config defaults", () => { beforeEach(async () => { vi.resetModules(); - ({ applyContextPruningDefaults } = await import("./defaults.js")); + ({ applyAgentDefaults, applyContextPruningDefaults, applyMessageDefaults } = + await import("./defaults.js")); mocks.applyProviderConfigDefaultsWithPlugin.mockReset(); }); @@ -54,4 +58,33 @@ describe("config defaults", () => { expect(applyContextPruningDefaults(cfg as never)).toBe(nextCfg); expect(mocks.applyProviderConfigDefaultsWithPlugin).toHaveBeenCalledTimes(1); }); + + it("defaults ackReactionScope without deriving other message fields", () => { + const next = applyMessageDefaults({ + agents: { + list: [ + { + id: "main", + identity: { + name: "Samantha", + theme: "helpful sloth", + emoji: "🦥", + }, + }, + ], + }, + messages: {}, + } as never); + + expect(next.messages?.ackReactionScope).toBe("group-mentions"); + expect(next.messages?.responsePrefix).toBeUndefined(); + expect(next.messages?.groupChat?.mentionPatterns).toBeUndefined(); + }); + + it("fills missing agent concurrency defaults", () => { + const next = applyAgentDefaults({ messages: {} } as never); + + expect(next.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); + expect(next.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); + }); });