/// <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. Load more
/// <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);