From c9e07616c75340a1dd1b7f47e183f4366043b770 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 06:27:13 +0000 Subject: [PATCH] refactor: centralize WhatsApp config merging --- src/commands/onboard-providers.test.ts | 24 ++++++++++++++++++ src/commands/onboard-providers.ts | 24 ++++++------------ src/config/merge-config.ts | 35 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 src/config/merge-config.ts diff --git a/src/commands/onboard-providers.test.ts b/src/commands/onboard-providers.test.ts index f231fbb16..45ccb7f5f 100644 --- a/src/commands/onboard-providers.test.ts +++ b/src/commands/onboard-providers.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; import { + mergeWhatsAppConfig, setWhatsAppAllowFrom, setWhatsAppDmPolicy, setWhatsAppSelfChatMode, @@ -54,4 +55,27 @@ describe("onboard-providers WhatsApp setters", () => { expect(next.whatsapp?.selfChatMode).toBe(true); expect(next.whatsapp?.allowFrom).toEqual(["+15555550123"]); }); + + it("merges WhatsApp config without clobbering fields", () => { + const cfg: ClawdbotConfig = { + whatsapp: { + dmPolicy: "pairing", + allowFrom: ["*"], + }, + }; + + const merged = mergeWhatsAppConfig(cfg, { + dmPolicy: "open", + allowFrom: undefined, + }); + const cleared = mergeWhatsAppConfig( + cfg, + { allowFrom: undefined }, + { unsetOnUndefined: ["allowFrom"] }, + ); + + expect(merged.whatsapp?.dmPolicy).toBe("open"); + expect(merged.whatsapp?.allowFrom).toEqual(["*"]); + expect(cleared.whatsapp?.allowFrom).toBeUndefined(); + }); }); diff --git a/src/commands/onboard-providers.ts b/src/commands/onboard-providers.ts index 63472ecaf..f3cb60634 100644 --- a/src/commands/onboard-providers.ts +++ b/src/commands/onboard-providers.ts @@ -1,7 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { ClawdbotConfig } from "../config/config.js"; -import type { DmPolicy, WhatsAppConfig } from "../config/types.js"; +import { mergeWhatsAppConfig } from "../config/merge-config.js"; +import type { DmPolicy } from "../config/types.js"; import { listDiscordAccountIds, resolveDefaultDiscordAccountId, @@ -249,20 +250,7 @@ async function noteSlackTokenHelp( ); } -export function mergeWhatsAppConfig( - cfg: ClawdbotConfig, - patch: Partial, -): ClawdbotConfig { - const base = cfg.whatsapp ?? {}; - return { - ...cfg, - whatsapp: { - selfChatMode: base.selfChatMode, - ...base, - ...patch, - }, - }; -} +export { mergeWhatsAppConfig }; export function setWhatsAppDmPolicy( cfg: ClawdbotConfig, @@ -275,7 +263,11 @@ export function setWhatsAppAllowFrom( cfg: ClawdbotConfig, allowFrom?: string[], ): ClawdbotConfig { - return mergeWhatsAppConfig(cfg, { allowFrom }); + return mergeWhatsAppConfig( + cfg, + { allowFrom }, + { unsetOnUndefined: ["allowFrom"] }, + ); } function setMessagesResponsePrefix( diff --git a/src/config/merge-config.ts b/src/config/merge-config.ts new file mode 100644 index 000000000..1e78d0cb6 --- /dev/null +++ b/src/config/merge-config.ts @@ -0,0 +1,35 @@ +import type { ClawdbotConfig } from "./config.js"; +import type { WhatsAppConfig } from "./types.js"; + +export type MergeSectionOptions = { + unsetOnUndefined?: Array; +}; + +export function mergeConfigSection>( + base: T | undefined, + patch: Partial, + options: MergeSectionOptions = {}, +): T { + const next: Record = { ...(base ?? {}) }; + for (const [key, value] of Object.entries(patch) as [keyof T, T[keyof T]][]) { + if (value === undefined) { + if (options.unsetOnUndefined?.includes(key)) { + delete next[key as string]; + } + continue; + } + next[key as string] = value as unknown; + } + return next as T; +} + +export function mergeWhatsAppConfig( + cfg: ClawdbotConfig, + patch: Partial, + options?: MergeSectionOptions, +): ClawdbotConfig { + return { + ...cfg, + whatsapp: mergeConfigSection(cfg.whatsapp, patch, options), + }; +}