diff --git a/assistant/src/__tests__/config-schema.test.ts b/assistant/src/__tests__/config-schema.test.ts index 20482c4fb58..b174f1cc6f1 100644 --- a/assistant/src/__tests__/config-schema.test.ts +++ b/assistant/src/__tests__/config-schema.test.ts @@ -2073,6 +2073,37 @@ describe("loadConfig with schema validation", () => { expect(config.calls.provider).toBe("twilio"); }); + test("recovers from partial filing.activeHours without wiping unrelated fields", () => { + // Only activeHoursStart is set. The superRefine must emit the issue so + // the loader's delete-and-retry can strip the set field; otherwise the + // mismatch persists and the config falls back to full defaults (which + // would reset maxTokens below to 64000). + writeConfig({ + maxTokens: 4096, + filing: { activeHoursStart: 8 }, + }); + const config = loadConfig(); + expect(config.maxTokens).toBe(4096); + expect(config.filing.activeHoursStart).toBeNull(); + expect(config.filing.activeHoursEnd).toBeNull(); + }); + + test("recovers from partial heartbeat.activeHours without wiping unrelated fields", () => { + // activeHoursStart is explicitly nulled while activeHoursEnd defaults to + // 22 — a mismatch. Without emitting on both paths, delete-and-retry can't + // resolve this (deleting the null side is a no-op) and the whole config + // would be reset to defaults. + writeConfig({ + maxTokens: 4096, + heartbeat: { activeHoursStart: null }, + }); + const config = loadConfig(); + expect(config.maxTokens).toBe(4096); + // Both fall back to the heartbeat defaults (8, 22). + expect(config.heartbeat.activeHoursStart).toBe(8); + expect(config.heartbeat.activeHoursEnd).toBe(22); + }); + test("applies calls defaults when not specified", () => { writeConfig({}); const config = loadConfig(); diff --git a/assistant/src/config/schemas/filing.ts b/assistant/src/config/schemas/filing.ts index 335a28116f2..b0282a7ec8d 100644 --- a/assistant/src/config/schemas/filing.ts +++ b/assistant/src/config/schemas/filing.ts @@ -47,11 +47,20 @@ export const FilingConfigSchema = z const startNull = config.activeHoursStart == null; const endNull = config.activeHoursEnd == null; if (startNull !== endNull) { + // Emit on both fields so validateWithSchema's delete-and-retry repair + // can strip whichever side was set (and no-op the null side), letting + // the config fall back to both-null defaults without a full reset. + const message = + "filing.activeHoursStart and filing.activeHoursEnd must both be set or both be null"; ctx.addIssue({ code: z.ZodIssueCode.custom, - path: [startNull ? "activeHoursStart" : "activeHoursEnd"], - message: - "filing.activeHoursStart and filing.activeHoursEnd must both be set or both be null", + path: ["activeHoursStart"], + message, + }); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["activeHoursEnd"], + message, }); return; } diff --git a/assistant/src/config/schemas/heartbeat.ts b/assistant/src/config/schemas/heartbeat.ts index 528b0278a2e..efc26775e52 100644 --- a/assistant/src/config/schemas/heartbeat.ts +++ b/assistant/src/config/schemas/heartbeat.ts @@ -43,11 +43,20 @@ export const HeartbeatConfigSchema = z const startNull = config.activeHoursStart == null; const endNull = config.activeHoursEnd == null; if (startNull !== endNull) { + // Emit on both fields so validateWithSchema's delete-and-retry repair + // can strip whichever side was set (and no-op the null side), letting + // the config fall back to the schema defaults without a full reset. + const message = + "heartbeat.activeHoursStart and heartbeat.activeHoursEnd must both be set or both be null"; ctx.addIssue({ code: z.ZodIssueCode.custom, - path: [startNull ? "activeHoursStart" : "activeHoursEnd"], - message: - "heartbeat.activeHoursStart and heartbeat.activeHoursEnd must both be set or both be null", + path: ["activeHoursStart"], + message, + }); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["activeHoursEnd"], + message, }); return; }