mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-15 19:21:08 +00:00
Scope Slack turn-local delivery dedupe by reply dispatch kind so identical tool and final payloads on the same thread do not collapse into one send. Expose the existing dispatcher kind on the public reply-runtime seam and cover the Slack tracker and preview-fallback paths with regression tests.
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createSlackTurnDeliveryTracker,
|
|
isSlackStreamingEnabled,
|
|
resolveSlackStreamingThreadHint,
|
|
shouldEnableSlackPreviewStreaming,
|
|
shouldInitializeSlackDraftStream,
|
|
} from "./dispatch.js";
|
|
|
|
describe("slack native streaming defaults", () => {
|
|
it("is enabled for partial mode when native streaming is on", () => {
|
|
expect(isSlackStreamingEnabled({ mode: "partial", nativeStreaming: true })).toBe(true);
|
|
});
|
|
|
|
it("is disabled outside partial mode or when native streaming is off", () => {
|
|
expect(isSlackStreamingEnabled({ mode: "partial", nativeStreaming: false })).toBe(false);
|
|
expect(isSlackStreamingEnabled({ mode: "block", nativeStreaming: true })).toBe(false);
|
|
expect(isSlackStreamingEnabled({ mode: "progress", nativeStreaming: true })).toBe(false);
|
|
expect(isSlackStreamingEnabled({ mode: "off", nativeStreaming: true })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("slack turn delivery tracker", () => {
|
|
it("treats repeated text payloads on the same thread as duplicates", () => {
|
|
const tracker = createSlackTurnDeliveryTracker();
|
|
const payload = { text: "same reply" };
|
|
|
|
expect(tracker.hasDelivered({ kind: "final", payload, threadTs: "123.456" })).toBe(false);
|
|
tracker.markDelivered({ kind: "final", payload, threadTs: "123.456" });
|
|
expect(tracker.hasDelivered({ kind: "final", payload, threadTs: "123.456" })).toBe(true);
|
|
expect(tracker.hasDelivered({ kind: "final", payload, threadTs: "other-thread" })).toBe(false);
|
|
});
|
|
|
|
it("keeps explicit reply targets distinct from the shared thread target", () => {
|
|
const tracker = createSlackTurnDeliveryTracker();
|
|
|
|
tracker.markDelivered({
|
|
kind: "final",
|
|
payload: { text: "same reply", replyToId: "thread-A" },
|
|
threadTs: "123.456",
|
|
});
|
|
|
|
expect(
|
|
tracker.hasDelivered({
|
|
kind: "final",
|
|
payload: { text: "same reply", replyToId: "thread-B" },
|
|
threadTs: "123.456",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps distinct dispatch kinds separate for identical payloads", () => {
|
|
const tracker = createSlackTurnDeliveryTracker();
|
|
const payload = { text: "same reply" };
|
|
|
|
tracker.markDelivered({ kind: "tool", payload, threadTs: "123.456" });
|
|
|
|
expect(tracker.hasDelivered({ kind: "tool", payload, threadTs: "123.456" })).toBe(true);
|
|
expect(tracker.hasDelivered({ kind: "final", payload, threadTs: "123.456" })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("slack native streaming thread hint", () => {
|
|
it("stays off-thread when replyToMode=off and message is not in a thread", () => {
|
|
expect(
|
|
resolveSlackStreamingThreadHint({
|
|
replyToMode: "off",
|
|
incomingThreadTs: undefined,
|
|
messageTs: "1000.1",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("uses first-reply thread when replyToMode=first", () => {
|
|
expect(
|
|
resolveSlackStreamingThreadHint({
|
|
replyToMode: "first",
|
|
incomingThreadTs: undefined,
|
|
messageTs: "1000.2",
|
|
}),
|
|
).toBe("1000.2");
|
|
});
|
|
|
|
it("uses the existing incoming thread regardless of replyToMode", () => {
|
|
expect(
|
|
resolveSlackStreamingThreadHint({
|
|
replyToMode: "off",
|
|
incomingThreadTs: "2000.1",
|
|
messageTs: "1000.3",
|
|
}),
|
|
).toBe("2000.1");
|
|
});
|
|
});
|
|
|
|
describe("slack preview streaming eligibility", () => {
|
|
it("stays on for room messages when streaming mode is enabled", () => {
|
|
expect(
|
|
shouldEnableSlackPreviewStreaming({
|
|
mode: "partial",
|
|
isDirectMessage: false,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("stays off for top-level DMs without a reply thread", () => {
|
|
expect(
|
|
shouldEnableSlackPreviewStreaming({
|
|
mode: "partial",
|
|
isDirectMessage: true,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("allows DM preview when the reply is threaded", () => {
|
|
expect(
|
|
shouldEnableSlackPreviewStreaming({
|
|
mode: "partial",
|
|
isDirectMessage: true,
|
|
threadTs: "1000.1",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps top-level DMs off even when replyToMode would create a reply thread", () => {
|
|
const streamThreadHint = resolveSlackStreamingThreadHint({
|
|
replyToMode: "all",
|
|
incomingThreadTs: undefined,
|
|
messageTs: "1000.4",
|
|
isThreadReply: false,
|
|
});
|
|
|
|
expect(
|
|
shouldEnableSlackPreviewStreaming({
|
|
mode: "partial",
|
|
isDirectMessage: true,
|
|
threadTs: undefined,
|
|
}),
|
|
).toBe(false);
|
|
expect(streamThreadHint).toBe("1000.4");
|
|
});
|
|
});
|
|
|
|
describe("slack draft stream initialization", () => {
|
|
it("stays off when preview streaming is disabled", () => {
|
|
expect(
|
|
shouldInitializeSlackDraftStream({
|
|
previewStreamingEnabled: false,
|
|
useStreaming: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("stays off when native streaming is active", () => {
|
|
expect(
|
|
shouldInitializeSlackDraftStream({
|
|
previewStreamingEnabled: true,
|
|
useStreaming: true,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("turns on only for preview-only paths", () => {
|
|
expect(
|
|
shouldInitializeSlackDraftStream({
|
|
previewStreamingEnabled: true,
|
|
useStreaming: false,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|