From 7c6eba463476f3cc780c6ebfbe9f9980959f094a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Apr 2026 00:25:53 +0900 Subject: [PATCH] test(config): cover thread binding legacy doctor paths --- src/commands/doctor-config-flow.test.ts | 130 ++++++++++++++++++++++++ src/config/config-misc.test.ts | 61 +++++++++++ 2 files changed, 191 insertions(+) diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 095310f7e4d..2e65d7e1515 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -1430,6 +1430,136 @@ describe("doctor config flow", () => { } }); + it("warns clearly about legacy thread binding ttlHours config and points to doctor --fix", async () => { + const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {}); + try { + await runDoctorConfigWithInput({ + config: { + session: { + threadBindings: { + ttlHours: 24, + }, + }, + channels: { + discord: { + threadBindings: { + ttlHours: 12, + }, + accounts: { + alpha: { + threadBindings: { + ttlHours: 6, + }, + }, + }, + }, + }, + }, + run: loadAndMaybeMigrateDoctorConfig, + }); + + expect( + noteSpy.mock.calls.some( + ([message, title]) => + title === "Legacy config keys detected" && + String(message).includes("session.threadBindings:") && + String(message).includes("session.threadBindings.idleHours"), + ), + ).toBe(true); + expect( + noteSpy.mock.calls.some( + ([message, title]) => + title === "Legacy config keys detected" && + String(message).includes("channels.discord.threadBindings:") && + String(message).includes("channels.discord.threadBindings.idleHours"), + ), + ).toBe(true); + expect( + noteSpy.mock.calls.some( + ([message, title]) => + title === "Legacy config keys detected" && + String(message).includes("channels.discord.accounts:") && + String(message).includes("channels.discord.accounts..threadBindings.idleHours"), + ), + ).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("repairs legacy thread binding ttlHours config on repair", async () => { + const result = await runDoctorConfigWithInput({ + repair: true, + config: { + session: { + threadBindings: { + ttlHours: 24, + }, + }, + channels: { + discord: { + threadBindings: { + ttlHours: 12, + }, + accounts: { + alpha: { + threadBindings: { + ttlHours: 6, + }, + }, + }, + }, + }, + }, + run: loadAndMaybeMigrateDoctorConfig, + }); + + const cfg = result.cfg as { + session?: { + threadBindings?: { + idleHours?: number; + ttlHours?: number; + }; + }; + channels?: { + discord?: { + threadBindings?: { + idleHours?: number; + ttlHours?: number; + }; + accounts?: Record< + string, + { + threadBindings?: { + idleHours?: number; + ttlHours?: number; + }; + } + >; + }; + }; + }; + expect(cfg.session?.threadBindings).toMatchObject({ + idleHours: 24, + }); + expect(cfg.channels?.discord?.threadBindings).toMatchObject({ + idleHours: 12, + }); + expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings).toMatchObject({ + idleHours: 6, + }); + expect(cfg.session?.threadBindings?.ttlHours).toBeUndefined(); + expect(cfg.channels?.discord?.threadBindings?.ttlHours).toBeUndefined(); + expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings?.ttlHours).toBeUndefined(); + }); + it("warns clearly about legacy tts provider config and points to doctor --fix", async () => { const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {}); try { diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 5d1483672fc..455d0811803 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -672,6 +672,67 @@ describe("config strict validation", () => { }); }); + it("accepts legacy thread binding ttlHours via auto-migration 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.discord.threadBindings"), + ).toBe(true); + expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.accounts")).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 | undefined) + ?.ttlHours, + ).toBeUndefined(); + expect( + (snap.sourceConfig.channels?.discord?.threadBindings as Record | undefined) + ?.ttlHours, + ).toBeUndefined(); + expect( + ( + snap.sourceConfig.channels?.discord?.accounts?.alpha?.threadBindings as + | Record + | undefined + )?.ttlHours, + ).toBeUndefined(); + }); + }); + it("accepts legacy channel streaming aliases via auto-migration and reports legacyIssues", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, {