Tests verify that string thread IDs (Slack thread_ts) are correctly included in queue routing keys and found in item searches. Covers the regression where typeof === 'number' silently ignored string IDs, collapsing different Slack threads into the same routing key.
185 lines
6.3 KiB
TypeScript
185 lines
6.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { hasCrossChannelItems } from "../../../utils/queue-helpers.js";
|
|
|
|
/**
|
|
* Tests for PR #4413: thread_ts null check fix.
|
|
*
|
|
* The queue drain logic uses `hasCrossChannelItems` with a key resolver that
|
|
* includes threadId in the routing key. Previously it used
|
|
* `typeof threadId === "number"` which silently ignored string thread IDs
|
|
* (e.g. Slack's `thread_ts = "1706000000.000100"`). The fix uses
|
|
* `threadId != null` to handle both number and string thread IDs.
|
|
*/
|
|
|
|
interface FakeQueueItem {
|
|
originatingChannel?: string;
|
|
originatingTo?: string;
|
|
originatingAccountId?: string;
|
|
originatingThreadId?: string | number;
|
|
}
|
|
|
|
/** Mirrors the resolveKey callback from drain.ts (post-fix version). */
|
|
function resolveKey(item: FakeQueueItem) {
|
|
const channel = item.originatingChannel;
|
|
const to = item.originatingTo;
|
|
const accountId = item.originatingAccountId;
|
|
const threadId = item.originatingThreadId;
|
|
if (!channel && !to && !accountId && threadId == null) {
|
|
return {};
|
|
}
|
|
// isRoutableChannel simplified: just check truthy for test purposes
|
|
if (!channel || !to) {
|
|
return { cross: true };
|
|
}
|
|
const threadKey = threadId != null ? String(threadId) : "";
|
|
return {
|
|
key: [channel, to, accountId || "", threadKey].join("|"),
|
|
};
|
|
}
|
|
|
|
/** The OLD (buggy) resolveKey that used typeof === "number". */
|
|
function resolveKeyBuggy(item: FakeQueueItem) {
|
|
const channel = item.originatingChannel;
|
|
const to = item.originatingTo;
|
|
const accountId = item.originatingAccountId;
|
|
const threadId = item.originatingThreadId;
|
|
if (!channel && !to && !accountId && typeof threadId !== "number") {
|
|
return {};
|
|
}
|
|
if (!channel || !to) {
|
|
return { cross: true };
|
|
}
|
|
const threadKey = typeof threadId === "number" ? String(threadId) : "";
|
|
return {
|
|
key: [channel, to, accountId || "", threadKey].join("|"),
|
|
};
|
|
}
|
|
|
|
describe("thread_ts null check (PR #4413)", () => {
|
|
describe("resolveKey with string thread IDs (Slack thread_ts)", () => {
|
|
const slackItem: FakeQueueItem = {
|
|
originatingChannel: "slack",
|
|
originatingTo: "C123",
|
|
originatingAccountId: "T456",
|
|
originatingThreadId: "1706000000.000100",
|
|
};
|
|
|
|
it("includes string threadId in key (fixed)", () => {
|
|
const result = resolveKey(slackItem);
|
|
expect(result.key).toBe("slack|C123|T456|1706000000.000100");
|
|
});
|
|
|
|
it("old code drops string threadId from key (bug)", () => {
|
|
const result = resolveKeyBuggy(slackItem);
|
|
// Bug: string threadId is ignored, threadKey becomes ""
|
|
expect(result.key).toBe("slack|C123|T456|");
|
|
});
|
|
|
|
it("distinguishes messages in different Slack threads", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ ...slackItem, originatingThreadId: "1706000000.000100" },
|
|
{ ...slackItem, originatingThreadId: "1706000000.000200" },
|
|
];
|
|
expect(hasCrossChannelItems(items, resolveKey)).toBe(true);
|
|
});
|
|
|
|
it("old code collapses different Slack threads into same key (bug)", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ ...slackItem, originatingThreadId: "1706000000.000100" },
|
|
{ ...slackItem, originatingThreadId: "1706000000.000200" },
|
|
];
|
|
// Bug: both get key "slack|C123|T456|" so they look same-channel
|
|
expect(hasCrossChannelItems(items, resolveKeyBuggy)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("resolveKey with numeric thread IDs", () => {
|
|
it("includes numeric threadId in key", () => {
|
|
const result = resolveKey({
|
|
originatingChannel: "discord",
|
|
originatingTo: "guild123",
|
|
originatingThreadId: 42,
|
|
});
|
|
expect(result.key).toBe("discord|guild123||42");
|
|
});
|
|
|
|
it("old code also handles numeric threadId (was already working)", () => {
|
|
const result = resolveKeyBuggy({
|
|
originatingChannel: "discord",
|
|
originatingTo: "guild123",
|
|
originatingThreadId: 42,
|
|
});
|
|
expect(result.key).toBe("discord|guild123||42");
|
|
});
|
|
});
|
|
|
|
describe("resolveKey with null/undefined threadId", () => {
|
|
it("returns empty object when all fields are empty", () => {
|
|
expect(resolveKey({})).toEqual({});
|
|
});
|
|
|
|
it("uses empty string for threadKey when threadId is undefined", () => {
|
|
const result = resolveKey({
|
|
originatingChannel: "slack",
|
|
originatingTo: "C123",
|
|
});
|
|
expect(result.key).toBe("slack|C123||");
|
|
});
|
|
|
|
it("uses empty string for threadKey when threadId is null-ish", () => {
|
|
const result = resolveKey({
|
|
originatingChannel: "slack",
|
|
originatingTo: "C123",
|
|
originatingThreadId: undefined,
|
|
});
|
|
expect(result.key).toBe("slack|C123||");
|
|
});
|
|
});
|
|
|
|
describe("finding originatingThreadId in items (second fix site)", () => {
|
|
/**
|
|
* The drain also does:
|
|
* items.find(i => i.originatingThreadId != null)?.originatingThreadId
|
|
* Previously: items.find(i => typeof i.originatingThreadId === "number")
|
|
*/
|
|
const findThreadId = (items: FakeQueueItem[]) =>
|
|
items.find((i) => i.originatingThreadId != null)?.originatingThreadId;
|
|
|
|
const findThreadIdBuggy = (items: FakeQueueItem[]) =>
|
|
items.find((i) => typeof i.originatingThreadId === "number")?.originatingThreadId;
|
|
|
|
it("finds string thread ID (fixed)", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ originatingChannel: "slack" },
|
|
{ originatingChannel: "slack", originatingThreadId: "1706000000.000100" },
|
|
];
|
|
expect(findThreadId(items)).toBe("1706000000.000100");
|
|
});
|
|
|
|
it("old code misses string thread ID (bug)", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ originatingChannel: "slack" },
|
|
{ originatingChannel: "slack", originatingThreadId: "1706000000.000100" },
|
|
];
|
|
expect(findThreadIdBuggy(items)).toBeUndefined();
|
|
});
|
|
|
|
it("finds numeric thread ID", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ originatingChannel: "discord" },
|
|
{ originatingChannel: "discord", originatingThreadId: 99 },
|
|
];
|
|
expect(findThreadId(items)).toBe(99);
|
|
expect(findThreadIdBuggy(items)).toBe(99);
|
|
});
|
|
|
|
it("returns undefined when no items have threadId", () => {
|
|
const items: FakeQueueItem[] = [
|
|
{ originatingChannel: "slack" },
|
|
{ originatingChannel: "slack" },
|
|
];
|
|
expect(findThreadId(items)).toBeUndefined();
|
|
});
|
|
});
|
|
});
|