fix(heartbeat): fallback on invalid notify_cooldown values

This commit is contained in:
William Valentin
2026-02-16 15:24:12 -08:00
parent ab80f305ef
commit 7ce58f5966
3 changed files with 38 additions and 4 deletions
+2 -2
View File
@@ -193,7 +193,7 @@
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Implemented operator-focused hardening and onboarding polish: added setup Operator Pack flows in both Automation menu and first-run wizard to preconfigure scheduled backups, heartbeat alerts, daily briefing, and default MinIO sync; added operator-pack prompts for output channel/peer routing; added heartbeat notification throttling via `automation.heartbeat.notify_cooldown`; and added `flynn doctor --strict` to treat warnings as failures. Updated docs/default config examples and onboarding tests accordingly.",
"summary": "Implemented operator-focused hardening and onboarding polish: added setup Operator Pack flows in both Automation menu and first-run wizard to preconfigure scheduled backups, heartbeat alerts, daily briefing, and default MinIO sync; added operator-pack prompts for output channel/peer routing; added heartbeat notification throttling via `automation.heartbeat.notify_cooldown` (with invalid-value fallback handling); and added `flynn doctor --strict` to treat warnings as failures. Updated docs/default config examples and onboarding tests accordingly.",
"files_modified": [
"src/cli/setup/config.ts",
"src/cli/setup/config.test.ts",
@@ -3499,7 +3499,7 @@
}
},
"overall_progress": {
"total_test_count": 1869,
"total_test_count": 1870,
"all_tests_passing": true,
"p0_completion": "3/3 (100%)",
"p1_completion": "4/4 (100%)",
+24
View File
@@ -307,6 +307,30 @@ describe('HeartbeatMonitor', () => {
}));
});
it('falls back to default cooldown when notify_cooldown is invalid', async () => {
const mockSend = vi.fn().mockResolvedValue(undefined);
const mockGet = vi.fn().mockReturnValue({ send: mockSend });
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const deps = makeDeps({
config: makeConfig({
checks: ['model'],
failure_threshold: 1,
notify_cooldown: 'not-a-duration',
notify: { channel: 'telegram', peer: '123' },
}),
modelRouter: undefined,
channelLookup: { get: mockGet },
});
monitor = new HeartbeatMonitor(deps);
await monitor.runChecks();
expect(mockSend).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid notify_cooldown'));
warnSpy.mockRestore();
});
it('recovery notification sent when checks pass after failures', async () => {
const mockSend = vi.fn().mockResolvedValue(undefined);
const mockGet = vi.fn().mockReturnValue({ send: mockSend });
+12 -2
View File
@@ -485,8 +485,18 @@ export class HeartbeatMonitor {
return Date.now() - lastAt >= cooldownMs;
}
private getNotifyCooldownMs(): number {
const raw = this.deps.config.notify_cooldown ?? '30m';
try {
return parseInterval(raw);
} catch {
console.warn(`HeartbeatMonitor: invalid notify_cooldown '${raw}', falling back to 30m`);
return parseInterval('30m');
}
}
private async notifyFailureWithCooldown(text: string, signature: string): Promise<boolean> {
const cooldownMs = parseInterval(this.deps.config.notify_cooldown ?? '30m');
const cooldownMs = this.getNotifyCooldownMs();
const signatureChanged = signature !== this.lastFailureSignature;
const cooldownPassed = this.shouldNotifyByCooldown(this.lastFailureNotificationAt, cooldownMs);
if (!signatureChanged && !cooldownPassed) {
@@ -500,7 +510,7 @@ export class HeartbeatMonitor {
}
private async notifyRecoveryWithCooldown(text: string): Promise<boolean> {
const cooldownMs = parseInterval(this.deps.config.notify_cooldown ?? '30m');
const cooldownMs = this.getNotifyCooldownMs();
const cooldownPassed = this.shouldNotifyByCooldown(this.lastRecoveryNotificationAt, cooldownMs);
if (!cooldownPassed) {
return false;