mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(cron): require verified message delivery target
This commit is contained in:
@@ -848,6 +848,39 @@ describe("dispatchCronDelivery — double-announce guard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps unresolved message-tool delivery out of delivered status", async () => {
|
||||
const params = makeBaseParams({ synthesizedText: "hello from cron" });
|
||||
params.resolvedDelivery = {
|
||||
ok: false,
|
||||
channel: undefined,
|
||||
to: undefined,
|
||||
accountId: undefined,
|
||||
threadId: undefined,
|
||||
mode: "implicit",
|
||||
error: new Error("sessionKey is required to resolve delivery.channel=last"),
|
||||
};
|
||||
params.unverifiedMessagingToolDelivery = true;
|
||||
|
||||
const state = await dispatchCronDelivery(params);
|
||||
|
||||
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||
expect(state.delivered).toBe(false);
|
||||
expect(state.deliveryAttempted).toBe(false);
|
||||
expect(state.result).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "error",
|
||||
errorKind: "delivery-target",
|
||||
deliveryAttempted: false,
|
||||
}),
|
||||
);
|
||||
expect(state.result?.error).toContain(
|
||||
"sessionKey is required to resolve delivery.channel=last",
|
||||
);
|
||||
expect(state.result?.error).toContain(
|
||||
"the agent used the message tool, but OpenClaw could not verify",
|
||||
);
|
||||
});
|
||||
|
||||
it("builds outbound session context from the run session key under per-channel-peer scoping", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
|
||||
@@ -110,6 +110,7 @@ type DispatchCronDeliveryParams = {
|
||||
deliveryRequested: boolean;
|
||||
skipHeartbeatDelivery: boolean;
|
||||
skipMessagingToolDelivery?: boolean;
|
||||
unverifiedMessagingToolDelivery?: boolean;
|
||||
deliveryBestEffort: boolean;
|
||||
deliveryPayloadHasStructuredContent: boolean;
|
||||
deliveryPayloads: ReplyPayload[];
|
||||
@@ -445,10 +446,14 @@ export async function dispatchCronDelivery(
|
||||
let delivered = skipMessagingToolDelivery;
|
||||
let deliveryAttempted = skipMessagingToolDelivery;
|
||||
let directCronSessionDeleted = false;
|
||||
const formatDeliveryTargetError = (error: string) =>
|
||||
params.unverifiedMessagingToolDelivery === true
|
||||
? `${error}; the agent used the message tool, but OpenClaw could not verify that message matched the cron delivery target`
|
||||
: error;
|
||||
const failDeliveryTarget = (error: string) =>
|
||||
params.withRunSession({
|
||||
status: "error",
|
||||
error,
|
||||
error: formatDeliveryTargetError(error),
|
||||
errorKind: "delivery-target",
|
||||
summary,
|
||||
outputText,
|
||||
|
||||
@@ -396,6 +396,41 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not mark message tool delivery as matched when cron target resolution failed", async () => {
|
||||
mockRunCronFallbackPassthrough();
|
||||
resolveCronDeliveryPlanMock.mockReturnValue({
|
||||
requested: true,
|
||||
mode: "announce",
|
||||
channel: "last",
|
||||
});
|
||||
resolveDeliveryTargetMock.mockResolvedValue({
|
||||
ok: false,
|
||||
channel: undefined,
|
||||
to: undefined,
|
||||
accountId: undefined,
|
||||
threadId: undefined,
|
||||
mode: "implicit",
|
||||
error: new Error("sessionKey is required to resolve delivery.channel=last"),
|
||||
});
|
||||
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: "sent" }],
|
||||
didSendViaMessagingTool: true,
|
||||
messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }],
|
||||
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
|
||||
});
|
||||
|
||||
await runCronIsolatedAgentTurn(makeParams());
|
||||
|
||||
expect(dispatchCronDeliveryMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchCronDeliveryMock.mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
deliveryRequested: true,
|
||||
skipMessagingToolDelivery: false,
|
||||
unverifiedMessagingToolDelivery: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("marks no-deliver runs delivered when the message tool sends to the current target", async () => {
|
||||
mockRunCronFallbackPassthrough();
|
||||
resolveCronDeliveryPlanMock.mockReturnValue({
|
||||
|
||||
@@ -386,15 +386,22 @@ function resetRunOutcomeMocks(): void {
|
||||
deliveryRequested,
|
||||
skipHeartbeatDelivery,
|
||||
skipMessagingToolDelivery,
|
||||
resolvedDelivery,
|
||||
}) => ({
|
||||
result: undefined,
|
||||
delivered: Boolean(
|
||||
skipMessagingToolDelivery ||
|
||||
(deliveryRequested && !skipHeartbeatDelivery && !skipMessagingToolDelivery),
|
||||
(deliveryRequested &&
|
||||
!skipHeartbeatDelivery &&
|
||||
!skipMessagingToolDelivery &&
|
||||
resolvedDelivery.ok),
|
||||
),
|
||||
deliveryAttempted: Boolean(
|
||||
skipMessagingToolDelivery ||
|
||||
(deliveryRequested && !skipHeartbeatDelivery && !skipMessagingToolDelivery),
|
||||
(deliveryRequested &&
|
||||
!skipHeartbeatDelivery &&
|
||||
!skipMessagingToolDelivery &&
|
||||
resolvedDelivery.ok),
|
||||
),
|
||||
summary,
|
||||
outputText,
|
||||
|
||||
@@ -662,17 +662,19 @@ async function finalizeCronRun(params: {
|
||||
matchesMessagingToolDeliveryTarget,
|
||||
resolveCronDeliveryBestEffort,
|
||||
} = await loadCronDeliveryRuntime();
|
||||
const messagingToolSentTargets = finalRunResult.messagingToolSentTargets ?? [];
|
||||
const didSendViaMessagingTool =
|
||||
finalRunResult.didSendViaMessagingTool === true && messagingToolSentTargets.length > 0;
|
||||
const skipMessagingToolDelivery =
|
||||
finalRunResult.didSendViaMessagingTool === true &&
|
||||
(prepared.resolvedDelivery.ok
|
||||
? (finalRunResult.messagingToolSentTargets ?? []).some((target) =>
|
||||
matchesMessagingToolDeliveryTarget(target, {
|
||||
channel: prepared.resolvedDelivery.channel,
|
||||
to: prepared.resolvedDelivery.to,
|
||||
accountId: prepared.resolvedDelivery.accountId,
|
||||
}),
|
||||
)
|
||||
: (finalRunResult.messagingToolSentTargets ?? []).length > 0);
|
||||
didSendViaMessagingTool &&
|
||||
prepared.resolvedDelivery.ok &&
|
||||
messagingToolSentTargets.some((target) =>
|
||||
matchesMessagingToolDeliveryTarget(target, {
|
||||
channel: prepared.resolvedDelivery.channel,
|
||||
to: prepared.resolvedDelivery.to,
|
||||
accountId: prepared.resolvedDelivery.accountId,
|
||||
}),
|
||||
);
|
||||
const deliveryResult = await dispatchCronDelivery({
|
||||
cfg: prepared.input.cfg,
|
||||
cfgWithAgentDefaults: prepared.cfgWithAgentDefaults,
|
||||
@@ -687,6 +689,7 @@ async function finalizeCronRun(params: {
|
||||
deliveryRequested: prepared.deliveryRequested,
|
||||
skipHeartbeatDelivery,
|
||||
skipMessagingToolDelivery,
|
||||
unverifiedMessagingToolDelivery: didSendViaMessagingTool && !prepared.resolvedDelivery.ok,
|
||||
deliveryBestEffort: resolveCronDeliveryBestEffort(prepared.input.job),
|
||||
deliveryPayloadHasStructuredContent,
|
||||
deliveryPayloads,
|
||||
|
||||
Reference in New Issue
Block a user