feat(companion): add heartbeat success observability state

This commit is contained in:
William Valentin
2026-02-16 19:37:43 -08:00
parent 6dccef94a6
commit 369250077a
4 changed files with 52 additions and 1 deletions
+20
View File
@@ -96,6 +96,24 @@ describe('CompanionHeartbeatLoop', () => {
loop.stop();
});
it('tracks successCount and lastSuccessAt in loop state', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 300 });
expect(loop.successCount).toBe(0);
expect(loop.lastSuccessAt).toBeNull();
loop.start();
await Promise.resolve();
expect(loop.successCount).toBe(1);
expect(loop.lastSuccessAt).not.toBeNull();
expect(loop.getState().successCount).toBe(1);
expect(loop.getState().lastSuccessAt).toBe(loop.lastSuccessAt);
loop.stop();
});
it('passes buildHeartbeat payload into publishHeartbeat', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const buildHeartbeat = vi.fn(() => ({ statusText: 'loop', powerSource: 'ac' as const }));
@@ -193,6 +211,8 @@ describe('CompanionHeartbeatLoop', () => {
expect(loop.running).toBe(false);
expect(loop.getState()).toEqual({
running: false,
successCount: 1,
lastSuccessAt: expect.any(Number),
failureCount: 0,
lastFailure: null,
});
+18
View File
@@ -18,6 +18,8 @@ export interface CompanionHeartbeatLoopOptions {
export interface CompanionHeartbeatLoopState {
running: boolean;
successCount: number;
lastSuccessAt: number | null;
failureCount: number;
lastFailure: Error | null;
}
@@ -40,6 +42,8 @@ export class CompanionHeartbeatLoop {
private timer: NodeJS.Timeout | null = null;
private started = false;
private inFlight = false;
private successfulHeartbeats = 0;
private lastSuccessAtMs: number | null = null;
private consecutiveFailures = 0;
private lastError: Error | null = null;
@@ -79,6 +83,14 @@ export class CompanionHeartbeatLoop {
return this.consecutiveFailures;
}
get successCount(): number {
return this.successfulHeartbeats;
}
get lastSuccessAt(): number | null {
return this.lastSuccessAtMs;
}
get lastFailure(): Error | null {
return this.lastError;
}
@@ -86,6 +98,8 @@ export class CompanionHeartbeatLoop {
getState(): CompanionHeartbeatLoopState {
return {
running: this.running,
successCount: this.successCount,
lastSuccessAt: this.lastSuccessAt,
failureCount: this.failureCount,
lastFailure: this.lastFailure,
};
@@ -96,6 +110,8 @@ export class CompanionHeartbeatLoop {
return;
}
this.started = true;
this.successfulHeartbeats = 0;
this.lastSuccessAtMs = null;
this.consecutiveFailures = 0;
this.lastError = null;
@@ -147,6 +163,8 @@ export class CompanionHeartbeatLoop {
try {
const payload = this.buildHeartbeat ? await this.buildHeartbeat() : undefined;
const result = await this.publisher.publishHeartbeat(payload);
this.successfulHeartbeats += 1;
this.lastSuccessAtMs = Date.now();
this.consecutiveFailures = 0;
this.lastError = null;
this.onSuccess?.(result);