diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index 216d08eafb8..75eeaaac0c4 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -110,6 +110,68 @@ describe("config io write", () => { }); }); + it("does not reintroduce Slack/Discord legacy dm.policy defaults when writing", async () => { + await withTempHome("openclaw-config-io-", async (home) => { + const configPath = path.join(home, ".openclaw", "openclaw.json"); + await fs.mkdir(path.dirname(configPath), { recursive: true }); + await fs.writeFile( + configPath, + JSON.stringify( + { + channels: { + discord: { + dmPolicy: "pairing", + dm: { enabled: true, policy: "pairing" }, + }, + slack: { + dmPolicy: "pairing", + dm: { enabled: true, policy: "pairing" }, + }, + }, + gateway: { port: 18789 }, + }, + null, + 2, + ), + "utf-8", + ); + + const io = createConfigIO({ + env: {} as NodeJS.ProcessEnv, + homedir: () => home, + logger: silentLogger, + }); + + const snapshot = await io.readConfigFileSnapshot(); + expect(snapshot.valid).toBe(true); + + const next = structuredClone(snapshot.config); + // Simulate doctor removing legacy keys while keeping dm enabled. + if (next.channels?.discord?.dm && typeof next.channels.discord.dm === "object") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper + delete (next.channels.discord.dm as any).policy; + } + if (next.channels?.slack?.dm && typeof next.channels.slack.dm === "object") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper + delete (next.channels.slack.dm as any).policy; + } + + await io.writeConfigFile(next); + + const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as { + channels?: { + discord?: { dm?: Record; dmPolicy?: unknown }; + slack?: { dm?: Record; dmPolicy?: unknown }; + }; + }; + + expect(persisted.channels?.discord?.dmPolicy).toBe("pairing"); + expect(persisted.channels?.discord?.dm).toEqual({ enabled: true }); + expect(persisted.channels?.slack?.dmPolicy).toBe("pairing"); + expect(persisted.channels?.slack?.dm).toEqual({ enabled: true }); + }); + }); + it("keeps env refs in arrays when appending entries", async () => { await withTempHome("openclaw-config-io-", async (home) => { const configPath = path.join(home, ".openclaw", "openclaw.json"); diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 632b86d1190..88bbe934b9a 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -211,7 +211,7 @@ export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({ export const DiscordDmSchema = z .object({ enabled: z.boolean().optional(), - policy: DmPolicySchema.optional().default("pairing"), + policy: DmPolicySchema.optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupEnabled: z.boolean().optional(), groupChannels: z.array(z.union([z.string(), z.number()])).optional(), @@ -459,7 +459,7 @@ export const GoogleChatConfigSchema = GoogleChatAccountSchema.extend({ export const SlackDmSchema = z .object({ enabled: z.boolean().optional(), - policy: DmPolicySchema.optional().default("pairing"), + policy: DmPolicySchema.optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupEnabled: z.boolean().optional(), groupChannels: z.array(z.union([z.string(), z.number()])).optional(),