Gateway/TUI: filter heartbeat ACK noise in chat events

This commit is contained in:
Vignesh Natarajan
2026-02-20 20:23:17 -08:00
parent 1ded4c672a
commit 2227840989
3 changed files with 196 additions and 17 deletions

View File

@@ -1,12 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadConfig } from "../config/config.js";
import { registerAgentRunContext, resetAgentRunContextForTest } from "../infra/agent-events.js";
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
import {
createAgentEventHandler,
createChatRunState,
createToolEventRecipientRegistry,
} from "./server-chat.js";
vi.mock("../config/config.js", () => ({
loadConfig: vi.fn(() => ({})),
}));
vi.mock("../infra/heartbeat-visibility.js", () => ({
resolveHeartbeatVisibility: vi.fn(() => ({
showOk: false,
showAlerts: true,
useIndicator: true,
})),
}));
describe("agent event handler", () => {
beforeEach(() => {
vi.mocked(loadConfig).mockReturnValue({});
vi.mocked(resolveHeartbeatVisibility).mockReturnValue({
showOk: false,
showAlerts: true,
useIndicator: true,
});
resetAgentRunContextForTest();
});
afterEach(() => {
resetAgentRunContextForTest();
});
function createHarness(params?: {
now?: number;
resolveSessionKeyForRun?: (runId: string) => string | undefined;
@@ -393,4 +421,93 @@ describe("agent event handler", () => {
expect(payload.runId).toBe("run-tool-client");
resetAgentRunContextForTest();
});
it("suppresses heartbeat ack-like chat output when showOk is false", () => {
const { broadcast, nodeSendToSession, chatRunState, handler } = createHarness({
now: 2_000,
});
chatRunState.registry.add("run-heartbeat", {
sessionKey: "session-heartbeat",
clientRunId: "client-heartbeat",
});
registerAgentRunContext("run-heartbeat", {
sessionKey: "session-heartbeat",
isHeartbeat: true,
verboseLevel: "off",
});
handler({
runId: "run-heartbeat",
seq: 1,
stream: "assistant",
ts: Date.now(),
data: {
text: "HEARTBEAT_OK Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.",
},
});
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
expect(sessionChatCalls(nodeSendToSession)).toHaveLength(0);
handler({
runId: "run-heartbeat",
seq: 2,
stream: "lifecycle",
ts: Date.now(),
data: { phase: "end" },
});
const chatCalls = chatBroadcastCalls(broadcast);
expect(chatCalls).toHaveLength(1);
const finalPayload = chatCalls[0]?.[1] as { state?: string; message?: unknown };
expect(finalPayload.state).toBe("final");
expect(finalPayload.message).toBeUndefined();
expect(sessionChatCalls(nodeSendToSession)).toHaveLength(1);
});
it("keeps heartbeat alert text in final chat output when remainder exceeds ackMaxChars", () => {
vi.mocked(loadConfig).mockReturnValue({
agents: { defaults: { heartbeat: { ackMaxChars: 10 } } },
});
const { broadcast, chatRunState, handler } = createHarness({ now: 3_000 });
chatRunState.registry.add("run-heartbeat-alert", {
sessionKey: "session-heartbeat-alert",
clientRunId: "client-heartbeat-alert",
});
registerAgentRunContext("run-heartbeat-alert", {
sessionKey: "session-heartbeat-alert",
isHeartbeat: true,
verboseLevel: "off",
});
handler({
runId: "run-heartbeat-alert",
seq: 1,
stream: "assistant",
ts: Date.now(),
data: {
text: "HEARTBEAT_OK Disk usage crossed 95 percent on /data and needs cleanup now.",
},
});
handler({
runId: "run-heartbeat-alert",
seq: 2,
stream: "lifecycle",
ts: Date.now(),
data: { phase: "end" },
});
const chatCalls = chatBroadcastCalls(broadcast);
expect(chatCalls).toHaveLength(1);
const payload = chatCalls[0]?.[1] as {
state?: string;
message?: { content?: Array<{ text?: string }> };
};
expect(payload.state).toBe("final");
expect(payload.message?.content?.[0]?.text).toBe(
"Disk usage crossed 95 percent on /data and needs cleanup now.",
);
});
});