From 1bb2807acadc763e4c3a1333c2363f9a941e7dff Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:13:00 -0500 Subject: [PATCH] fix: normalize device-pair notify thread ids --- extensions/device-pair/notify.test.ts | 88 +++++++++++++++++++++++++++ extensions/device-pair/notify.ts | 27 +++++++- 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 extensions/device-pair/notify.test.ts diff --git a/extensions/device-pair/notify.test.ts b/extensions/device-pair/notify.test.ts new file mode 100644 index 00000000000..47d8905c6af --- /dev/null +++ b/extensions/device-pair/notify.test.ts @@ -0,0 +1,88 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js"; + +const listDevicePairingMock = vi.hoisted(() => vi.fn(async () => ({ pending: [] }))); + +vi.mock("./api.js", () => ({ + listDevicePairing: listDevicePairingMock, +})); + +import { handleNotifyCommand } from "./notify.js"; + +describe("device-pair notify persistence", () => { + let stateDir: string; + + beforeEach(async () => { + vi.clearAllMocks(); + listDevicePairingMock.mockResolvedValue({ pending: [] }); + stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "device-pair-notify-")); + }); + + afterEach(async () => { + await fs.rm(stateDir, { recursive: true, force: true }); + }); + + it("matches persisted telegram thread ids across number and string roundtrips", async () => { + await fs.writeFile( + path.join(stateDir, "device-pair-notify.json"), + JSON.stringify( + { + subscribers: [ + { + to: "chat-123", + accountId: "telegram-default", + messageThreadId: 271, + mode: "persistent", + addedAtMs: 1, + }, + ], + notifiedRequestIds: {}, + }, + null, + 2, + ), + "utf8", + ); + + const api = createTestPluginApi({ + runtime: { + state: { + resolveStateDir: () => stateDir, + }, + } as never, + }); + + const status = await handleNotifyCommand({ + api, + ctx: { + channel: "telegram", + senderId: "chat-123", + accountId: "telegram-default", + messageThreadId: "271", + }, + action: "status", + }); + + expect(status.text).toContain("Pair request notifications: enabled for this chat."); + expect(status.text).toContain("Mode: persistent"); + + await handleNotifyCommand({ + api, + ctx: { + channel: "telegram", + senderId: "chat-123", + accountId: "telegram-default", + messageThreadId: "271", + }, + action: "off", + }); + + const persisted = JSON.parse( + await fs.readFile(path.join(stateDir, "device-pair-notify.json"), "utf8"), + ) as { subscribers: unknown[] }; + expect(persisted.subscribers).toEqual([]); + }); +}); diff --git a/extensions/device-pair/notify.ts b/extensions/device-pair/notify.ts index 9684277df6a..dd574bae965 100644 --- a/extensions/device-pair/notify.ts +++ b/extensions/device-pair/notify.ts @@ -154,7 +154,32 @@ function notifySubscriberKey(subscriber: { accountId?: string; messageThreadId?: string | number; }): string { - return [subscriber.to, subscriber.accountId ?? "", subscriber.messageThreadId ?? ""].join("|"); + return [ + subscriber.to, + subscriber.accountId ?? "", + normalizeNotifyThreadKey(subscriber.messageThreadId), + ].join("|"); +} + +function normalizeNotifyThreadKey(messageThreadId?: string | number): string { + if (typeof messageThreadId === "number" && Number.isFinite(messageThreadId)) { + return String(Math.trunc(messageThreadId)); + } + if (typeof messageThreadId !== "string") { + return ""; + } + const normalized = normalizeOptionalString(messageThreadId); + if (!normalized) { + return ""; + } + if (!/^-?\d+$/u.test(normalized)) { + return normalized; + } + try { + return BigInt(normalized).toString(); + } catch { + return normalized; + } } type NotifyTarget = {