From 00e902a60b31ca413335ccf6ee429d204fee23fc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 12:42:21 +0100 Subject: [PATCH] test: speed up legacy config tests --- ...etection.accepts-imessage-dmpolicy.test.ts | 127 ++---- ...etection.rejects-routing-allowfrom.test.ts | 408 +++++++----------- ...nfig.legacy-config-provider-shapes.test.ts | 171 -------- .../config.legacy-config-snapshot.test.ts | 372 ---------------- 4 files changed, 200 insertions(+), 878 deletions(-) diff --git a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts index f0c5a49db14..79388eea0ae 100644 --- a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts +++ b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts @@ -1,6 +1,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { + DiscordConfigSchema, + MSTeamsConfigSchema, + SlackConfigSchema, +} from "./zod-schema.providers-core.js"; const { loadConfig, readConfigFileSnapshot, validateConfigObject } = await vi.importActual("./config.js"); @@ -55,6 +60,20 @@ function expectValidConfigValue(params: { expect(params.readValue(res.config)).toBe(params.expectedValue); } +function expectSchemaConfigValue(params: { + schema: { safeParse: (value: unknown) => { success: true; data: unknown } | { success: false } }; + config: unknown; + readValue: (config: unknown) => unknown; + expectedValue: unknown; +}) { + const res = params.schema.safeParse(params.config); + expect(res.success).toBe(true); + if (!res.success) { + throw new Error("expected schema config to be valid"); + } + expect(params.readValue(res.data)).toBe(params.expectedValue); +} + function expectInvalidIssuePath(config: unknown, expectedPath: string) { const res = validateConfigObject(config); expect(res.ok).toBe(false); @@ -84,49 +103,48 @@ describe("legacy config detection", () => { expect(res.config.channels?.imessage?.dmPolicy).toBe("open"); } }); - it.each([ - [ - "defaults imessage.dmPolicy to pairing when imessage section exists", - { channels: { imessage: {} } }, - (config: unknown) => + it("defaults imessage.dmPolicy to pairing when imessage section exists", () => { + expectValidConfigValue({ + config: { channels: { imessage: {} } }, + readValue: (config) => (config as { channels?: { imessage?: { dmPolicy?: string } } }).channels?.imessage ?.dmPolicy, - "pairing", - ], - [ - "defaults imessage.groupPolicy to allowlist when imessage section exists", - { channels: { imessage: {} } }, - (config: unknown) => + expectedValue: "pairing", + }); + }); + it("defaults imessage.groupPolicy to allowlist when imessage section exists", () => { + expectValidConfigValue({ + config: { channels: { imessage: {} } }, + readValue: (config) => (config as { channels?: { imessage?: { groupPolicy?: string } } }).channels?.imessage ?.groupPolicy, - "allowlist", - ], + expectedValue: "allowlist", + }); + }); + it.each([ [ "defaults discord.groupPolicy to allowlist when discord section exists", - { channels: { discord: {} } }, - (config: unknown) => - (config as { channels?: { discord?: { groupPolicy?: string } } }).channels?.discord - ?.groupPolicy, + DiscordConfigSchema, + {}, + (config: unknown) => (config as { groupPolicy?: string }).groupPolicy, "allowlist", ], [ "defaults slack.groupPolicy to allowlist when slack section exists", - { channels: { slack: {} } }, - (config: unknown) => - (config as { channels?: { slack?: { groupPolicy?: string } } }).channels?.slack - ?.groupPolicy, + SlackConfigSchema, + {}, + (config: unknown) => (config as { groupPolicy?: string }).groupPolicy, "allowlist", ], [ "defaults msteams.groupPolicy to allowlist when msteams section exists", - { channels: { msteams: {} } }, - (config: unknown) => - (config as { channels?: { msteams?: { groupPolicy?: string } } }).channels?.msteams - ?.groupPolicy, + MSTeamsConfigSchema, + {}, + (config: unknown) => (config as { groupPolicy?: string }).groupPolicy, "allowlist", ], - ])("defaults: %s", (_name, config, readValue, expectedValue) => { - expectValidConfigValue({ config, readValue, expectedValue }); + ])("defaults: %s", (_name, schema, config, readValue, expectedValue) => { + expectSchemaConfigValue({ schema, config, readValue, expectedValue }); }); it("rejects unsafe executable config values", async () => { const res = validateConfigObject({ @@ -207,47 +225,6 @@ describe("legacy config detection", () => { expect(res.issues[0]?.message).toContain('"agent"'); } }); - it("flags channels.telegram.groupMentionsOnly as legacy in snapshot", async () => { - await withSnapshotForConfig( - { channels: { telegram: { groupMentionsOnly: true } } }, - async (ctx) => { - expect(ctx.snapshot.valid).toBe(true); - expect( - ctx.snapshot.legacyIssues.some( - (issue) => issue.path === "channels.telegram.groupMentionsOnly", - ), - ).toBe(true); - const parsed = ctx.parsed as { - channels?: { telegram?: { groupMentionsOnly?: boolean } }; - }; - expect(parsed.channels?.telegram?.groupMentionsOnly).toBe(true); - }, - ); - }); - - it("rejects removed routing.allowFrom in snapshot", async () => { - await withSnapshotForConfig({ routing: { allowFrom: ["+15555550123"] } }, async (ctx) => { - expectSnapshotInvalidRootKey(ctx, "routing"); - }); - }); - it("flags top-level memorySearch as legacy in snapshot", async () => { - await withSnapshotForConfig( - { memorySearch: { provider: "local", fallback: "none" } }, - async (ctx) => { - expect(ctx.snapshot.valid).toBe(true); - expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true); - }, - ); - }); - it("flags top-level heartbeat as legacy in snapshot", async () => { - await withSnapshotForConfig( - { heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" } }, - async (ctx) => { - expect(ctx.snapshot.valid).toBe(true); - expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true); - }, - ); - }); it("rejects removed legacy provider sections in snapshot", async () => { await withSnapshotForConfig({ whatsapp: { allowFrom: ["+1555"] } }, async (ctx) => { expectSnapshotInvalidRootKey(ctx, "whatsapp"); @@ -283,20 +260,6 @@ describe("legacy config detection", () => { expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token"); }); }); - it("still flags memorySearch in snapshot under the shorter support window", async () => { - await withSnapshotForConfig( - { memorySearch: { provider: "local", fallback: "none" } }, - async (ctx) => { - expect(ctx.snapshot.valid).toBe(true); - expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true); - }, - ); - }); - it("rejects removed routing.allowFrom in snapshot with other values", async () => { - await withSnapshotForConfig({ routing: { allowFrom: ["+1666"] } }, async (ctx) => { - expectSnapshotInvalidRootKey(ctx, "routing"); - }); - }); it("rejects bindings[].match.provider on load", async () => { await expectLoadRejectionPreservesField({ config: { diff --git a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts index cb7eba45ac0..37a5ebaa780 100644 --- a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts +++ b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts @@ -1,18 +1,46 @@ import { describe, expect, it } from "vitest"; -import { applyLegacyDoctorMigrations } from "../commands/doctor/shared/legacy-config-migrate.js"; -import type { OpenClawConfig } from "./config.js"; +import { IMessageConfigSchema } from "../../extensions/imessage/config-api.js"; +import { SignalConfigSchema } from "../../extensions/signal/config-api.js"; +import { TelegramConfigSchema } from "../../extensions/telegram/config-api.js"; +import { WhatsAppConfigSchema } from "../../extensions/whatsapp/config-api.js"; +import { findLegacyConfigIssues } from "./legacy.js"; import { validateConfigObject } from "./validation.js"; +import { + DiscordConfigSchema, + MSTeamsConfigSchema, + SlackConfigSchema, +} from "./zod-schema.providers-core.js"; -function getChannelConfig(config: unknown, provider: string) { - const channels = (config as { channels?: Record> } | undefined) - ?.channels; - return channels?.[provider]; +function expectSchemaInvalidIssuePath( + schema: { + safeParse: ( + value: unknown, + ) => + | { success: true } + | { success: false; error: { issues: Array<{ path?: Array }> } }; + }, + config: unknown, + expectedPath: string, +) { + const res = schema.safeParse(config); + expect(res.success).toBe(false); + if (!res.success) { + expect(res.error.issues[0]?.path?.join(".")).toBe(expectedPath); + } } -function expectMigratedConfig(input: unknown, name: string) { - const migrated = applyLegacyDoctorMigrations(input); - expect(migrated.next, name).not.toBeNull(); - return migrated.next as NonNullable; +function expectSchemaConfigValue(params: { + schema: { safeParse: (value: unknown) => { success: true; data: unknown } | { success: false } }; + config: unknown; + readValue: (config: unknown) => unknown; + expectedValue: unknown; +}) { + const res = params.schema.safeParse(params.config); + expect(res.success).toBe(true); + if (!res.success) { + throw new Error("expected schema config to be valid"); + } + expect(params.readValue(res.data)).toBe(params.expectedValue); } describe("legacy config detection", () => { @@ -82,15 +110,10 @@ describe("legacy config detection", () => { } }); it("rejects channels.telegram.groupMentionsOnly", async () => { - const res = validateConfigObject({ + const issues = findLegacyConfigIssues({ channels: { telegram: { groupMentionsOnly: true } }, }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues.some((issue) => issue.path === "channels.telegram.groupMentionsOnly")).toBe( - true, - ); - } + expect(issues.some((issue) => issue.path === "channels.telegram.groupMentionsOnly")).toBe(true); }); it("rejects gateway.token", async () => { const res = validateConfigObject({ @@ -116,265 +139,144 @@ describe("legacy config detection", () => { ); it.each([ { - provider: "telegram", + name: "telegram", + schema: TelegramConfigSchema, allowFrom: ["123456789"], - expectedIssuePath: "channels.telegram.allowFrom", + expectedIssuePath: "allowFrom", }, { - provider: "whatsapp", + name: "whatsapp", + schema: WhatsAppConfigSchema, allowFrom: ["+15555550123"], - expectedIssuePath: "channels.whatsapp.allowFrom", + expectedIssuePath: "allowFrom", }, { - provider: "signal", + name: "signal", + schema: SignalConfigSchema, allowFrom: ["+15555550123"], - expectedIssuePath: "channels.signal.allowFrom", + expectedIssuePath: "allowFrom", }, { - provider: "imessage", + name: "imessage", + schema: IMessageConfigSchema, allowFrom: ["+15555550123"], - expectedIssuePath: "channels.imessage.allowFrom", + expectedIssuePath: "allowFrom", }, ] as const)( - 'enforces dmPolicy="open" allowFrom wildcard for $provider', - ({ provider, allowFrom, expectedIssuePath }) => { + 'enforces dmPolicy="open" allowFrom wildcard for $name', + ({ name, schema, allowFrom, expectedIssuePath }) => { + if (schema) { + expectSchemaInvalidIssuePath(schema, { dmPolicy: "open", allowFrom }, expectedIssuePath); + return; + } const res = validateConfigObject({ channels: { - [provider]: { dmPolicy: "open", allowFrom }, + [name]: { dmPolicy: "open", allowFrom }, }, }); - expect(res.ok, provider).toBe(false); + expect(res.ok, name).toBe(false); if (!res.ok) { - expect(res.issues[0]?.path, provider).toBe(expectedIssuePath); + expect(res.issues[0]?.path, name).toBe(expectedIssuePath); } }, 180_000, ); - it.each(["telegram", "whatsapp", "signal"] as const)( - 'accepts dmPolicy="open" with wildcard for %s', - (provider) => { - const res = validateConfigObject({ - channels: { [provider]: { dmPolicy: "open", allowFrom: ["*"] } }, - }); - expect(res.ok, provider).toBe(true); - if (res.ok) { - const channel = getChannelConfig(res.config, provider); - expect(channel?.dmPolicy, provider).toBe("open"); - } - }, - ); - - it.each(["telegram", "whatsapp", "signal"] as const)( - "defaults dm/group policy for configured provider %s", - (provider) => { - const res = validateConfigObject({ channels: { [provider]: {} } }); - expect(res.ok, provider).toBe(true); - if (res.ok) { - const channel = getChannelConfig(res.config, provider); - expect(channel?.dmPolicy, provider).toBe("pairing"); - expect(channel?.groupPolicy, provider).toBe("allowlist"); - } - }, - ); it.each([ - { - name: "top-level off", - input: { channels: { telegram: { streamMode: "off" } } }, - assert: (config: NonNullable) => { - expect(config.channels?.telegram?.streaming?.mode).toBe("off"); - expect( - (config.channels?.telegram as Record | undefined)?.streamMode, - ).toBeUndefined(); - }, - }, - { - name: "top-level block", - input: { channels: { telegram: { streamMode: "block" } } }, - assert: (config: NonNullable) => { - expect(config.channels?.telegram?.streaming?.mode).toBe("block"); - expect( - (config.channels?.telegram as Record | undefined)?.streamMode, - ).toBeUndefined(); - }, - }, - { - name: "per-account off", - input: { - channels: { - telegram: { - accounts: { - ops: { - streamMode: "off", - }, - }, - }, - }, - }, - assert: (config: NonNullable) => { - expect(config.channels?.telegram?.accounts?.ops?.streaming?.mode).toBe("off"); - expect( - (config.channels?.telegram?.accounts?.ops as Record | undefined) - ?.streamMode, - ).toBeUndefined(); - }, - }, - ] as const)( - "normalizes telegram legacy streamMode alias during migration: $name", - ({ input, assert, name }) => { - assert(expectMigratedConfig(input, name)); - }, - ); - - it.each([ - { - name: "boolean streaming=true", - input: { channels: { discord: { streaming: true } } }, - expectedChanges: [ - "Moved channels.discord.streaming (boolean) → channels.discord.streaming.mode (partial).", - ], - expectedStreaming: "partial", - }, - { - name: "streamMode with streaming boolean", - input: { channels: { discord: { streaming: false, streamMode: "block" } } }, - expectedChanges: [ - "Moved channels.discord.streamMode → channels.discord.streaming.mode (block).", - ], - expectedStreaming: "block", - }, - ] as const)( - "normalizes discord streaming fields during legacy migration: $name", - ({ input, expectedChanges, expectedStreaming, name }) => { - const migrated = applyLegacyDoctorMigrations(input); - for (const expectedChange of expectedChanges) { - expect(migrated.changes, name).toContain(expectedChange); - } - const config = migrated.next as NonNullable | null; - expect(config, name).not.toBeNull(); - expect(config?.channels?.discord?.streaming?.mode, name).toBe(expectedStreaming); - expect( - (config?.channels?.discord as Record | undefined)?.streamMode, - name, - ).toBeUndefined(); - }, - ); - - it.each([ - { - name: "streaming=true", - input: { channels: { discord: { streaming: true } } }, - expectedStreaming: "partial", - }, - { - name: "streaming=false", - input: { channels: { discord: { streaming: false } } }, - expectedStreaming: "off", - }, - { - name: "streamMode overrides streaming boolean", - input: { channels: { discord: { streamMode: "block", streaming: false } } }, - expectedStreaming: "block", - }, - ] as const)( - "rejects legacy discord streaming fields during validation: $name", - ({ input, name }) => { - const res = validateConfigObject(input); - expect(res.ok, name).toBe(false); - if (!res.ok) { - expect(res.issues[0]?.path, name).toBe("channels.discord"); - expect(res.issues[0]?.message, name).toContain( - "channels.discord.streamMode, channels.discord.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy", - ); - } - }, - ); - - it.each([ - { - name: "discord account streaming boolean", - input: { - channels: { - discord: { - accounts: { - work: { - streaming: true, - }, - }, - }, - }, - }, - assert: (config: NonNullable) => { - expect(config.channels?.discord?.accounts?.work?.streaming?.mode).toBe("partial"); - expect( - (config.channels?.discord?.accounts?.work as Record | undefined) - ?.streamMode, - ).toBeUndefined(); - }, - }, - { - name: "slack streamMode alias", - input: { - channels: { - slack: { - streamMode: "status_final", - }, - }, - }, - assert: (config: NonNullable) => { - expect(config.channels?.slack?.streaming?.mode).toBe("progress"); - expect( - (config.channels?.slack as Record | undefined)?.streamMode, - ).toBeUndefined(); - expect(config.channels?.slack?.streaming?.nativeTransport).toBeUndefined(); - }, - }, - { - name: "slack streaming boolean legacy", - input: { - channels: { - slack: { - streaming: false, - }, - }, - }, - assert: (config: NonNullable) => { - expect(config.channels?.slack?.streaming?.mode).toBe("off"); - expect(config.channels?.slack?.streaming?.nativeTransport).toBe(false); - }, - }, - ] as const)( - "normalizes account-level discord/slack streaming alias during migration: $name", - ({ input, assert, name }) => { - assert(expectMigratedConfig(input, name)); - }, - ); - - it("accepts historyLimit overrides per provider and account", async () => { - const res = validateConfigObject({ - messages: { groupChat: { historyLimit: 12 } }, - channels: { - whatsapp: { historyLimit: 9, accounts: { work: { historyLimit: 4 } } }, - telegram: { historyLimit: 8, accounts: { ops: { historyLimit: 3 } } }, - slack: { historyLimit: 7, accounts: { ops: { historyLimit: 2 } } }, - signal: { historyLimit: 6 }, - imessage: { historyLimit: 5 }, - msteams: { historyLimit: 4 }, - discord: { historyLimit: 3 }, - }, + ["telegram", TelegramConfigSchema], + ["whatsapp", WhatsAppConfigSchema], + ["signal", SignalConfigSchema], + ] as const)('accepts dmPolicy="open" with wildcard for %s', (provider, schema) => { + expectSchemaConfigValue({ + schema, + config: { dmPolicy: "open", allowFrom: ["*"] }, + readValue: (config) => (config as { dmPolicy?: string }).dmPolicy, + expectedValue: "open", + }); + }); + + it.each([ + ["telegram", TelegramConfigSchema], + ["whatsapp", WhatsAppConfigSchema], + ["signal", SignalConfigSchema], + ] as const)("defaults dm/group policy for configured provider %s", (provider, schema) => { + expectSchemaConfigValue({ + schema, + config: {}, + readValue: (config) => (config as { dmPolicy?: string }).dmPolicy, + expectedValue: "pairing", + }); + expectSchemaConfigValue({ + schema, + config: {}, + readValue: (config) => (config as { groupPolicy?: string }).groupPolicy, + expectedValue: "allowlist", + }); + }); + it("accepts historyLimit overrides per provider and account", async () => { + expectSchemaConfigValue({ + schema: WhatsAppConfigSchema, + config: { historyLimit: 9, accounts: { work: { historyLimit: 4 } } }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 9, + }); + expectSchemaConfigValue({ + schema: WhatsAppConfigSchema, + config: { historyLimit: 9, accounts: { work: { historyLimit: 4 } } }, + readValue: (config) => + (config as { accounts?: { work?: { historyLimit?: number } } }).accounts?.work + ?.historyLimit, + expectedValue: 4, + }); + expectSchemaConfigValue({ + schema: TelegramConfigSchema, + config: { historyLimit: 8, accounts: { ops: { historyLimit: 3 } } }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 8, + }); + expectSchemaConfigValue({ + schema: TelegramConfigSchema, + config: { historyLimit: 8, accounts: { ops: { historyLimit: 3 } } }, + readValue: (config) => + (config as { accounts?: { ops?: { historyLimit?: number } } }).accounts?.ops?.historyLimit, + expectedValue: 3, + }); + expectSchemaConfigValue({ + schema: SlackConfigSchema, + config: { historyLimit: 7, accounts: { ops: { historyLimit: 2 } } }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 7, + }); + expectSchemaConfigValue({ + schema: SlackConfigSchema, + config: { historyLimit: 7, accounts: { ops: { historyLimit: 2 } } }, + readValue: (config) => + (config as { accounts?: { ops?: { historyLimit?: number } } }).accounts?.ops?.historyLimit, + expectedValue: 2, + }); + expectSchemaConfigValue({ + schema: SignalConfigSchema, + config: { historyLimit: 6 }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 6, + }); + expectSchemaConfigValue({ + schema: IMessageConfigSchema, + config: { historyLimit: 5 }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 5, + }); + expectSchemaConfigValue({ + schema: MSTeamsConfigSchema, + config: { historyLimit: 4 }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 4, + }); + expectSchemaConfigValue({ + schema: DiscordConfigSchema, + config: { historyLimit: 3 }, + readValue: (config) => (config as { historyLimit?: number }).historyLimit, + expectedValue: 3, }); - expect(res.ok).toBe(true); - if (res.ok) { - expect(res.config.channels?.whatsapp?.historyLimit).toBe(9); - expect(res.config.channels?.whatsapp?.accounts?.work?.historyLimit).toBe(4); - expect(res.config.channels?.telegram?.historyLimit).toBe(8); - expect(res.config.channels?.telegram?.accounts?.ops?.historyLimit).toBe(3); - expect(res.config.channels?.slack?.historyLimit).toBe(7); - expect(res.config.channels?.slack?.accounts?.ops?.historyLimit).toBe(2); - expect(res.config.channels?.signal?.historyLimit).toBe(6); - expect(res.config.channels?.imessage?.historyLimit).toBe(5); - expect(res.config.channels?.msteams?.historyLimit).toBe(4); - expect(res.config.channels?.discord?.historyLimit).toBe(3); - } }); }); diff --git a/src/config/config.legacy-config-provider-shapes.test.ts b/src/config/config.legacy-config-provider-shapes.test.ts index 34e3e35ac3e..61dfa9e6ea7 100644 --- a/src/config/config.legacy-config-provider-shapes.test.ts +++ b/src/config/config.legacy-config-provider-shapes.test.ts @@ -35,175 +35,4 @@ describe("legacy provider-shaped config snapshots", () => { }); expect(res.ok).toBe(false); }); - - it("accepts legacy messages.tts provider keys via auto-migration and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - messages: { - tts: { - provider: "elevenlabs", - elevenlabs: { - apiKey: "test-key", - voiceId: "voice-1", - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "messages.tts")).toBe(true); - expect(snap.sourceConfig.messages?.tts).toEqual({ - provider: "elevenlabs", - providers: { - elevenlabs: { - apiKey: "test-key", - voiceId: "voice-1", - }, - }, - }); - expect( - (snap.sourceConfig.messages?.tts as Record | undefined)?.elevenlabs, - ).toBeUndefined(); - }); - }); - - it("accepts legacy talk flat fields via auto-migration and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - talk: { - voiceId: "voice-1", - modelId: "eleven_v3", - apiKey: "test-key", - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "talk")).toBe(true); - expect(snap.sourceConfig.talk?.providers?.elevenlabs).toEqual({ - voiceId: "voice-1", - modelId: "eleven_v3", - apiKey: "test-key", - }); - expect( - (snap.sourceConfig.talk as Record | undefined)?.voiceId, - ).toBeUndefined(); - expect( - (snap.sourceConfig.talk as Record | undefined)?.modelId, - ).toBeUndefined(); - expect( - (snap.sourceConfig.talk as Record | undefined)?.apiKey, - ).toBeUndefined(); - }); - }); - - it("accepts legacy plugins.entries.*.config.tts provider keys via auto-migration", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - plugins: { - entries: { - "voice-call": { - config: { - tts: { - provider: "openai", - openai: { - model: "gpt-4o-mini-tts", - voice: "alloy", - }, - }, - }, - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "plugins.entries")).toBe(true); - const voiceCallTts = ( - snap.sourceConfig.plugins?.entries as - | Record< - string, - { - config?: { - tts?: { - providers?: Record; - openai?: unknown; - }; - }; - } - > - | undefined - )?.["voice-call"]?.config?.tts; - expect(voiceCallTts).toEqual({ - provider: "openai", - providers: { - openai: { - model: "gpt-4o-mini-tts", - voice: "alloy", - }, - }, - }); - expect(voiceCallTts?.openai).toBeUndefined(); - }); - }); - - it("accepts legacy discord voice tts provider keys via auto-migration and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - channels: { - discord: { - voice: { - tts: { - provider: "elevenlabs", - elevenlabs: { - voiceId: "voice-1", - }, - }, - }, - accounts: { - main: { - voice: { - tts: { - edge: { - voice: "en-US-AvaNeural", - }, - }, - }, - }, - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.voice.tts")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.accounts")).toBe( - true, - ); - expect(snap.sourceConfig.channels?.discord?.voice?.tts).toEqual({ - provider: "elevenlabs", - providers: { - elevenlabs: { - voiceId: "voice-1", - }, - }, - }); - expect(snap.sourceConfig.channels?.discord?.accounts?.main?.voice?.tts).toEqual({ - providers: { - microsoft: { - voice: "en-US-AvaNeural", - }, - }, - }); - }); - }); }); diff --git a/src/config/config.legacy-config-snapshot.test.ts b/src/config/config.legacy-config-snapshot.test.ts index fcf2097a678..37aab447d35 100644 --- a/src/config/config.legacy-config-snapshot.test.ts +++ b/src/config/config.legacy-config-snapshot.test.ts @@ -38,366 +38,6 @@ describe("config strict validation", () => { } }); - it("detects top-level memorySearch and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - memorySearch: { - provider: "local", - fallback: "none", - query: { maxResults: 7 }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.memorySearch).toMatchObject({ - provider: "local", - fallback: "none", - query: { maxResults: 7 }, - }); - expect((snap.sourceConfig as { memorySearch?: unknown }).memorySearch).toBeUndefined(); - }); - }); - - it("detects top-level heartbeat agent settings and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - heartbeat: { - every: "30m", - model: "anthropic/claude-3-5-haiku-20241022", - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.heartbeat).toMatchObject({ - every: "30m", - model: "anthropic/claude-3-5-haiku-20241022", - }); - expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined(); - }); - }); - - it("detects top-level heartbeat visibility and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - heartbeat: { - showOk: true, - showAlerts: false, - useIndicator: true, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true); - expect(snap.sourceConfig.channels?.defaults?.heartbeat).toMatchObject({ - showOk: true, - showAlerts: false, - useIndicator: true, - }); - expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined(); - }); - }); - - it("detects legacy sandbox perSession and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - agents: { - defaults: { - sandbox: { - perSession: true, - }, - }, - list: [ - { - id: "pi", - sandbox: { - perSession: false, - }, - }, - ], - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "agents.defaults.sandbox")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "agents.list")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.sandbox).toEqual({ scope: "session" }); - expect(snap.sourceConfig.agents?.list?.[0]?.sandbox).toEqual({ scope: "shared" }); - }); - }); - - it("detects legacy x_search auth and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - tools: { - web: { - x_search: { - apiKey: "test-key", - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "tools.web.x_search.apiKey")).toBe( - true, - ); - expect(snap.sourceConfig.plugins?.entries?.xai?.enabled).toBe(true); - expect(snap.sourceConfig.plugins?.entries?.xai?.config?.webSearch).toMatchObject({ - apiKey: "test-key", - }); - expect( - (snap.sourceConfig.tools?.web?.x_search as Record | undefined)?.apiKey, - ).toBeUndefined(); - }); - }); - - it("detects legacy thread binding ttlHours and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - session: { - threadBindings: { - ttlHours: 24, - }, - }, - channels: { - discord: { - threadBindings: { - ttlHours: 12, - }, - accounts: { - alpha: { - threadBindings: { - ttlHours: 6, - }, - }, - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "session.threadBindings")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels")).toBe(true); - expect(snap.sourceConfig.session?.threadBindings).toMatchObject({ idleHours: 24 }); - expect(snap.sourceConfig.channels?.discord?.threadBindings).toMatchObject({ idleHours: 12 }); - expect(snap.sourceConfig.channels?.discord?.accounts?.alpha?.threadBindings).toMatchObject({ - idleHours: 6, - }); - expect( - (snap.sourceConfig.session?.threadBindings as Record | undefined) - ?.ttlHours, - ).toBeUndefined(); - }); - }); - - it("detects legacy channel streaming aliases and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - channels: { - telegram: { - streamMode: "block", - }, - discord: { - streaming: false, - accounts: { - work: { - streamMode: "block", - }, - }, - }, - googlechat: { - streamMode: "append", - accounts: { - work: { - streamMode: "replace", - }, - }, - }, - slack: { - streaming: true, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.legacyIssues.some((issue) => issue.path === "channels.telegram")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.accounts")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat.accounts")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack")).toBe(true); - expect(snap.sourceConfig.channels?.telegram).toMatchObject({ - streaming: { - mode: "block", - }, - }); - expect( - (snap.sourceConfig.channels?.telegram as Record | undefined)?.streamMode, - ).toBeUndefined(); - expect(snap.sourceConfig.channels?.discord).toMatchObject({ - streaming: { - mode: "off", - }, - }); - expect(snap.sourceConfig.channels?.discord?.accounts?.work).toMatchObject({ - streaming: { - mode: "block", - }, - }); - expect( - (snap.sourceConfig.channels?.googlechat as Record | undefined)?.streamMode, - ).toBeUndefined(); - expect( - ( - snap.sourceConfig.channels?.googlechat?.accounts?.work as - | Record - | undefined - )?.streamMode, - ).toBeUndefined(); - expect(snap.sourceConfig.channels?.slack).toMatchObject({ - streaming: { - mode: "partial", - nativeTransport: true, - }, - }); - }); - }); - - it("detects legacy nested channel allow aliases and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - channels: { - slack: { - channels: { - ops: { - allow: false, - }, - }, - accounts: { - work: { - channels: { - general: { - allow: true, - }, - }, - }, - }, - }, - googlechat: { - groups: { - "spaces/aaa": { - allow: false, - }, - }, - accounts: { - work: { - groups: { - "spaces/bbb": { - allow: true, - }, - }, - }, - }, - }, - discord: { - guilds: { - "100": { - channels: { - general: { - allow: false, - }, - }, - }, - }, - accounts: { - work: { - guilds: { - "200": { - channels: { - help: { - allow: true, - }, - }, - }, - }, - }, - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack.accounts")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat.accounts")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord")).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.accounts")).toBe( - true, - ); - expect(snap.sourceConfig.channels?.slack?.channels?.ops).toMatchObject({ enabled: false }); - expect(snap.sourceConfig.channels?.googlechat?.groups?.["spaces/aaa"]).toMatchObject({ - enabled: false, - }); - expect(snap.sourceConfig.channels?.discord?.guilds?.["100"]?.channels?.general).toMatchObject( - { enabled: false }, - ); - }); - }); - - it("detects telegram groupMentionsOnly and reports legacyIssues", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - channels: { - telegram: { - groupMentionsOnly: true, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect( - snap.legacyIssues.some((issue) => issue.path === "channels.telegram.groupMentionsOnly"), - ).toBe(true); - expect(snap.sourceConfig.channels?.telegram?.groups?.["*"]).toMatchObject({ - requireMention: true, - }); - expect( - (snap.sourceConfig.channels?.telegram as Record | undefined) - ?.groupMentionsOnly, - ).toBeUndefined(); - }); - }); - it("does not treat resolved-only gateway.bind aliases as source-literal legacy or invalid", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { @@ -420,16 +60,4 @@ describe("config strict validation", () => { } }); }); - - it("still marks literal gateway.bind host aliases as legacy", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - gateway: { bind: "0.0.0.0" }, - }); - - const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "gateway.bind")).toBe(true); - }); - }); });