mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 00:31:22 +00:00
fix(config): surface legacy channel streaming aliases (#60358)
This commit is contained in:
@@ -652,6 +652,62 @@ describe("doctor config flow", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("warns clearly about legacy channel streaming aliases and points to doctor --fix", async () => {
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
await runDoctorConfigWithInput({
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
streamMode: "block",
|
||||
},
|
||||
discord: {
|
||||
streaming: false,
|
||||
},
|
||||
slack: {
|
||||
streaming: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.telegram:") &&
|
||||
String(message).includes("channels.telegram.streamMode is legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.discord:") &&
|
||||
String(message).includes("boolean channels.discord.streaming are legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.slack:") &&
|
||||
String(message).includes("boolean channels.slack.streaming are legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Doctor" &&
|
||||
String(message).includes('Run "openclaw doctor --fix" to migrate legacy config keys.'),
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
noteSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("sanitizes config-derived doctor warnings and changes before logging", async () => {
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
|
||||
@@ -591,6 +591,55 @@ describe("config strict validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts legacy channel streaming aliases via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
channels: {
|
||||
telegram: {
|
||||
streamMode: "block",
|
||||
},
|
||||
discord: {
|
||||
streaming: false,
|
||||
accounts: {
|
||||
work: {
|
||||
streamMode: "block",
|
||||
},
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
streaming: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(true);
|
||||
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.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?.discord?.accounts?.work).toMatchObject({
|
||||
streaming: "block",
|
||||
});
|
||||
expect(snap.sourceConfig.channels?.slack).toMatchObject({
|
||||
streaming: "partial",
|
||||
nativeStreaming: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts legacy plugins.entries.*.config.tts provider keys via auto-migration", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
|
||||
@@ -482,6 +482,32 @@ describe("legacy migrate sandbox scope aliases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate channel streaming aliases", () => {
|
||||
it("migrates telegram and discord streaming aliases", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
channels: {
|
||||
telegram: {
|
||||
streamMode: "block",
|
||||
},
|
||||
discord: {
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.telegram.streamMode → channels.telegram.streaming (block).",
|
||||
);
|
||||
expect(res.changes).toContain("Normalized channels.discord.streaming boolean → enum (off).");
|
||||
expect(res.config?.channels?.telegram).toMatchObject({
|
||||
streaming: "block",
|
||||
});
|
||||
expect(res.config?.channels?.discord).toMatchObject({
|
||||
streaming: "off",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate x_search auth", () => {
|
||||
it("moves only legacy x_search auth into plugin-owned xai config", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
|
||||
@@ -61,6 +61,41 @@ function migrateThreadBindingsTtlHoursForPath(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasLegacyTelegramStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined;
|
||||
}
|
||||
|
||||
function hasLegacyDiscordStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined || typeof entry.streaming === "boolean";
|
||||
}
|
||||
|
||||
function hasLegacySlackStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined || typeof entry.streaming === "boolean";
|
||||
}
|
||||
|
||||
function hasLegacyStreamingKeysInAccounts(
|
||||
value: unknown,
|
||||
matchEntry: (entry: Record<string, unknown>) => boolean,
|
||||
): boolean {
|
||||
const accounts = getRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((entry) => matchEntry(getRecord(entry) ?? {}));
|
||||
}
|
||||
|
||||
const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["session", "threadBindings"],
|
||||
@@ -82,6 +117,45 @@ const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const CHANNEL_STREAMING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "telegram"],
|
||||
message:
|
||||
"channels.telegram.streamMode is legacy; use channels.telegram.streaming instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyTelegramStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "telegram", "accounts"],
|
||||
message:
|
||||
"channels.telegram.accounts.<id>.streamMode is legacy; use channels.telegram.accounts.<id>.streaming instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyStreamingKeysInAccounts(value, hasLegacyTelegramStreamingKeys),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord"],
|
||||
message:
|
||||
"channels.discord.streamMode and boolean channels.discord.streaming are legacy; use channels.discord.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyDiscordStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "accounts"],
|
||||
message:
|
||||
"channels.discord.accounts.<id>.streamMode and boolean channels.discord.accounts.<id>.streaming are legacy; use channels.discord.accounts.<id>.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyStreamingKeysInAccounts(value, hasLegacyDiscordStreamingKeys),
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack"],
|
||||
message:
|
||||
"channels.slack.streamMode and boolean channels.slack.streaming are legacy; use channels.slack.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacySlackStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack", "accounts"],
|
||||
message:
|
||||
"channels.slack.accounts.<id>.streamMode and boolean channels.slack.accounts.<id>.streaming are legacy; use channels.slack.accounts.<id>.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyStreamingKeysInAccounts(value, hasLegacySlackStreamingKeys),
|
||||
},
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "thread-bindings.ttlHours->idleHours",
|
||||
@@ -136,6 +210,7 @@ export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
||||
id: "channels.streaming-keys->channels.streaming",
|
||||
describe:
|
||||
"Normalize legacy streaming keys to channels.<provider>.streaming (Telegram/Discord/Slack)",
|
||||
legacyRules: CHANNEL_STREAMING_RULES,
|
||||
apply: (raw, changes) => {
|
||||
const channels = getRecord(raw.channels);
|
||||
if (!channels) {
|
||||
|
||||
Reference in New Issue
Block a user