fix(heartbeat): fallback on invalid notify_cooldown values
This commit is contained in:
@@ -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%)",
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user