From 2f4cf2d67d6be192833e59d14e7063fc2e5d4ee0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 21:46:38 +0100 Subject: [PATCH] test(extensions): move channel config schema coverage --- .../discord/src/config-schema.test.ts | 102 ++++++++++- .../signal/src/config-schema.test.ts | 8 +- .../telegram/src/config-schema.test.ts | 166 +++++++++++++++++- ...nel-webhook-and-actions.validation.test.ts | 128 -------------- .../config.discord-agent-components.test.ts | 48 ----- src/config/config.discord-presence.test.ts | 56 ------ .../config.telegram-custom-commands.test.ts | 42 ----- 7 files changed, 261 insertions(+), 289 deletions(-) rename src/config/config.discord.test.ts => extensions/discord/src/config-schema.test.ts (51%) rename src/config/zod-schema.signal-groups.test.ts => extensions/signal/src/config-schema.test.ts (82%) rename src/config/config.telegram-topic-agentid.test.ts => extensions/telegram/src/config-schema.test.ts (50%) delete mode 100644 src/config/channel-webhook-and-actions.validation.test.ts delete mode 100644 src/config/config.discord-agent-components.test.ts delete mode 100644 src/config/config.discord-presence.test.ts delete mode 100644 src/config/config.telegram-custom-commands.test.ts diff --git a/src/config/config.discord.test.ts b/extensions/discord/src/config-schema.test.ts similarity index 51% rename from src/config/config.discord.test.ts rename to extensions/discord/src/config-schema.test.ts index f49d711871a..97aa418153e 100644 --- a/src/config/config.discord.test.ts +++ b/extensions/discord/src/config-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { DiscordConfigSchema } from "./zod-schema.providers-core.js"; +import { DiscordConfigSchema } from "../config-api.js"; function expectValidDiscordConfig(config: unknown) { const res = DiscordConfigSchema.safeParse(config); @@ -19,8 +19,8 @@ function expectInvalidDiscordConfig(config: unknown) { return res.error.issues; } -describe("config discord", () => { - it("loads discord guild map + dm group settings", () => { +describe("discord config schema", () => { + it("loads guild map and dm group settings", () => { const cfg = expectValidDiscordConfig({ enabled: true, dm: { @@ -57,7 +57,7 @@ describe("config discord", () => { expect(cfg.guilds?.["123"]?.channels?.general?.autoThread).toBe(true); }); - it("coerces safe-integer numeric discord allowlist entries to strings", () => { + it("coerces safe-integer numeric allowlist entries to strings", () => { const cfg = expectValidDiscordConfig({ allowFrom: [123], dm: { allowFrom: [456], groupChannels: [789] }, @@ -83,7 +83,7 @@ describe("config discord", () => { expect(cfg.execApprovals?.approvers).toEqual(["555"]); }); - it("rejects numeric discord IDs that are not valid non-negative safe integers", () => { + it("rejects numeric IDs that are not valid non-negative safe integers", () => { const cases = [106232522769186816, -1, 123.45]; for (const id of cases) { const issues = expectInvalidDiscordConfig({ allowFrom: [id] }); @@ -93,4 +93,96 @@ describe("config discord", () => { ).toBe(true); } }); + + it.each([ + { name: "status-only presence", config: { status: "idle" } }, + { + name: "custom activity when type is omitted", + config: { activity: "Focus time" }, + }, + { + name: "custom activity type", + config: { activity: "Chilling", activityType: 4 }, + }, + { + name: "auto presence config", + config: { + autoPresence: { + enabled: true, + intervalMs: 30000, + minUpdateIntervalMs: 15000, + exhaustedText: "token exhausted", + }, + }, + }, + ] as const)("accepts $name", ({ config }) => { + expect(DiscordConfigSchema.safeParse(config).success).toBe(true); + }); + + it.each([ + { + name: "streaming activity without url", + config: { activity: "Live", activityType: 1 }, + }, + { + name: "activityUrl without streaming type", + config: { activity: "Live", activityUrl: "https://twitch.tv/openclaw" }, + }, + { + name: "auto presence min update interval above check interval", + config: { + autoPresence: { + enabled: true, + intervalMs: 5000, + minUpdateIntervalMs: 6000, + }, + }, + }, + ] as const)("rejects $name", ({ config }) => { + expect(DiscordConfigSchema.safeParse(config).success).toBe(false); + }); + + it("accepts agentComponents.enabled at channel scope", () => { + const res = DiscordConfigSchema.safeParse({ + agentComponents: { + enabled: true, + }, + }); + + expect(res.success).toBe(true); + }); + + it("accepts agentComponents.enabled at account scope", () => { + const res = DiscordConfigSchema.safeParse({ + accounts: { + work: { + agentComponents: { + enabled: false, + }, + }, + }, + }); + + expect(res.success).toBe(true); + }); + + it("rejects unknown fields under agentComponents", () => { + const res = DiscordConfigSchema.safeParse({ + agentComponents: { + enabled: true, + invalidField: true, + }, + }); + + expect(res.success).toBe(false); + if (!res.success) { + expect( + res.error.issues.some( + (issue) => + issue.path.join(".") === "agentComponents" && + issue.message.toLowerCase().includes("unrecognized"), + ), + ).toBe(true); + } + }); }); diff --git a/src/config/zod-schema.signal-groups.test.ts b/extensions/signal/src/config-schema.test.ts similarity index 82% rename from src/config/zod-schema.signal-groups.test.ts rename to extensions/signal/src/config-schema.test.ts index e30f0fd1211..393a85cee24 100644 --- a/src/config/zod-schema.signal-groups.test.ts +++ b/extensions/signal/src/config-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { SignalConfigSchema } from "./zod-schema.providers-core.js"; +import { SignalConfigSchema } from "../config-api.js"; function expectValidSignalConfig(config: unknown) { const res = SignalConfigSchema.safeParse(config); @@ -16,7 +16,7 @@ function expectInvalidSignalConfig(config: unknown) { } describe("signal groups schema", () => { - it("accepts top-level Signal groups overrides", () => { + it("accepts top-level group overrides", () => { expectValidSignalConfig({ groups: { "*": { @@ -29,7 +29,7 @@ describe("signal groups schema", () => { }); }); - it("accepts per-account Signal groups overrides", () => { + it("accepts per-account group overrides", () => { expectValidSignalConfig({ accounts: { primary: { @@ -43,7 +43,7 @@ describe("signal groups schema", () => { }); }); - it("rejects unknown keys in Signal groups entries", () => { + it("rejects unknown keys in group entries", () => { const issues = expectInvalidSignalConfig({ groups: { "*": { diff --git a/src/config/config.telegram-topic-agentid.test.ts b/extensions/telegram/src/config-schema.test.ts similarity index 50% rename from src/config/config.telegram-topic-agentid.test.ts rename to extensions/telegram/src/config-schema.test.ts index fd77f970daf..456412ac6bf 100644 --- a/src/config/config.telegram-topic-agentid.test.ts +++ b/extensions/telegram/src/config-schema.test.ts @@ -1,5 +1,47 @@ import { describe, expect, it } from "vitest"; -import { TelegramConfigSchema } from "./zod-schema.providers-core.js"; +import { TelegramConfigSchema } from "../config-api.js"; + +function expectTelegramConfigValid(config: unknown) { + expect(TelegramConfigSchema.safeParse(config).success).toBe(true); +} + +function expectTelegramConfigIssue(config: unknown, path: string) { + const res = TelegramConfigSchema.safeParse(config); + expect(res.success).toBe(false); + if (!res.success) { + expect(res.error.issues[0]?.path.join(".")).toBe(path); + } +} + +describe("telegram custom commands schema", () => { + it("normalizes custom commands", () => { + const res = TelegramConfigSchema.safeParse({ + customCommands: [{ command: "/Backup", description: " Git backup " }], + }); + + expect(res.success).toBe(true); + if (!res.success) { + return; + } + + expect(res.data.customCommands).toEqual([{ command: "backup", description: "Git backup" }]); + }); + + it("normalizes hyphens in custom command names", () => { + const res = TelegramConfigSchema.safeParse({ + customCommands: [{ command: "Bad-Name", description: "Override status" }], + }); + + expect(res.success).toBe(true); + if (!res.success) { + return; + } + + expect(res.data.customCommands).toEqual([ + { command: "bad_name", description: "Override status" }, + ]); + }); +}); describe("telegram topic agentId schema", () => { it("accepts valid agentId in forum group topic config", () => { @@ -45,7 +87,7 @@ describe("telegram topic agentId schema", () => { expect(res.data.direct?.["123456789"]?.topics?.["99"]?.agentId).toBe("support"); }); - it("accepts empty config without agentId (backward compatible)", () => { + it("accepts empty config without agentId", () => { const res = TelegramConfigSchema.safeParse({ groups: { "-1001234567890": { @@ -92,7 +134,7 @@ describe("telegram topic agentId schema", () => { expect(topics?.["5"]?.agentId).toBe("q"); }); - it("rejects unknown fields in topic config (strict schema)", () => { + it("rejects unknown fields in topic config", () => { const res = TelegramConfigSchema.safeParse({ groups: { "-1001234567890": { @@ -147,8 +189,10 @@ describe("telegram disableAudioPreflight schema", () => { expect(res.success).toBe(false); }); +}); - it("accepts telegram botToken without tokenFile", () => { +describe("telegram token schema", () => { + it("accepts botToken without tokenFile", () => { const res = TelegramConfigSchema.safeParse({ botToken: "123:ABC", }); @@ -162,7 +206,7 @@ describe("telegram disableAudioPreflight schema", () => { expect(res.data.tokenFile).toBeUndefined(); }); - it("accepts telegram tokenFile without botToken", () => { + it("accepts tokenFile without botToken", () => { const res = TelegramConfigSchema.safeParse({ tokenFile: "/run/agenix/telegram-token", }); @@ -176,7 +220,7 @@ describe("telegram disableAudioPreflight schema", () => { expect(res.data.botToken).toBeUndefined(); }); - it("accepts telegram botToken and tokenFile together", () => { + it("accepts botToken and tokenFile together", () => { const res = TelegramConfigSchema.safeParse({ botToken: "fallback:token", tokenFile: "/run/agenix/telegram-token", @@ -191,3 +235,113 @@ describe("telegram disableAudioPreflight schema", () => { expect(res.data.tokenFile).toBe("/run/agenix/telegram-token"); }); }); + +describe("telegram poll actions schema", () => { + it("accepts actions.poll", () => { + expectTelegramConfigValid({ actions: { poll: false } }); + }); + + it("accepts account actions.poll", () => { + expectTelegramConfigValid({ accounts: { ops: { actions: { poll: false } } } }); + }); +}); + +describe("telegram webhook schema", () => { + it("accepts a positive webhookPort", () => { + expectTelegramConfigValid({ + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + webhookPort: 8787, + }); + }); + + it("accepts webhookPort set to 0 for ephemeral port binding", () => { + expectTelegramConfigValid({ + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + webhookPort: 0, + }); + }); + + it("rejects negative webhookPort", () => { + expectTelegramConfigIssue( + { + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + webhookPort: -1, + }, + "webhookPort", + ); + }); + + it.each([ + { + name: "webhookUrl when webhookSecret is configured", + config: { + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + }, + }, + { + name: "webhookUrl when webhookSecret is configured as SecretRef", + config: { + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: { + source: "env", + provider: "default", + id: "TELEGRAM_WEBHOOK_SECRET", + }, + }, + }, + { + name: "account webhookUrl when base webhookSecret is configured", + config: { + webhookSecret: "secret", + accounts: { + ops: { + webhookUrl: "https://example.com/telegram-webhook", + }, + }, + }, + }, + { + name: "account webhookUrl when account webhookSecret is configured as SecretRef", + config: { + accounts: { + ops: { + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: { + source: "env", + provider: "default", + id: "TELEGRAM_OPS_WEBHOOK_SECRET", + }, + }, + }, + }, + }, + ] as const)("accepts $name", ({ config }) => { + expectTelegramConfigValid(config); + }); + + it("rejects webhookUrl without webhookSecret", () => { + expectTelegramConfigIssue( + { + webhookUrl: "https://example.com/telegram-webhook", + }, + "webhookSecret", + ); + }); + + it("rejects account webhookUrl without webhookSecret", () => { + expectTelegramConfigIssue( + { + accounts: { + ops: { + webhookUrl: "https://example.com/telegram-webhook", + }, + }, + }, + "accounts.ops.webhookSecret", + ); + }); +}); diff --git a/src/config/channel-webhook-and-actions.validation.test.ts b/src/config/channel-webhook-and-actions.validation.test.ts deleted file mode 100644 index f7c364f4b56..00000000000 --- a/src/config/channel-webhook-and-actions.validation.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { TelegramConfigSchema } from "./zod-schema.providers-core.js"; - -function expectTelegramConfigValid(config: unknown) { - expect(TelegramConfigSchema.safeParse(config).success).toBe(true); -} - -function expectTelegramConfigIssue(config: unknown, path: string) { - const res = TelegramConfigSchema.safeParse(config); - expect(res.success).toBe(false); - if (!res.success) { - expect(res.error.issues[0]?.path.join(".")).toBe(path); - } -} - -describe("channel webhook and actions validation", () => { - describe("Telegram poll actions", () => { - it("accepts channels.telegram.actions.poll", () => { - expectTelegramConfigValid({ actions: { poll: false } }); - }); - - it("accepts channels.telegram.accounts..actions.poll", () => { - expectTelegramConfigValid({ accounts: { ops: { actions: { poll: false } } } }); - }); - }); - - describe("Telegram webhookPort", () => { - it("accepts a positive webhookPort", () => { - expectTelegramConfigValid({ - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: "secret", - webhookPort: 8787, - }); - }); - - it("accepts webhookPort set to 0 for ephemeral port binding", () => { - expectTelegramConfigValid({ - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: "secret", - webhookPort: 0, - }); - }); - - it("rejects negative webhookPort", () => { - expectTelegramConfigIssue( - { - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: "secret", - webhookPort: -1, - }, - "webhookPort", - ); - }); - }); - - describe("Telegram webhook secret", () => { - it.each([ - { - name: "webhookUrl when webhookSecret is configured", - config: { - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: "secret", - }, - }, - { - name: "webhookUrl when webhookSecret is configured as SecretRef", - config: { - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: { - source: "env", - provider: "default", - id: "TELEGRAM_WEBHOOK_SECRET", - }, - }, - }, - { - name: "account webhookUrl when base webhookSecret is configured", - config: { - webhookSecret: "secret", - accounts: { - ops: { - webhookUrl: "https://example.com/telegram-webhook", - }, - }, - }, - }, - { - name: "account webhookUrl when account webhookSecret is configured as SecretRef", - config: { - accounts: { - ops: { - webhookUrl: "https://example.com/telegram-webhook", - webhookSecret: { - source: "env", - provider: "default", - id: "TELEGRAM_OPS_WEBHOOK_SECRET", - }, - }, - }, - }, - }, - ] as const)("accepts $name", ({ config }) => { - expectTelegramConfigValid(config); - }); - - it("rejects webhookUrl without webhookSecret", () => { - expectTelegramConfigIssue( - { - webhookUrl: "https://example.com/telegram-webhook", - }, - "webhookSecret", - ); - }); - - it("rejects account webhookUrl without webhookSecret", () => { - expectTelegramConfigIssue( - { - accounts: { - ops: { - webhookUrl: "https://example.com/telegram-webhook", - }, - }, - }, - "accounts.ops.webhookSecret", - ); - }); - }); -}); diff --git a/src/config/config.discord-agent-components.test.ts b/src/config/config.discord-agent-components.test.ts deleted file mode 100644 index e78b6587cf8..00000000000 --- a/src/config/config.discord-agent-components.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { DiscordConfigSchema } from "./zod-schema.providers-core.js"; - -describe("discord agentComponents config", () => { - it("accepts channels.discord.agentComponents.enabled", () => { - const res = DiscordConfigSchema.safeParse({ - agentComponents: { - enabled: true, - }, - }); - - expect(res.success).toBe(true); - }); - - it("accepts channels.discord.accounts..agentComponents.enabled", () => { - const res = DiscordConfigSchema.safeParse({ - accounts: { - work: { - agentComponents: { - enabled: false, - }, - }, - }, - }); - - expect(res.success).toBe(true); - }); - - it("rejects unknown fields under channels.discord.agentComponents", () => { - const res = DiscordConfigSchema.safeParse({ - agentComponents: { - enabled: true, - invalidField: true, - }, - }); - - expect(res.success).toBe(false); - if (!res.success) { - expect( - res.error.issues.some( - (issue) => - issue.path.join(".") === "agentComponents" && - issue.message.toLowerCase().includes("unrecognized"), - ), - ).toBe(true); - } - }); -}); diff --git a/src/config/config.discord-presence.test.ts b/src/config/config.discord-presence.test.ts deleted file mode 100644 index ca6b62e6a48..00000000000 --- a/src/config/config.discord-presence.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { DiscordConfigSchema } from "./zod-schema.providers-core.js"; - -describe("config discord presence", () => { - it.each([ - { name: "status-only presence", config: { discord: { status: "idle" } } }, - { - name: "custom activity when type is omitted", - config: { discord: { activity: "Focus time" } }, - }, - { - name: "custom activity type", - config: { discord: { activity: "Chilling", activityType: 4 } }, - }, - { - name: "auto presence config", - config: { - discord: { - autoPresence: { - enabled: true, - intervalMs: 30000, - minUpdateIntervalMs: 15000, - exhaustedText: "token exhausted", - }, - }, - }, - }, - ] as const)("accepts $name", ({ config }) => { - expect(DiscordConfigSchema.safeParse(config.discord).success).toBe(true); - }); - - it.each([ - { - name: "streaming activity without url", - config: { discord: { activity: "Live", activityType: 1 } }, - }, - { - name: "activityUrl without streaming type", - config: { discord: { activity: "Live", activityUrl: "https://twitch.tv/openclaw" } }, - }, - { - name: "auto presence min update interval above check interval", - config: { - discord: { - autoPresence: { - enabled: true, - intervalMs: 5000, - minUpdateIntervalMs: 6000, - }, - }, - }, - }, - ] as const)("rejects $name", ({ config }) => { - expect(DiscordConfigSchema.safeParse(config.discord).success).toBe(false); - }); -}); diff --git a/src/config/config.telegram-custom-commands.test.ts b/src/config/config.telegram-custom-commands.test.ts deleted file mode 100644 index 27ff0450220..00000000000 --- a/src/config/config.telegram-custom-commands.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { OpenClawSchema } from "./zod-schema.js"; - -describe("telegram custom commands schema", () => { - it("normalizes custom commands", () => { - const res = OpenClawSchema.safeParse({ - channels: { - telegram: { - customCommands: [{ command: "/Backup", description: " Git backup " }], - }, - }, - }); - - expect(res.success).toBe(true); - if (!res.success) { - return; - } - - expect(res.data.channels?.telegram?.customCommands).toEqual([ - { command: "backup", description: "Git backup" }, - ]); - }); - - it("normalizes hyphens in custom command names", () => { - const res = OpenClawSchema.safeParse({ - channels: { - telegram: { - customCommands: [{ command: "Bad-Name", description: "Override status" }], - }, - }, - }); - - expect(res.success).toBe(true); - if (!res.success) { - return; - } - - expect(res.data.channels?.telegram?.customCommands).toEqual([ - { command: "bad_name", description: "Override status" }, - ]); - }); -});