test: update legacy config doctor expectations

This commit is contained in:
Peter Steinberger
2026-04-05 16:12:10 +01:00
parent 97878b853a
commit 37b3acad34
10 changed files with 89 additions and 184 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { applyChannelDoctorCompatibilityMigrations } from "./legacy-config.js";
import { applyChannelDoctorCompatibilityMigrations } from "./channel-legacy-config-migrate.js";
describe("bundled channel legacy config migrations", () => {
it("normalizes legacy private-network aliases exposed through bundled contract surfaces", () => {

View File

@@ -497,7 +497,7 @@ describe("config strict validation", () => {
}
});
it("accepts top-level memorySearch via auto-migration and reports legacyIssues", async () => {
it("rejects top-level memorySearch until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
memorySearch: {
@@ -509,18 +509,17 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
expect(snap.sourceConfig.agents?.defaults?.memorySearch).toMatchObject({
expect((snap.sourceConfig as { memorySearch?: unknown }).memorySearch).toMatchObject({
provider: "local",
fallback: "none",
query: { maxResults: 7 },
});
expect((snap.sourceConfig as { memorySearch?: unknown }).memorySearch).toBeUndefined();
});
});
it("accepts top-level heartbeat agent settings via auto-migration and reports legacyIssues", async () => {
it("rejects top-level heartbeat agent settings until doctor repairs them and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
heartbeat: {
@@ -531,17 +530,16 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true);
expect(snap.sourceConfig.agents?.defaults?.heartbeat).toMatchObject({
expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toMatchObject({
every: "30m",
model: "anthropic/claude-3-5-haiku-20241022",
});
expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined();
});
});
it("accepts top-level heartbeat visibility via auto-migration and reports legacyIssues", async () => {
it("rejects top-level heartbeat visibility until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
heartbeat: {
@@ -553,18 +551,17 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true);
expect(snap.sourceConfig.channels?.defaults?.heartbeat).toMatchObject({
expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toMatchObject({
showOk: true,
showAlerts: false,
useIndicator: true,
});
expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined();
});
});
it("accepts legacy messages.tts provider keys via auto-migration and reports legacyIssues", async () => {
it("rejects legacy messages.tts provider keys until doctor repairs them and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
messages: {
@@ -580,15 +577,15 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "messages.tts")).toBe(true);
expect(snap.sourceConfig.messages?.tts?.providers?.elevenlabs).toEqual({
apiKey: "test-key",
voiceId: "voice-1",
expect(snap.sourceConfig.messages?.tts).toEqual({
provider: "elevenlabs",
elevenlabs: {
apiKey: "test-key",
voiceId: "voice-1",
},
});
expect(
(snap.sourceConfig.messages?.tts as Record<string, unknown> | undefined)?.elevenlabs,
).toBeUndefined();
});
});
@@ -614,7 +611,7 @@ describe("config strict validation", () => {
});
});
it("accepts legacy sandbox perSession via auto-migration and reports legacyIssues", async () => {
it("rejects legacy sandbox perSession until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
agents: {
@@ -636,21 +633,17 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
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",
});
expect(snap.sourceConfig.agents?.defaults?.sandbox).toEqual({ perSession: true });
expect(snap.sourceConfig.agents?.list?.[0]?.sandbox).toEqual({ perSession: false });
});
});
it("accepts legacy x_search auth via auto-migration and reports legacyIssues", async () => {
it("rejects legacy x_search auth until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
tools: {
@@ -664,20 +657,17 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "tools.web.x_search.apiKey")).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();
).toBe("test-key");
});
});
it("accepts legacy thread binding ttlHours via auto-migration and reports legacyIssues", async () => {
it("rejects legacy thread binding ttlHours until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
session: {
@@ -703,37 +693,18 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
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.session?.threadBindings).toMatchObject({ ttlHours: 24 });
expect(snap.sourceConfig.channels?.discord?.threadBindings).toMatchObject({ ttlHours: 12 });
expect(snap.sourceConfig.channels?.discord?.accounts?.alpha?.threadBindings).toMatchObject({
idleHours: 6,
ttlHours: 6,
});
expect(
(snap.sourceConfig.session?.threadBindings as Record<string, unknown> | undefined)
?.ttlHours,
).toBeUndefined();
expect(
(snap.sourceConfig.channels?.discord?.threadBindings as Record<string, unknown> | undefined)
?.ttlHours,
).toBeUndefined();
expect(
(
snap.sourceConfig.channels?.discord?.accounts?.alpha?.threadBindings as
| Record<string, unknown>
| undefined
)?.ttlHours,
).toBeUndefined();
});
});
it("accepts legacy channel streaming aliases via auto-migration and reports legacyIssues", async () => {
it("rejects legacy channel streaming aliases until doctor repairs them and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
channels: {
@@ -764,7 +735,7 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
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(
@@ -775,36 +746,20 @@ describe("config strict validation", () => {
true,
);
expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack")).toBe(true);
expect(snap.sourceConfig.channels?.telegram).toMatchObject({
streaming: "block",
});
expect(
(snap.sourceConfig.channels?.telegram as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
expect(snap.sourceConfig.channels?.discord).toMatchObject({
streaming: "off",
});
expect(snap.sourceConfig.channels?.telegram).toMatchObject({ streamMode: "block" });
expect(snap.sourceConfig.channels?.discord).toMatchObject({ streaming: false });
expect(snap.sourceConfig.channels?.discord?.accounts?.work).toMatchObject({
streaming: "block",
streamMode: "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: "partial",
nativeStreaming: true,
expect(snap.sourceConfig.channels?.googlechat).toMatchObject({ streamMode: "append" });
expect(snap.sourceConfig.channels?.googlechat?.accounts?.work).toMatchObject({
streamMode: "replace",
});
expect(snap.sourceConfig.channels?.slack).toMatchObject({ streaming: true });
});
});
it("accepts legacy nested channel allow aliases via auto-migration and reports legacyIssues", async () => {
it("rejects legacy nested channel allow aliases until doctor repairs them and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
channels: {
@@ -869,7 +824,7 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack")).toBe(true);
expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack.accounts")).toBe(
true,
@@ -882,39 +837,17 @@ describe("config strict validation", () => {
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?.slack?.channels?.ops).toMatchObject({ allow: false });
expect(snap.sourceConfig.channels?.googlechat?.groups?.["spaces/aaa"]).toMatchObject({
enabled: false,
allow: false,
});
expect(snap.sourceConfig.channels?.discord?.guilds?.["100"]?.channels?.general).toMatchObject(
{
enabled: false,
},
{ allow: false },
);
expect(
(snap.sourceConfig.channels?.slack?.channels?.ops as Record<string, unknown> | undefined)
?.allow,
).toBeUndefined();
expect(
(
snap.sourceConfig.channels?.googlechat?.groups?.["spaces/aaa"] as
| Record<string, unknown>
| undefined
)?.allow,
).toBeUndefined();
expect(
(
snap.sourceConfig.channels?.discord?.guilds?.["100"]?.channels?.general as
| Record<string, unknown>
| undefined
)?.allow,
).toBeUndefined();
});
});
it("accepts telegram groupMentionsOnly via auto-migration and reports legacyIssues", async () => {
it("rejects telegram groupMentionsOnly until doctor repairs it and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
channels: {
@@ -926,21 +859,15 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
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();
expect(snap.sourceConfig.channels?.telegram).toMatchObject({ groupMentionsOnly: true });
});
});
it("accepts legacy plugins.entries.*.config.tts provider keys via auto-migration", async () => {
it("rejects legacy plugins.entries.*.config.tts provider keys until doctor repairs them", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
plugins: {
@@ -962,7 +889,7 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "plugins.entries")).toBe(true);
const voiceCallTts = (
snap.sourceConfig.plugins?.entries as
@@ -979,15 +906,17 @@ describe("config strict validation", () => {
>
| undefined
)?.["voice-call"]?.config?.tts;
expect(voiceCallTts?.providers?.openai).toEqual({
model: "gpt-4o-mini-tts",
voice: "alloy",
expect(voiceCallTts).toEqual({
provider: "openai",
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 () => {
it("rejects legacy discord voice tts provider keys until doctor repairs them and reports legacyIssues", async () => {
await withTempHome(async (home) => {
await writeOpenClawConfig(home, {
channels: {
@@ -1017,32 +946,24 @@ describe("config strict validation", () => {
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(true);
expect(snap.valid).toBe(false);
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?.providers?.elevenlabs).toEqual({
voiceId: "voice-1",
expect(snap.sourceConfig.channels?.discord?.voice?.tts).toEqual({
provider: "elevenlabs",
elevenlabs: {
voiceId: "voice-1",
},
});
expect(
snap.sourceConfig.channels?.discord?.accounts?.main?.voice?.tts?.providers?.microsoft,
).toEqual({
voice: "en-US-AvaNeural",
expect(snap.sourceConfig.channels?.discord?.accounts?.main?.voice?.tts).toEqual({
edge: {
voice: "en-US-AvaNeural",
},
});
expect(
(snap.sourceConfig.channels?.discord?.voice?.tts as Record<string, unknown> | undefined)
?.elevenlabs,
).toBeUndefined();
expect(
(
snap.sourceConfig.channels?.discord?.accounts?.main?.voice?.tts as
| Record<string, unknown>
| undefined
)?.edge,
).toBeUndefined();
});
});

View File

@@ -2,10 +2,15 @@ import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
const { loadConfig, migrateLegacyConfig, readConfigFileSnapshot, validateConfigObject } =
const { loadConfig, readConfigFileSnapshot, validateConfigObject } =
await vi.importActual<typeof import("./config.js")>("./config.js");
const { migrateLegacyConfig: migrateLegacyConfigFromDoctor } = await vi.importActual<
typeof import("../commands/doctor/shared/legacy-config-migrate.js")
>("../commands/doctor/shared/legacy-config-migrate.js");
import { withTempHome } from "./test-helpers.js";
const migrateLegacyConfig = migrateLegacyConfigFromDoctor;
async function expectLoadRejectionPreservesField(params: {
config: unknown;
readValue: (parsed: unknown) => unknown;
@@ -219,7 +224,7 @@ describe("legacy config detection", () => {
await withSnapshotForConfig(
{ channels: { telegram: { groupMentionsOnly: true } } },
async (ctx) => {
expect(ctx.snapshot.valid).toBe(true);
expect(ctx.snapshot.valid).toBe(false);
expect(
ctx.snapshot.legacyIssues.some(
(issue) => issue.path === "channels.telegram.groupMentionsOnly",
@@ -263,7 +268,7 @@ describe("legacy config detection", () => {
await withSnapshotForConfig(
{ memorySearch: { provider: "local", fallback: "none" } },
async (ctx) => {
expect(ctx.snapshot.valid).toBe(true);
expect(ctx.snapshot.valid).toBe(false);
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
},
);
@@ -272,7 +277,7 @@ describe("legacy config detection", () => {
await withSnapshotForConfig(
{ heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" } },
async (ctx) => {
expect(ctx.snapshot.valid).toBe(true);
expect(ctx.snapshot.valid).toBe(false);
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true);
},
);
@@ -316,7 +321,7 @@ describe("legacy config detection", () => {
await withSnapshotForConfig(
{ memorySearch: { provider: "local", fallback: "none" } },
async (ctx) => {
expect(ctx.snapshot.valid).toBe(true);
expect(ctx.snapshot.valid).toBe(false);
expect(ctx.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true);
},
);

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import { migrateLegacyConfig } from "../commands/doctor/shared/legacy-config-migrate.js";
import type { OpenClawConfig } from "./types.js";
import { validateConfigObject } from "./validation.js";

View File

@@ -389,7 +389,7 @@ describe("web search provider config", () => {
expect(res.ok).toBe(false);
});
it("accepts legacy scoped provider config for bundled providers via auto-migration", () => {
it("rejects legacy scoped provider config for bundled providers until doctor repairs it", () => {
const res = validateConfigObjectWithPlugins({
tools: {
web: {
@@ -403,21 +403,7 @@ describe("web search provider config", () => {
},
});
expect(res.ok).toBe(true);
if (!res.ok) {
return;
}
expect(res.config.tools?.web?.search).toEqual({
provider: "gemini",
});
expect(res.config.plugins?.entries?.google).toEqual({
enabled: true,
config: {
webSearch: {
apiKey: "legacy-key",
},
},
});
expect(res.ok).toBe(false);
});
it("accepts gemini provider with no extra config", () => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import { migrateLegacyConfig } from "../commands/doctor/shared/legacy-config-migrate.js";
import {
validateConfigObjectRawWithPlugins,
validateConfigObjectWithPlugins,

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "../commands/doctor/shared/legacy-config-migrate.js";
import type { OpenClawConfig } from "./config.js";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import {
listLegacyWebSearchConfigPaths,
migrateLegacyWebSearchConfig,
@@ -108,7 +108,7 @@ describe("legacy web search config", () => {
{
path: "tools.web.search",
message:
"tools.web.search provider-owned config moved to plugins.entries.<plugin>.config.webSearch (auto-migrated on load).",
'tools.web.search provider-owned config moved to plugins.entries.<plugin>.config.webSearch. Run "openclaw doctor --fix".',
},
]);

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import { migrateLegacyConfig } from "../commands/doctor/shared/legacy-config-migrate.js";
import { validateConfigObjectRaw } from "./validation.js";
describe("thread binding config keys", () => {

View File

@@ -3117,7 +3117,7 @@ describe("secrets runtime snapshot", () => {
}
});
it("migrates legacy x_search SecretRefs into the xai plugin webSearch auth at runtime", async () => {
it("keeps legacy x_search SecretRefs in place until doctor repairs them", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
tools: {
@@ -3138,17 +3138,14 @@ describe("secrets runtime snapshot", () => {
});
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
apiKey: "xai-runtime-key",
enabled: true,
model: "grok-4-1-fast",
});
expect(snapshot.config.plugins?.entries?.xai?.config).toEqual({
webSearch: {
apiKey: "xai-runtime-key",
},
});
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
});
it("still migrates legacy x_search auth when general legacy migration returns an invalid config", async () => {
it("still resolves legacy x_search auth in place even when unrelated legacy config is present", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: asConfig({
tools: {
@@ -3174,13 +3171,10 @@ describe("secrets runtime snapshot", () => {
});
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
apiKey: "xai-runtime-key-invalid-config",
enabled: true,
});
expect(snapshot.config.plugins?.entries?.xai?.config).toEqual({
webSearch: {
apiKey: "xai-runtime-key-invalid-config",
},
});
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
});
it("does not force-enable xai at runtime for knob-only x_search config", async () => {

View File

@@ -19,7 +19,6 @@ export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi
registerService() {},
registerCliBackend() {},
registerConfigMigration() {},
registerLegacyConfigMigration() {},
registerAutoEnableProbe() {},
registerProvider() {},
registerSpeechProvider() {},