mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -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" });
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user