diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index ff47639873c..7a4356da86c 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -600,6 +600,36 @@ describe("doctor config flow", () => { expectGoogleChatDmAllowFromRepaired(result.cfg); }); + it("migrates top-level heartbeat into agents.defaults.heartbeat on repair", async () => { + const result = await runDoctorConfigWithInput({ + repair: true, + config: { + heartbeat: { + model: "anthropic/claude-3-5-haiku-20241022", + every: "30m", + }, + }, + run: loadAndMaybeMigrateDoctorConfig, + }); + + const cfg = result.cfg as { + heartbeat?: unknown; + agents?: { + defaults?: { + heartbeat?: { + model?: string; + every?: string; + }; + }; + }; + }; + expect(cfg.heartbeat).toBeUndefined(); + expect(cfg.agents?.defaults?.heartbeat).toMatchObject({ + model: "anthropic/claude-3-5-haiku-20241022", + every: "30m", + }); + }); + it("repairs googlechat account dm.policy open by setting dm.allowFrom on repair", async () => { const result = await runDoctorConfigWithInput({ repair: true, diff --git a/src/gateway/server.legacy-migration.test.ts b/src/gateway/server.legacy-migration.test.ts index f24501ebfa1..6e306e8a2d0 100644 --- a/src/gateway/server.legacy-migration.test.ts +++ b/src/gateway/server.legacy-migration.test.ts @@ -43,4 +43,41 @@ describe("gateway startup legacy migration fallback", () => { ); expect(message).not.toContain("Legacy config entries detected but auto-migration failed."); }); + + test("keeps detailed validation errors when heartbeat comes from include-resolved config", async () => { + testState.legacyIssues = [ + { + path: "heartbeat", + message: + "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat instead.", + }, + ]; + // Simulate a parsed source that only contains include directives, while + // legacy heartbeat is surfaced from the resolved config. + testState.legacyParsed = { + $include: ["heartbeat.defaults.json"], + }; + testState.migrationConfig = null; + testState.migrationChanges = []; + + let server: Awaited> | undefined; + let thrown: unknown; + try { + server = await startGatewayServer(await getFreePort()); + } catch (err) { + thrown = err; + } + + if (server) { + await server.close(); + } + + expect(thrown).toBeInstanceOf(Error); + const message = String((thrown as Error).message); + expect(message).toContain("Invalid config at"); + expect(message).toContain( + "heartbeat: top-level heartbeat is not a valid config path; use agents.defaults.heartbeat instead.", + ); + expect(message).not.toContain("Legacy config entries detected but auto-migration failed."); + }); });