935 lines · 33.1 KB · TypeScript
RawDownload
/// <reference types="node" /> import * as NodeCrypto from "node:crypto"; import { beforeEach, vi } from "vite-plus/test";import { describe, expect, it } from "@effect/vitest";import Constants from "expo-constants";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import { FetchHttpClient } from "effect/unstable/http";import { ManagedRelay } from "@t3tools/client-runtime/relay"; import type { EnvironmentId } from "@t3tools/contracts";import { verifyDpopProof } from "@t3tools/shared/dpop";import type { SavedRemoteConnection } from "../../lib/connection";import { cryptoLayer } from "../cloud/dpop";import { managedRelayClientLayer } from "../cloud/managedRelayLayer";import {  clearAgentAwarenessRegistrationRecord,  loadAgentAwarenessRegistrationRecord,  loadOrCreateAgentAwarenessDeviceId,  loadPreferences,  saveAgentAwarenessRegistrationRecord,} from "../../persistence/imperative";import type { Preferences } from "../../persistence/mobile-preferences";import { makeRelayDeviceRegistrationRequest, resolveApsEnvironment } from "./registrationPayload";import {  AgentAwarenessOperationError,  __resetAgentAwarenessRemoteRegistrationForTest,  armAgentAwarenessLiveActivityForLocalWork,  getAgentAwarenessRegistrationStatus,  mergeAgentAwarenessRegistrationPreferences,  refreshActiveLiveActivityRemoteRegistration,  refreshAgentAwarenessRegistration,  normalizeAgentAwarenessRelayBaseUrl,  registerAgentAwarenessConnection,  registerLiveActivityPushToken,  releaseAgentAwarenessRelayTokenProvider,  setAgentAwarenessRelayTokenProvider,  shouldRegisterAgentAwarenessDeviceForProvider,  unregisterAgentAwarenessConnection,} from "./remoteRegistration";import * as Notifications from "expo-notifications"; const secureStore = vi.hoisted(() => new Map<string, string>());const widgetMocks = vi.hoisted(() => ({  getInstances: vi.fn(() => []),  start: vi.fn(() => ({})),}));const environmentConfigsMock = vi.hoisted(() => ({  configs: new Map<    string,    { environment: { capabilities: { agentActivityPublishing?: boolean } } }  >(),}));const backgroundRuntime = vi.hoisted(() => ({  pending: [] as Array<{    readonly operation: unknown;    readonly resolve: (exit: Exit.Exit<unknown, unknown>) => void;  }>,}));const appStateMock = vi.hoisted(() => ({  listeners: [] as Array<(state: string) => void>,}));const registrationRecordStore = vi.hoisted(() => ({  current: null as {    readonly identity: string;    readonly signature: string;    readonly pushToStartToken?: string;  } | null,})); vi.mock("expo-constants", () => ({  default: {    expoConfig: {      version: "1.0.0",      extra: {},    },  },})); vi.mock("expo-widgets", () => ({  addPushToStartTokenListener: vi.fn(() => ({ remove: vi.fn() })),})); vi.mock("../../widgets/AgentActivity", () => ({  default: {    getInstances: widgetMocks.getInstances,    start: widgetMocks.start,  },})); // The state modules pull the whole connection stack (and native expo modules)// into the import graph; the arming gate only needs the configs map.vi.mock("../../state/atom-registry", () => ({  appAtomRegistry: {    get: () => environmentConfigsMock.configs,  },})); vi.mock("../../state/server", () => ({  environmentServerConfigsAtom: Symbol("environmentServerConfigsAtom"),})); vi.mock("expo-notifications", () => ({  addPushTokenListener: vi.fn(() => ({ remove: vi.fn() })),  getDevicePushTokenAsync: vi.fn(() => Promise.resolve({ type: "ios", data: "apns-token" })),  getPermissionsAsync: vi.fn(() => Promise.resolve({ granted: true })),})); vi.mock("expo-crypto", () => ({  CryptoDigestAlgorithm: {    SHA1: "SHA-1",    SHA256: "SHA-256",    SHA384: "SHA-384",    SHA512: "SHA-512",  },  getRandomBytes: (byteCount: number) => new Uint8Array(NodeCrypto.randomBytes(byteCount)),  getRandomBytesAsync: (byteCount: number) =>    Promise.resolve(new Uint8Array(NodeCrypto.randomBytes(byteCount))),  digest: (algorithm: string, data: unknown) => {    if (!(data instanceof Uint8Array)) {      return Promise.reject(new TypeError("expo-crypto digest data must be a typed array."));    }    return Promise.resolve(      new Uint8Array(NodeCrypto.createHash(algorithm).update(data).digest()).buffer,    );  },})); vi.mock("expo-secure-store", () => ({  getItemAsync: (key: string) => Promise.resolve(secureStore.get(key) ?? null),  setItemAsync: (key: string, value: string) => {    secureStore.set(key, value);    return Promise.resolve();  },})); vi.mock("react-native", () => ({  Platform: {    OS: "ios",    Version: "18.0",  },  AppState: {    addEventListener: (_event: string, listener: (state: string) => void) => {      appStateMock.listeners.push(listener);      return {        remove: () => {          const index = appStateMock.listeners.indexOf(listener);          if (index >= 0) {            appStateMock.listeners.splice(index, 1);          }        },      };    },  },})); vi.mock("../../lib/runtime", () => ({  runtime: {    runPromiseExit: (operation: unknown) =>      new Promise((resolve) => {        backgroundRuntime.pending.push({ operation, resolve });      }),  },})); vi.mock("../../persistence/imperative", () => ({  loadAgentAwarenessDeviceId: vi.fn(() => Promise.resolve("device-1")),  loadOrCreateAgentAwarenessDeviceId: vi.fn(() => Promise.resolve("device-1")),  loadPreferences: vi.fn(() => Promise.resolve({ liveActivitiesEnabled: false })),  loadAgentAwarenessRegistrationRecord: vi.fn(() =>    Promise.resolve(registrationRecordStore.current),  ),  saveAgentAwarenessRegistrationRecord: vi.fn((record: { identity: string; signature: string }) => {    registrationRecordStore.current = record;    return Promise.resolve();  }),  clearAgentAwarenessRegistrationRecord: vi.fn(() => {    registrationRecordStore.current = null;    return Promise.resolve();  }),})); function proofIat(proof: string): number {  const payload = proof.split(".")[1];  if (!payload) {    throw new Error("Missing DPoP payload.");  }  const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as {    readonly iat: number;  };  return decoded.iat;} function savedConnection(): SavedRemoteConnection {  return {    environmentId: "env-1" as EnvironmentId,    environmentLabel: "Desktop",    pairingUrl: "https://desktop.example/pair",    displayUrl: "https://desktop.example",    httpBaseUrl: "https://desktop.example",    wsBaseUrl: "wss://desktop.example/ws",    bearerToken: "bearer-token",  };} const relayTestLayer = managedRelayClientLayer("https://relay.example.test").pipe(  Layer.provide(Layer.mergeAll(FetchHttpClient.layer, cryptoLayer)),); const runBackgroundOperations = Effect.fn("TestRemoteRegistration.runBackgroundOperations")(  function* () {    let idlePasses = 0;    for (;;) {      yield* Effect.promise(() => Promise.resolve());      const pending = backgroundRuntime.pending.shift();      if (!pending) {        idlePasses++;        if (idlePasses >= 3) {          return;        }        continue;      }      idlePasses = 0;      const exit = yield* Effect.exit(        pending.operation as Effect.Effect<unknown, unknown, ManagedRelay.ManagedRelayClient>,      );      yield* Effect.sync(() => {        pending.resolve(exit);      });    }  },); describe("makeRelayDeviceRegistrationRequest", () => {  beforeEach(() => {    vi.unstubAllGlobals();    vi.stubGlobal("__DEV__", false);    secureStore.clear();    backgroundRuntime.pending.length = 0;    Constants.expoConfig!.extra = {};    __resetAgentAwarenessRemoteRegistrationForTest();    appStateMock.listeners.length = 0;    registrationRecordStore.current = null;    vi.mocked(saveAgentAwarenessRegistrationRecord).mockClear();    vi.mocked(loadAgentAwarenessRegistrationRecord).mockClear();    vi.mocked(clearAgentAwarenessRegistrationRecord).mockClear();    vi.mocked(loadOrCreateAgentAwarenessDeviceId).mockResolvedValue("device-1");    widgetMocks.getInstances.mockReset();    widgetMocks.getInstances.mockReturnValue([]);    widgetMocks.start.mockClear();    environmentConfigsMock.configs.clear();  });   it("preserves disabled Live Activity preferences in relay registrations", () => {    expect(      makeRelayDeviceRegistrationRequest({        deviceId: "device-1",        label: "Julius's iPhone",        iosMajorVersion: 18,        appVersion: "1.0.0",        pushToken: "apns-token",        pushToStartToken: "push-to-start-token",        notificationsEnabled: true,        preferences: {          liveActivitiesEnabled: false,        },      }),    ).toEqual({      deviceId: "device-1",      label: "Julius's iPhone",      platform: "ios",      iosMajorVersion: 18,      appVersion: "1.0.0",      pushToken: "apns-token",      pushToStartToken: "push-to-start-token",      preferences: {        liveActivitiesEnabled: false,        notificationsEnabled: true,        notifyOnApproval: true,        notifyOnInput: true,        notifyOnCompletion: true,        notifyOnFailure: true,      },    });  });   it("registers the app's APNs routing so the relay targets the right bundle", () => {    expect(      makeRelayDeviceRegistrationRequest({        deviceId: "device-1",        label: "Julius's iPhone",        iosMajorVersion: 18,        appVersion: "1.0.0",        bundleId: "com.t3tools.t3code.preview",        apsEnvironment: resolveApsEnvironment("preview"),        notificationsEnabled: true,        preferences: {},      }),    ).toMatchObject({      bundleId: "com.t3tools.t3code.preview",      apsEnvironment: "production",    });  });   it("routes development builds to the APNs sandbox", () => {    expect(resolveApsEnvironment("development")).toBe("sandbox");    expect(resolveApsEnvironment("preview")).toBe("production");    expect(resolveApsEnvironment("production")).toBe("production");    expect(resolveApsEnvironment(undefined)).toBe("production");  });   it("disables push features in Personal Team relay registrations", () => {    Constants.expoConfig!.extra = { iosPersonalTeamBuild: true };     expect(      makeRelayDeviceRegistrationRequest({        deviceId: "device-1",        label: "Julius's iPhone",        iosMajorVersion: 18,        appVersion: "1.0.0",        pushToken: "apns-token",        pushToStartToken: "push-to-start-token",        notificationsEnabled: true,        preferences: {},      }).preferences,    ).toMatchObject({      liveActivitiesEnabled: false,      notificationsEnabled: false,    });  });   it("marks notification delivery disabled when APNs permission is unavailable", () => {    expect(      makeRelayDeviceRegistrationRequest({        deviceId: "device-1",        label: "Julius's iPhone",        iosMajorVersion: 18,        appVersion: "1.0.0",        pushToStartToken: "push-to-start-token",        notificationsEnabled: false,        preferences: {          liveActivitiesEnabled: true,        },      }),    ).toEqual({      deviceId: "device-1",      label: "Julius's iPhone",      platform: "ios",      iosMajorVersion: 18,      appVersion: "1.0.0",      pushToStartToken: "push-to-start-token",      preferences: {        liveActivitiesEnabled: true,        notificationsEnabled: false,        notifyOnApproval: true,        notifyOnInput: true,        notifyOnCompletion: true,        notifyOnFailure: true,      },    });  });   it("normalizes relay base URLs for APNs registration requests", () => {    expect(normalizeAgentAwarenessRelayBaseUrl(" https://relay.example.test/// ")).toBe(      "https://relay.example.test",    );    expect(normalizeAgentAwarenessRelayBaseUrl("   ")).toBeNull();  });   it("overrides persisted preferences for an in-flight registration", () => {    expect(      mergeAgentAwarenessRegistrationPreferences(        { liveActivitiesEnabled: false, baseFontSize: 18 },        { liveActivitiesEnabled: true },      ),    ).toEqual({ liveActivitiesEnabled: true, baseFontSize: 18 });  });   it.effect("registers at most one listener while a Live Activity push token is pending", () => {    registerAgentAwarenessConnection(savedConnection());    const addPushTokenListener = vi.fn();    const activity = {      getPushToken: vi.fn(() => Promise.resolve(null)),      addPushTokenListener,    };     return Effect.gen(function* () {      expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false);      expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false);       expect(activity.getPushToken).toHaveBeenCalledTimes(2);      expect(addPushTokenListener).toHaveBeenCalledTimes(1);    }).pipe(Effect.provide(relayTestLayer));  });   it.effect("preserves Live Activity push-token lookup failures", () => {    const cause = new Error("native token lookup failed");    const activity = {      getPushToken: vi.fn(() => Promise.reject(cause)),      addPushTokenListener: vi.fn(),    };     return Effect.gen(function* () {      const error = yield* Effect.flip(        registerLiveActivityPushToken({ activity: activity as never }),      );       expect(error).toBeInstanceOf(AgentAwarenessOperationError);      expect(error).toMatchObject({        _tag: "AgentAwarenessOperationError",        operation: "read-live-activity-push-token",        cause,        message: "Agent awareness operation read-live-activity-push-token failed.",      });    }).pipe(Effect.provide(relayTestLayer));  });   it.effect(    "reports Live Activity token registration as skipped when relay auth is unavailable",    () => {      registerAgentAwarenessConnection(savedConnection());      const activity = {        getPushToken: vi.fn(() => Promise.resolve("activity-token")),        addPushTokenListener: vi.fn(),      };       return Effect.gen(function* () {        expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false);      }).pipe(Effect.provide(relayTestLayer));    },  );   it.effect(    "registers APNS-started Live Activities for relay updates without mutating them locally",    () => {      const activity = {        getPushToken: vi.fn(() => Promise.resolve("activity-token")),        addPushTokenListener: vi.fn(),        start: vi.fn(),        update: vi.fn(),        end: vi.fn(),      };      widgetMocks.getInstances.mockReturnValue([activity] as never);      setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));       return Effect.gen(function* () {        yield* refreshActiveLiveActivityRemoteRegistration();         expect(activity.getPushToken).toHaveBeenCalled();        expect(activity.start).not.toHaveBeenCalled();        expect(activity.update).not.toHaveBeenCalled();        expect(activity.end).not.toHaveBeenCalled();      }).pipe(Effect.provide(relayTestLayer));    },  );   it.effect(    "re-registers active Live Activity tokens when the app returns to the foreground",    () => {      const activity = {        getPushToken: vi.fn(() => Promise.resolve("activity-token")),        addPushTokenListener: vi.fn(),      };      widgetMocks.getInstances.mockReturnValue([activity] as never);      setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));       return Effect.gen(function* () {        yield* runBackgroundOperations();        activity.getPushToken.mockClear();         expect(appStateMock.listeners).toHaveLength(1);        for (const listener of appStateMock.listeners) {          listener("background");        }        yield* runBackgroundOperations();        expect(activity.getPushToken).not.toHaveBeenCalled();         for (const listener of appStateMock.listeners) {          listener("active");        }        yield* runBackgroundOperations();        expect(activity.getPushToken).toHaveBeenCalled();      }).pipe(Effect.provide(relayTestLayer));    },  );   it("ends local Live Activities and stops foreground reconciliation on cloud sign-out", () => {    const end = vi.fn(() => Promise.resolve());    const activity = {      getPushToken: vi.fn(() => Promise.resolve("activity-token")),      addPushTokenListener: vi.fn(),      end,    };    widgetMocks.getInstances.mockReturnValue([activity] as never);    setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));    expect(appStateMock.listeners).toHaveLength(1); 

Lines 1–500 of 935.