179 lines · 5.4 KB · TypeScript
RawDownload
import type { NotificationResponse } from "expo-notifications";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer"; import {  extractAgentNotificationDeepLink,  routeAgentNotificationResponseOnce,} from "./notificationPayload"; function responseWithData(data: Record<string, unknown>, identifier = "notification-1") {  return {    notification: {      request: {        identifier,        content: {          data,        },      },    },  };} afterEach(() => {  vi.restoreAllMocks();}); describe("consumeLastAgentNotificationResponse", () => {  it("reports which initial-response operation failed", async () => {    const cause = new Error("notification lookup unavailable");    const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);     await consumeLastAgentNotificationResponse({      getLastResponse: () => Promise.reject(cause),      clearLastResponse: () => Promise.resolve(),      handleResponse: vi.fn(),    });     expect(consoleError).toHaveBeenCalledWith(      expect.objectContaining({        _tag: "NotificationNavigationError",        operation: "read",      }),    );  });   it("routes a response before reporting a clear failure", async () => {    const cause = new Error("notification clear unavailable");    const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);    const response = responseWithData({}, "notification-clear") as NotificationResponse;    const handleResponse = vi.fn();     await consumeLastAgentNotificationResponse({      getLastResponse: () => Promise.resolve(response),      clearLastResponse: () => Promise.reject(cause),      handleResponse,    });     expect(handleResponse).toHaveBeenCalledWith(response);    expect(consoleError).toHaveBeenCalledWith(      expect.objectContaining({        _tag: "NotificationNavigationError",        operation: "clear",        notificationId: "notification-clear",      }),    );  });   it("reports routing failures before clearing the response", async () => {    const cause = new Error("notification routing unavailable");    const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);    const response = responseWithData({}, "notification-route") as NotificationResponse;    const clearLastResponse = vi.fn(() => Promise.resolve());     await consumeLastAgentNotificationResponse({      getLastResponse: () => Promise.resolve(response),      clearLastResponse,      handleResponse: () => {        throw cause;      },    });     expect(clearLastResponse).not.toHaveBeenCalled();    expect(consoleError).toHaveBeenCalledWith(      expect.objectContaining({        _tag: "NotificationNavigationError",        operation: "route",        notificationId: "notification-route",      }),    );  });}); describe("extractAgentNotificationDeepLink", () => {  it("uses explicit deep links from APNs payload data", () => {    expect(      extractAgentNotificationDeepLink(        responseWithData({          deepLink: "/threads/env/thread",          environmentId: "ignored",          threadId: "ignored",        }),      ),    ).toBe("/threads/env/thread");  });   it("normalizes explicit thread deep links from APNs payload data", () => {    expect(      extractAgentNotificationDeepLink(        responseWithData({          deepLink: "/threads/env%201/thread%2F2",        }),      ),    ).toBe("/threads/env%201/thread%2F2");  });   it("falls back to the thread route from environment and thread ids", () => {    expect(      extractAgentNotificationDeepLink(        responseWithData({          environmentId: "env 1",          threadId: "thread/2",        }),      ),    ).toBe("/threads/env%201/thread%2F2");  });   it("falls back to ids when explicit deep link is not an agent thread route", () => {    expect(      extractAgentNotificationDeepLink(        responseWithData({          deepLink: "/",          environmentId: "env",          threadId: "thread",        }),      ),    ).toBe("/threads/env/thread");  });   it("ignores malformed or external links", () => {    expect(      extractAgentNotificationDeepLink(responseWithData({ deepLink: "https://example.com" })),    ).toBeNull();    expect(      extractAgentNotificationDeepLink(responseWithData({ deepLink: "/settings" })),    ).toBeNull();    expect(      extractAgentNotificationDeepLink(responseWithData({ deepLink: "//example.com" })),    ).toBeNull();    expect(      extractAgentNotificationDeepLink(responseWithData({ deepLink: "/threads/env/thread?x=1" })),    ).toBeNull();    expect(extractAgentNotificationDeepLink({})).toBeNull();  });}); describe("routeAgentNotificationResponseOnce", () => {  it("does not navigate twice when the initial and listener responses refer to one notification", () => {    const handledResponseIds = new Set<string>();    const navigations: Array<string> = [];    const response = responseWithData({      environmentId: "env",      threadId: "thread",    });     routeAgentNotificationResponseOnce({      handledResponseIds,      response,      navigate: (deepLink) => navigations.push(deepLink),    });    routeAgentNotificationResponseOnce({      handledResponseIds,      response,      navigate: (deepLink) => navigations.push(deepLink),    });     expect(navigations).toEqual(["/threads/env/thread"]);  });});