fix(config): strip legacy googlechat streamMode on load

This commit is contained in:
Vincent Koc
2026-04-04 16:26:28 +09:00
parent 09997f032f
commit 9ea37202a8
4 changed files with 170 additions and 0 deletions

View File

@@ -664,6 +664,9 @@ describe("doctor config flow", () => {
discord: {
streaming: false,
},
googlechat: {
streamMode: "append",
},
slack: {
streaming: true,
},
@@ -688,6 +691,14 @@ describe("doctor config flow", () => {
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.googlechat:") &&
String(message).includes("channels.googlechat.streamMode is legacy and no longer used"),
),
).toBe(true);
expect(
noteSpy.mock.calls.some(
([message, title]) =>
@@ -708,6 +719,36 @@ describe("doctor config flow", () => {
}
});
it("repairs legacy googlechat streamMode by removing it", async () => {
const result = await runDoctorConfigWithInput({
config: {
channels: {
googlechat: {
streamMode: "append",
accounts: {
work: {
streamMode: "replace",
},
},
},
},
},
run: loadAndMaybeMigrateDoctorConfig,
});
const cfg = result.cfg as {
channels: {
googlechat: {
accounts?: {
work?: Record<string, unknown>;
};
} & Record<string, unknown>;
};
};
expect(cfg.channels.googlechat.streamMode).toBeUndefined();
expect(cfg.channels.googlechat.accounts?.work?.streamMode).toBeUndefined();
});
it("warns clearly about legacy nested channel allow aliases and points to doctor --fix", async () => {
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
try {

View File

@@ -757,6 +757,14 @@ describe("config strict validation", () => {
},
},
},
googlechat: {
streamMode: "append",
accounts: {
work: {
streamMode: "replace",
},
},
},
slack: {
streaming: true,
},
@@ -771,6 +779,10 @@ describe("config strict validation", () => {
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: "block",
@@ -784,6 +796,16 @@ describe("config strict validation", () => {
expect(snap.sourceConfig.channels?.discord?.accounts?.work).toMatchObject({
streaming: "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,

View File

@@ -499,6 +499,48 @@ describe("legacy migrate channel streaming aliases", () => {
streaming: "off",
});
});
it("removes legacy googlechat streamMode aliases", () => {
const raw = {
channels: {
googlechat: {
streamMode: "append",
accounts: {
work: {
streamMode: "replace",
},
},
},
},
};
const validated = validateConfigObjectWithPlugins(raw);
expect(validated.ok).toBe(true);
if (!validated.ok) {
return;
}
expect(
(validated.config.channels?.googlechat as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
expect(
(
validated.config.channels?.googlechat?.accounts?.work as Record<string, unknown> | undefined
)?.streamMode,
).toBeUndefined();
const res = migrateLegacyConfig(raw);
expect(res.changes).toContain("Removed channels.googlechat.streamMode (legacy key no longer used).");
expect(res.changes).toContain(
"Removed channels.googlechat.accounts.work.streamMode (legacy key no longer used).",
);
expect(
(res.config?.channels?.googlechat as Record<string, unknown> | undefined)?.streamMode,
).toBeUndefined();
expect(
(res.config?.channels?.googlechat?.accounts?.work as Record<string, unknown> | undefined)
?.streamMode,
).toBeUndefined();
});
});
describe("legacy migrate nested channel enabled aliases", () => {

View File

@@ -242,6 +242,14 @@ function hasLegacySlackStreamingKeys(value: unknown): boolean {
return entry.streamMode !== undefined || typeof entry.streaming === "boolean";
}
function hasLegacyGoogleChatStreamMode(value: unknown): boolean {
const entry = getRecord(value);
if (!entry) {
return false;
}
return entry.streamMode !== undefined;
}
function hasLegacyKeysInAccounts(
value: unknown,
matchEntry: (entry: Record<string, unknown>) => boolean,
@@ -409,6 +417,21 @@ const CHANNEL_ENABLED_ALIAS_RULES: LegacyConfigRule[] = [
},
];
const GOOGLECHAT_STREAMMODE_RULES: LegacyConfigRule[] = [
{
path: ["channels", "googlechat"],
message:
"channels.googlechat.streamMode is legacy and no longer used; it is removed on load.",
match: (value) => hasLegacyGoogleChatStreamMode(value),
},
{
path: ["channels", "googlechat", "accounts"],
message:
"channels.googlechat.accounts.<id>.streamMode is legacy and no longer used; it is removed on load.",
match: (value) => hasLegacyKeysInAccounts(value, hasLegacyGoogleChatStreamMode),
},
];
export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
defineLegacyConfigMigration({
id: "thread-bindings.ttlHours->idleHours",
@@ -683,4 +706,46 @@ export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
raw.channels = channels;
},
}),
defineLegacyConfigMigration({
id: "channels.googlechat.streamMode->remove",
describe: "Remove legacy Google Chat streamMode keys that are no longer used",
legacyRules: GOOGLECHAT_STREAMMODE_RULES,
apply: (raw, changes) => {
const channels = getRecord(raw.channels);
if (!channels) {
return;
}
const migrateEntry = (entry: Record<string, unknown>, pathPrefix: string) => {
if (entry.streamMode === undefined) {
return;
}
delete entry.streamMode;
changes.push(`Removed ${pathPrefix}.streamMode (legacy key no longer used).`);
};
const googlechat = getRecord(channels.googlechat);
if (!googlechat) {
return;
}
migrateEntry(googlechat, "channels.googlechat");
const accounts = getRecord(googlechat.accounts);
if (accounts) {
for (const [accountId, accountValue] of Object.entries(accounts)) {
const account = getRecord(accountValue);
if (!account) {
continue;
}
migrateEntry(account, `channels.googlechat.accounts.${accountId}`);
accounts[accountId] = account;
}
googlechat.accounts = accounts;
}
channels.googlechat = googlechat;
raw.channels = channels;
},
}),
];