Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions assistant/src/__tests__/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2090,9 +2090,8 @@ describe("loadConfig with schema validation", () => {

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.
// 22 — a mismatch. Single-emit on the null side lets delete-and-retry
// strip activeHoursStart, after which the default (8) restores it.
writeConfig({
maxTokens: 4096,
heartbeat: { activeHoursStart: null },
Expand All @@ -2104,6 +2103,37 @@ describe("loadConfig with schema validation", () => {
expect(config.heartbeat.activeHoursEnd).toBe(22);
});

test("partial heartbeat.activeHours preserves the explicit non-null value", () => {
// User sets activeHoursEnd: 20 and nulls activeHoursStart. Single-emit
// on the null side strips activeHoursStart only — default 8 restores
// it, user's explicit 20 survives. Dual-emit would strip both and lose
// the 20.
writeConfig({
maxTokens: 1234,
heartbeat: { activeHoursStart: null, activeHoursEnd: 20 },
});
const config = loadConfig();
expect(config.maxTokens).toBe(1234);
expect(config.heartbeat.activeHoursStart).toBe(8);
expect(config.heartbeat.activeHoursEnd).toBe(20);
});

test("recovers from equal filing.activeHours without wiping unrelated fields", () => {
// activeHoursStart === activeHoursEnd is invalid (empty window). Filing's
// defaults are null/null, so single-emit on one path would strip one side
// and the null default would recreate a mismatch — cascading to a full
// defaults reset that wipes maxTokens. Dual-emit strips both sides so
// both defaults restore to null.
writeConfig({
maxTokens: 1234,
filing: { activeHoursStart: 5, activeHoursEnd: 5 },
});
const config = loadConfig();
expect(config.maxTokens).toBe(1234);
expect(config.filing.activeHoursStart).toBeNull();
expect(config.filing.activeHoursEnd).toBeNull();
});

test("applies calls defaults when not specified", () => {
writeConfig({});
const config = loadConfig();
Expand Down
14 changes: 12 additions & 2 deletions assistant/src/config/schemas/filing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,21 @@ export const FilingConfigSchema = z
config.activeHoursEnd != null &&
config.activeHoursStart === config.activeHoursEnd
) {
// Emit on both fields. Filing's defaults are null/null, so single-emit
// on one side would cascade: delete-and-retry strips one key, the null
// default recreates a new mismatch, and the loader falls back to full
// defaults — wiping unrelated fields like maxTokens.
const message =
"filing.activeHoursStart and filing.activeHoursEnd must not be equal (would create an empty window)";
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHoursStart"],
message,
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHoursEnd"],
message:
"filing.activeHoursStart and filing.activeHoursEnd must not be equal (would create an empty window)",
message,
});
}
});
Expand Down
19 changes: 7 additions & 12 deletions assistant/src/config/schemas/heartbeat.ts
Comment thread
siddseethepalli marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,15 @@ 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";
// Emit on the null side only. Heartbeat's defaults are 8/22, so
// delete-and-retry strips the null key, letting the non-null default
// restore it while preserving the user's explicit value on the other
// side.
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHoursStart"],
message,
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHoursEnd"],
message,
path: [startNull ? "activeHoursStart" : "activeHoursEnd"],
Comment thread
siddseethepalli marked this conversation as resolved.
message:
"heartbeat.activeHoursStart and heartbeat.activeHoursEnd must both be set or both be null",
});
return;
}
Expand Down
Loading