test: speed up legacy config tests

This commit is contained in:
Peter Steinberger
2026-04-07 12:42:21 +01:00
parent c83db77629
commit 00e902a60b
4 changed files with 200 additions and 878 deletions

View File

@@ -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<typeof import("./config.js")>("./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: {

View File

@@ -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<string, Record<string, unknown>> } | undefined)
?.channels;
return channels?.[provider];
function expectSchemaInvalidIssuePath(
schema: {
safeParse: (
value: unknown,
) =>
| { success: true }
| { success: false; error: { issues: Array<{ path?: Array<string | number> }> } };
},
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<OpenClawConfig>;
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<OpenClawConfig>) => {
expect(config.channels?.telegram?.streaming?.mode).toBe("off");
expect(
(config.channels?.telegram as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
},
},
{
name: "top-level block",
input: { channels: { telegram: { streamMode: "block" } } },
assert: (config: NonNullable<OpenClawConfig>) => {
expect(config.channels?.telegram?.streaming?.mode).toBe("block");
expect(
(config.channels?.telegram as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
},
},
{
name: "per-account off",
input: {
channels: {
telegram: {
accounts: {
ops: {
streamMode: "off",
},
},
},
},
},
assert: (config: NonNullable<OpenClawConfig>) => {
expect(config.channels?.telegram?.accounts?.ops?.streaming?.mode).toBe("off");
expect(
(config.channels?.telegram?.accounts?.ops as Record<string, unknown> | 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<OpenClawConfig> | null;
expect(config, name).not.toBeNull();
expect(config?.channels?.discord?.streaming?.mode, name).toBe(expectedStreaming);
expect(
(config?.channels?.discord as Record<string, unknown> | 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<OpenClawConfig>) => {
expect(config.channels?.discord?.accounts?.work?.streaming?.mode).toBe("partial");
expect(
(config.channels?.discord?.accounts?.work as Record<string, unknown> | undefined)
?.streamMode,
).toBeUndefined();
},
},
{
name: "slack streamMode alias",
input: {
channels: {
slack: {
streamMode: "status_final",
},
},
},
assert: (config: NonNullable<OpenClawConfig>) => {
expect(config.channels?.slack?.streaming?.mode).toBe("progress");
expect(
(config.channels?.slack as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
expect(config.channels?.slack?.streaming?.nativeTransport).toBeUndefined();
},
},
{
name: "slack streaming boolean legacy",
input: {
channels: {
slack: {
streaming: false,
},
},
},
assert: (config: NonNullable<OpenClawConfig>) => {
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);
}
});
});

View File

@@ -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<string, unknown> | 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<string, unknown> | undefined)?.voiceId,
).toBeUndefined();
expect(
(snap.sourceConfig.talk as Record<string, unknown> | undefined)?.modelId,
).toBeUndefined();
expect(
(snap.sourceConfig.talk as Record<string, unknown> | 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<string, unknown>;
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",
},
},
});
});
});
});

View File

@@ -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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
expect(
(
snap.sourceConfig.channels?.googlechat?.accounts?.work as
| Record<string, unknown>
| 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<string, unknown> | 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);
});
});
});