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", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
"updated": "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": [ "files_modified": [
"src/cli/setup/config.ts", "src/cli/setup/config.ts",
"src/cli/setup/config.test.ts", "src/cli/setup/config.test.ts",
@@ -3499,7 +3499,7 @@
} }
}, },
"overall_progress": { "overall_progress": {
"total_test_count": 1869, "total_test_count": 1870,
"all_tests_passing": true, "all_tests_passing": true,
"p0_completion": "3/3 (100%)", "p0_completion": "3/3 (100%)",
"p1_completion": "4/4 (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 () => { it('recovery notification sent when checks pass after failures', async () => {
const mockSend = vi.fn().mockResolvedValue(undefined); const mockSend = vi.fn().mockResolvedValue(undefined);
const mockGet = vi.fn().mockReturnValue({ send: mockSend }); const mockGet = vi.fn().mockReturnValue({ send: mockSend });
+12 -2
View File
@@ -485,8 +485,18 @@ export class HeartbeatMonitor {
return Date.now() - lastAt >= cooldownMs; 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> { 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 signatureChanged = signature !== this.lastFailureSignature;
const cooldownPassed = this.shouldNotifyByCooldown(this.lastFailureNotificationAt, cooldownMs); const cooldownPassed = this.shouldNotifyByCooldown(this.lastFailureNotificationAt, cooldownMs);
if (!signatureChanged && !cooldownPassed) { if (!signatureChanged && !cooldownPassed) {
@@ -500,7 +510,7 @@ export class HeartbeatMonitor {
} }
private async notifyRecoveryWithCooldown(text: string): Promise<boolean> { 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); const cooldownPassed = this.shouldNotifyByCooldown(this.lastRecoveryNotificationAt, cooldownMs);
if (!cooldownPassed) { if (!cooldownPassed) {
return false; return false;