fix(cron): suppress announce control replies

Use the shared suppressed-control-reply detector for cron delivery so NO_REPLY, ANNOUNCE_SKIP, and REPLY_SKIP do not leak to outbound channels, with direct/text delivery coverage.
This commit is contained in:
Andy Ye
2026-06-15 15:25:21 -07:00
committed by GitHub
parent 923828ccd7
commit 57bad4bdf8
2 changed files with 50 additions and 30 deletions
@@ -1391,36 +1391,55 @@ describe("dispatchCronDelivery — double-announce guard", () => {
}
});
it("suppresses NO_REPLY payload in direct delivery so sentinel never leaks to external channels", async () => {
const params = makeBaseParams({ synthesizedText: "NO_REPLY" });
// Force the useDirectDelivery path (structured content) to exercise
// deliverViaDirect without going through finalizeTextDelivery.
(params as Record<string, unknown>).deliveryPayloadHasStructuredContent = true;
const state = await dispatchCronDelivery(params);
it.each([SILENT_REPLY_TOKEN, "ANNOUNCE_SKIP", "REPLY_SKIP"])(
"suppresses %s payload in direct delivery so control tokens never leak to external channels",
async (controlToken) => {
const params = makeBaseParams({ synthesizedText: controlToken });
// Force the useDirectDelivery path (structured content) to exercise
// deliverViaDirect without going through finalizeTextDelivery.
(params as Record<string, unknown>).deliveryPayloadHasStructuredContent = true;
const state = await dispatchCronDelivery(params);
// NO_REPLY must be filtered out before reaching the outbound adapter.
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
expectResultFields(state.result, {
status: "ok",
delivered: false,
deliveryAttempted: true,
});
// deliveryAttempted must be true so the heartbeat timer does not fire
// a fallback enqueueSystemEvent with the NO_REPLY sentinel text.
expect(state.deliveryAttempted).toBe(true);
// Control tokens must be filtered out before reaching the outbound adapter.
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
expectResultFields(state.result, {
status: "ok",
delivered: false,
deliveryAttempted: true,
});
// deliveryAttempted must be true so the heartbeat timer does not fire
// a fallback enqueueSystemEvent with the control-token text.
expect(state.deliveryAttempted).toBe(true);
// Verify timer guard agrees: shouldEnqueueCronMainSummary returns false
expect(
shouldEnqueueCronMainSummary({
summaryText: "NO_REPLY",
deliveryRequested: true,
delivered: state.result?.delivered,
deliveryAttempted: state.result?.deliveryAttempted,
suppressMainSummary: false,
isCronSystemEvent: () => true,
}),
).toBe(false);
});
// Verify timer guard agrees: shouldEnqueueCronMainSummary returns false
expect(
shouldEnqueueCronMainSummary({
summaryText: controlToken,
deliveryRequested: true,
delivered: state.result?.delivered,
deliveryAttempted: state.result?.deliveryAttempted,
suppressMainSummary: false,
isCronSystemEvent: () => true,
}),
).toBe(false);
},
);
it.each(["ANNOUNCE_SKIP", "REPLY_SKIP"])(
"suppresses %s payload in text delivery so control tokens never leak to external channels",
async (controlToken) => {
const params = makeBaseParams({ synthesizedText: controlToken });
const state = await dispatchCronDelivery(params);
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
expectResultFields(state.result, {
status: "ok",
delivered: false,
deliveryAttempted: true,
});
expect(state.deliveryAttempted).toBe(true);
},
);
it("delivers explicit targets with direct text through the outbound adapter", async () => {
const params = makeBaseParams({ synthesizedText: "hello from cron" });
+3 -2
View File
@@ -20,6 +20,7 @@ import {
import { resolveMirroredTranscriptText } from "../../config/sessions/transcript-mirror.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { TtsAutoMode } from "../../config/types.tts.js";
import { isSuppressedControlReplyText } from "../../gateway/control-reply-text.js";
import { sleepWithAbort } from "../../infra/backoff.js";
import { formatErrorMessage } from "../../infra/errors.js";
import type {
@@ -63,7 +64,7 @@ function normalizeSilentReplyText(text: string | undefined): NormalizedSilentRep
if (!text) {
return { text, strippedTrailingSilentToken: false };
}
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
if (isSuppressedControlReplyText(text)) {
return { text: undefined, strippedTrailingSilentToken: false };
}
@@ -81,7 +82,7 @@ function normalizeSilentReplyText(text: string | undefined): NormalizedSilentRep
next = stripped;
}
if (!next.trim() || isSilentReplyText(next, SILENT_REPLY_TOKEN)) {
if (!next.trim() || isSuppressedControlReplyText(next)) {
return { text: undefined, strippedTrailingSilentToken };
}
return { text: next, strippedTrailingSilentToken };