From 89173ea9c2beda223d7e05a3db26a4ed27ffcf67 Mon Sep 17 00:00:00 2001 From: metaforismo <39646696+metaforismo@users.noreply.github.com> Date: Fri, 7 Aug 2026 03:51:29 +0200 Subject: [PATCH] fix(heartbeat): explain target-none skips (#119689) Co-authored-by: Patrick Erichsen --- src/infra/heartbeat-events.test.ts | 31 +++++++++++++++++++ src/infra/heartbeat-events.ts | 12 ++++++- .../gateway-rpc-automation.e2e.test.ts | 3 ++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/infra/heartbeat-events.test.ts b/src/infra/heartbeat-events.test.ts index baa0553c8bb2..3e11b91a993b 100644 --- a/src/infra/heartbeat-events.test.ts +++ b/src/infra/heartbeat-events.test.ts @@ -48,6 +48,37 @@ describe("heartbeat events", () => { }); }); + it("adds a delivery-disabled message to target-none events without changing the reason", () => { + const listener = vi.fn(); + const unsubscribe = onHeartbeatEvent(listener); + + emitHeartbeatEvent({ status: "skipped", reason: "target-none" }); + + const expected = { + ts: 1767960000000, + status: "skipped", + reason: "target-none", + message: "Heartbeat delivery is disabled by configuration (target: none).", + }; + expect(getLastHeartbeatEvent()).toEqual(expected); + expect(listener).toHaveBeenCalledWith(expected); + + unsubscribe(); + }); + + it("preserves an explicit message for target-none events", () => { + emitHeartbeatEvent({ + status: "skipped", + reason: "target-none", + message: "custom diagnostic", + }); + + expect(getLastHeartbeatEvent()).toMatchObject({ + reason: "target-none", + message: "custom diagnostic", + }); + }); + it("delivers events to listeners, isolates listener failures, and supports unsubscribe", () => { const seen: string[] = []; const unsubscribeFirst = onHeartbeatEvent((evt) => { diff --git a/src/infra/heartbeat-events.ts b/src/infra/heartbeat-events.ts index 9961c2f77ca0..b82a39eae8e6 100644 --- a/src/infra/heartbeat-events.ts +++ b/src/infra/heartbeat-events.ts @@ -4,6 +4,8 @@ import { notifyListeners, registerListener } from "../shared/listeners.js"; export type HeartbeatIndicatorType = "ok" | "alert" | "error"; +const TARGET_NONE_MESSAGE = "Heartbeat delivery is disabled by configuration (target: none)."; + export type HeartbeatEventPayload = { ts: number; status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed"; @@ -13,6 +15,8 @@ export type HeartbeatEventPayload = { durationMs?: number; hasMedia?: boolean; reason?: string; + /** Operator-facing companion to the machine-stable reason code. */ + message?: string; /** The channel this heartbeat was sent to. */ channel?: string; /** Whether the message was silently suppressed (showOk: false). */ @@ -51,7 +55,13 @@ const state = resolveGlobalSingleton(HEARTBEAT_EVENT_STATE_ })); export function emitHeartbeatEvent(evt: Omit) { - const enriched: HeartbeatEventPayload = { ts: Date.now(), ...evt }; + const enriched: HeartbeatEventPayload = { + ts: Date.now(), + ...evt, + ...(evt.reason === "target-none" && evt.message === undefined + ? { message: TARGET_NONE_MESSAGE } + : {}), + }; state.lastHeartbeat = enriched; notifyListeners(state.listeners, enriched); } diff --git a/test/e2e/qa-lab/runtime/gateway-rpc-automation.e2e.test.ts b/test/e2e/qa-lab/runtime/gateway-rpc-automation.e2e.test.ts index b890d7e5d8de..a9750d53cfea 100644 --- a/test/e2e/qa-lab/runtime/gateway-rpc-automation.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/gateway-rpc-automation.e2e.test.ts @@ -406,12 +406,15 @@ describe("Gateway task and automation RPCs", () => { ts: number; status: string; reason?: string; + message?: string; preview?: string; }>("last-heartbeat", {}); return ( lastHeartbeat.ts >= wakeRequestedAt && lastHeartbeat.status === "skipped" && lastHeartbeat.reason === "target-none" && + lastHeartbeat.message === + "Heartbeat delivery is disabled by configuration (target: none)." && lastHeartbeat.preview === `Heartbeat handled: ${wakeText}` ); },