mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 19:20:22 +00:00
fix(slack): keep status ack on silent runs
This commit is contained in:
@@ -598,6 +598,56 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps ack reaction when no reply is delivered and status reactions are enabled", async () => {
|
||||
replyMock.mockResolvedValue(undefined);
|
||||
slackTestState.config = {
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "group-mentions",
|
||||
removeAckAfterReply: true,
|
||||
statusReactions: {
|
||||
enabled: true,
|
||||
timing: { debounceMs: 0, doneHoldMs: 0, errorHoldMs: 0 },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
groupPolicy: "open",
|
||||
},
|
||||
},
|
||||
};
|
||||
const client = getSlackClient();
|
||||
if (!client) {
|
||||
throw new Error("Slack client not registered");
|
||||
}
|
||||
const conversations = client.conversations as {
|
||||
info: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
conversations.info.mockResolvedValueOnce({
|
||||
channel: { name: "general", is_channel: true },
|
||||
});
|
||||
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent({
|
||||
text: "<@bot-user> hello",
|
||||
ts: "456",
|
||||
channel_type: "channel",
|
||||
}),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await flush();
|
||||
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(reactMock).toHaveBeenCalledTimes(1);
|
||||
expect(reactMock).toHaveBeenCalledWith({
|
||||
channel: "C1",
|
||||
timestamp: "456",
|
||||
name: "eyes",
|
||||
});
|
||||
});
|
||||
|
||||
it("replies with pairing code when dmPolicy is pairing and no allowFrom is set", async () => {
|
||||
setPairingOnlyDirectMessages();
|
||||
|
||||
|
||||
@@ -559,7 +559,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
}
|
||||
};
|
||||
|
||||
let dispatchError = false;
|
||||
let dispatchError: unknown;
|
||||
let didAdvanceStatusReaction = false;
|
||||
let queuedFinal = false;
|
||||
let counts: { final?: number; block?: number } = {};
|
||||
try {
|
||||
@@ -588,11 +589,13 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
onReasoningEnd: onDraftBoundary,
|
||||
onReasoningStream: statusReactionsEnabled
|
||||
? async () => {
|
||||
didAdvanceStatusReaction = true;
|
||||
await statusReactions.setThinking();
|
||||
}
|
||||
: undefined,
|
||||
onToolStart: statusReactionsEnabled
|
||||
? async (payload) => {
|
||||
didAdvanceStatusReaction = true;
|
||||
await statusReactions.setTool(payload.name);
|
||||
}
|
||||
: undefined,
|
||||
@@ -601,30 +604,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
queuedFinal = result.queuedFinal;
|
||||
counts = result.counts;
|
||||
} catch (err) {
|
||||
dispatchError = true;
|
||||
throw err;
|
||||
dispatchError = err;
|
||||
} finally {
|
||||
await draftStream?.flush();
|
||||
draftStream?.stop();
|
||||
markDispatchIdle();
|
||||
|
||||
if (statusReactionsEnabled) {
|
||||
if (dispatchError) {
|
||||
await statusReactions.setError();
|
||||
} else {
|
||||
await statusReactions.setDone();
|
||||
}
|
||||
if (ctx.removeAckAfterReply) {
|
||||
void (async () => {
|
||||
await sleep(
|
||||
dispatchError ? statusReactionTiming.errorHoldMs : statusReactionTiming.doneHoldMs,
|
||||
);
|
||||
await statusReactions.clear();
|
||||
})();
|
||||
} else {
|
||||
void statusReactions.restoreInitial();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -641,6 +625,38 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
|
||||
const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0;
|
||||
|
||||
if (statusReactionsEnabled) {
|
||||
if (dispatchError) {
|
||||
await statusReactions.setError();
|
||||
if (ctx.removeAckAfterReply) {
|
||||
void (async () => {
|
||||
await sleep(statusReactionTiming.errorHoldMs);
|
||||
await statusReactions.clear();
|
||||
})();
|
||||
} else {
|
||||
void statusReactions.restoreInitial();
|
||||
}
|
||||
} else if (anyReplyDelivered) {
|
||||
await statusReactions.setDone();
|
||||
if (ctx.removeAckAfterReply) {
|
||||
void (async () => {
|
||||
await sleep(statusReactionTiming.doneHoldMs);
|
||||
await statusReactions.clear();
|
||||
})();
|
||||
} else {
|
||||
void statusReactions.restoreInitial();
|
||||
}
|
||||
} else if (didAdvanceStatusReaction) {
|
||||
// Silent success should preserve the original ack instead of looking like
|
||||
// we delivered a visible reply.
|
||||
await statusReactions.restoreInitial();
|
||||
}
|
||||
}
|
||||
|
||||
if (dispatchError) {
|
||||
throw dispatchError;
|
||||
}
|
||||
|
||||
// Record thread participation only when we actually delivered a reply and
|
||||
// know the thread ts that was used (set by deliverNormally, streaming start,
|
||||
// or draft stream). Falls back to statusThreadTs for edge cases.
|
||||
|
||||
Reference in New Issue
Block a user