| 1 | import { beforeEach, vi } from "vite-plus/test"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; |
| 6 | import * as Layer from "effect/Layer"; |
| 7 | import { HttpClient } from "effect/http"; |
| 8 | |
| 9 | import type { SavedRemoteConnection } from "../../lib/connection"; |
| 10 | import * as MobileStorage from "../../persistence/mobile-storage"; |
| 11 | import { |
| 12 | CloudEnvironmentLinkError, |
| 13 | linkEnvironmentToCloudWithPreference, |
| 14 | } from "../cloud/linkEnvironment"; |
| 15 | import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences"; |
| 16 | import { updateAgentAwarenessRegistrationPreferences } from "./remoteRegistration"; |
| 17 | |
| 18 | vi.mock("expo-secure-store", () => ({ |
| 19 | deleteItemAsync: vi.fn(), |
| 20 | getItemAsync: vi.fn(), |
| 21 | setItemAsync: vi.fn(), |
| 22 | })); |
| 23 | |
| 24 | vi.mock("react-native", () => ({ |
| 25 | Platform: { OS: "ios" }, |
| 26 | })); |
| 27 | |
| 28 | vi.mock("expo-constants", () => ({ |
| 29 | default: { expoConfig: {} }, |
| 30 | })); |
| 31 | |
| 32 | vi.mock("expo-device", () => ({})); |
| 33 | |
| 34 | vi.mock("../cloud/linkEnvironment", async (importOriginal) => ({ |
| 35 | ...(await importOriginal<typeof import("../cloud/linkEnvironment")>()), |
| 36 | linkEnvironmentToCloudWithPreference: vi.fn(() => Effect.void), |
| 37 | })); |
| 38 | |
| 39 | vi.mock("./remoteRegistration", () => ({ |
| 40 | updateAgentAwarenessRegistrationPreferences: vi.fn(() => Effect.void), |
| 41 | })); |
| 42 | |
| 43 | const connection: SavedRemoteConnection = { |
| 44 | environmentId: "env-1" as EnvironmentId, |
| 45 | environmentLabel: "Desktop", |
| 46 | pairingUrl: "https://desktop.example.test/", |
| 47 | displayUrl: "https://desktop.example.test/", |
| 48 | httpBaseUrl: "https://desktop.example.test/", |
| 49 | wsBaseUrl: "wss://desktop.example.test/ws", |
| 50 | bearerToken: "local-bearer", |
| 51 | }; |
| 52 | |
| 53 | const layerTest = Layer.mergeAll( |
| 54 | Layer.succeed(ManagedRelay.ManagedRelayClient, null as never), |
| 55 | Layer.succeed( |
| 56 | HttpClient.HttpClient, |
| 57 | HttpClient.make(() => Effect.die("unexpected HTTP request")), |
| 58 | ), |
| 59 | Layer.succeed( |
| 60 | MobileStorage.MobileStorage, |
| 61 | MobileStorage.MobileStorage.of({ |
| 62 | loadSavedConnections: Effect.succeed([]), |
| 63 | saveConnection: () => Effect.void, |
| 64 | clearSavedConnection: () => Effect.void, |
| 65 | loadOrCreateAgentAwarenessDeviceId: Effect.succeed("device-1"), |
| 66 | loadAgentAwarenessDeviceId: Effect.succeed("device-1"), |
| 67 | loadAgentAwarenessRegistrationRecord: Effect.succeed(null), |
| 68 | saveAgentAwarenessRegistrationRecord: () => Effect.void, |
| 69 | clearAgentAwarenessRegistrationRecord: Effect.void, |
| 70 | loadRecentThreadShortcuts: Effect.succeed([]), |
| 71 | saveRecentThreadShortcuts: () => Effect.void, |
| 72 | }), |
| 73 | ), |
| 74 | ); |
| 75 | |
| 76 | describe("liveActivityPreferences", () => { |
| 77 | beforeEach(() => { |
| 78 | vi.clearAllMocks(); |
| 79 | }); |
| 80 | |
| 81 | it.effect("pushes disabled Live Activity preferences to relay registrations", () => |
| 82 | Effect.gen(function* () { |
| 83 | yield* setLiveActivityUpdatesEnabled({ |
| 84 | enabled: false, |
| 85 | previousEnabled: true, |
| 86 | clerkToken: "clerk-token", |
| 87 | connections: [connection], |
| 88 | canConfigureEnvironment: () => true, |
| 89 | }); |
| 90 | |
| 91 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenCalledWith({ |
| 92 | liveActivitiesEnabled: false, |
| 93 | }); |
| 94 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledWith({ |
| 95 | clerkToken: "clerk-token", |
| 96 | connection, |
| 97 | liveActivitiesEnabled: false, |
| 98 | }); |
| 99 | }).pipe(Effect.provide(layerTest)), |
| 100 | ); |
| 101 | |
| 102 | it.effect("pushes enabled Live Activity preferences to relay registrations", () => |
| 103 | Effect.gen(function* () { |
| 104 | yield* setLiveActivityUpdatesEnabled({ |
| 105 | enabled: true, |
| 106 | previousEnabled: false, |
| 107 | clerkToken: "clerk-token", |
| 108 | connections: [connection], |
| 109 | canConfigureEnvironment: () => true, |
| 110 | }); |
| 111 | |
| 112 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenCalledWith({ |
| 113 | liveActivitiesEnabled: true, |
| 114 | }); |
| 115 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledWith({ |
| 116 | clerkToken: "clerk-token", |
| 117 | connection, |
| 118 | liveActivitiesEnabled: true, |
| 119 | }); |
| 120 | }).pipe(Effect.provide(layerTest)), |
| 121 | ); |
| 122 | |
| 123 | it.effect("keeps local preferences refreshable when signed out", () => |
| 124 | Effect.gen(function* () { |
| 125 | yield* setLiveActivityUpdatesEnabled({ |
| 126 | enabled: false, |
| 127 | previousEnabled: true, |
| 128 | clerkToken: null, |
| 129 | connections: [connection], |
| 130 | canConfigureEnvironment: () => true, |
| 131 | }); |
| 132 | |
| 133 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenCalledWith({ |
| 134 | liveActivitiesEnabled: false, |
| 135 | }); |
| 136 | expect(linkEnvironmentToCloudWithPreference).not.toHaveBeenCalled(); |
| 137 | }).pipe(Effect.provide(layerTest)), |
| 138 | ); |
| 139 | |
| 140 | it.effect("does not try to re-link managed relay connections without bearer credentials", () => { |
| 141 | const managedConnection: SavedRemoteConnection = { |
| 142 | ...connection, |
| 143 | bearerToken: null, |
| 144 | }; |
| 145 | |
| 146 | return Effect.gen(function* () { |
| 147 | yield* setLiveActivityUpdatesEnabled({ |
| 148 | enabled: true, |
| 149 | previousEnabled: false, |
| 150 | clerkToken: "clerk-token", |
| 151 | connections: [connection, managedConnection], |
| 152 | canConfigureEnvironment: () => true, |
| 153 | }); |
| 154 | |
| 155 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledTimes(1); |
| 156 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledWith({ |
| 157 | clerkToken: "clerk-token", |
| 158 | connection, |
| 159 | liveActivitiesEnabled: true, |
| 160 | }); |
| 161 | }).pipe(Effect.provide(layerTest)); |
| 162 | }); |
| 163 | |
| 164 | it.effect("restores relay preferences when an environment update fails", () => { |
| 165 | vi.mocked(linkEnvironmentToCloudWithPreference).mockImplementationOnce(() => |
| 166 | Effect.fail(new CloudEnvironmentLinkError({ message: "environment update failed" })), |
| 167 | ); |
| 168 | |
| 169 | return Effect.gen(function* () { |
| 170 | const exit = yield* Effect.exit( |
| 171 | setLiveActivityUpdatesEnabled({ |
| 172 | enabled: false, |
| 173 | previousEnabled: true, |
| 174 | clerkToken: "clerk-token", |
| 175 | connections: [connection], |
| 176 | canConfigureEnvironment: () => true, |
| 177 | }), |
| 178 | ); |
| 179 | |
| 180 | expect(exit._tag).toBe("Failure"); |
| 181 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenNthCalledWith(1, { |
| 182 | liveActivitiesEnabled: false, |
| 183 | }); |
| 184 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenNthCalledWith(2, { |
| 185 | liveActivitiesEnabled: true, |
| 186 | }); |
| 187 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenNthCalledWith(1, { |
| 188 | clerkToken: "clerk-token", |
| 189 | connection, |
| 190 | liveActivitiesEnabled: false, |
| 191 | }); |
| 192 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenNthCalledWith(2, { |
| 193 | clerkToken: "clerk-token", |
| 194 | connection, |
| 195 | liveActivitiesEnabled: true, |
| 196 | }); |
| 197 | }).pipe(Effect.provide(layerTest)); |
| 198 | }); |
| 199 | |
| 200 | it.effect("only re-links environments with relay access when enabling updates", () => { |
| 201 | const readOnlyConnection: SavedRemoteConnection = { |
| 202 | ...connection, |
| 203 | environmentId: "read-only" as EnvironmentId, |
| 204 | }; |
| 205 | |
| 206 | return Effect.gen(function* () { |
| 207 | yield* setLiveActivityUpdatesEnabled({ |
| 208 | enabled: true, |
| 209 | previousEnabled: false, |
| 210 | clerkToken: "clerk-token", |
| 211 | connections: [readOnlyConnection, connection], |
| 212 | canConfigureEnvironment: (environmentId) => environmentId === connection.environmentId, |
| 213 | }); |
| 214 | |
| 215 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenCalledWith({ |
| 216 | liveActivitiesEnabled: true, |
| 217 | }); |
| 218 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledExactlyOnceWith({ |
| 219 | clerkToken: "clerk-token", |
| 220 | connection, |
| 221 | liveActivitiesEnabled: true, |
| 222 | }); |
| 223 | }).pipe(Effect.provide(layerTest)); |
| 224 | }); |
| 225 | |
| 226 | it.effect("keeps device updates independently switchable without relay access", () => |
| 227 | Effect.gen(function* () { |
| 228 | for (const enabled of [false, true]) { |
| 229 | yield* setLiveActivityUpdatesEnabled({ |
| 230 | enabled, |
| 231 | previousEnabled: !enabled, |
| 232 | clerkToken: "clerk-token", |
| 233 | connections: [connection], |
| 234 | canConfigureEnvironment: () => false, |
| 235 | }); |
| 236 | } |
| 237 | |
| 238 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenNthCalledWith(1, { |
| 239 | liveActivitiesEnabled: false, |
| 240 | }); |
| 241 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenNthCalledWith(2, { |
| 242 | liveActivitiesEnabled: true, |
| 243 | }); |
| 244 | expect(linkEnvironmentToCloudWithPreference).not.toHaveBeenCalled(); |
| 245 | }).pipe(Effect.provide(layerTest)), |
| 246 | ); |
| 247 | |
| 248 | it.effect("checks current relay access after updating the device registration", () => { |
| 249 | let canConfigureEnvironment = true; |
| 250 | vi.mocked(updateAgentAwarenessRegistrationPreferences).mockImplementationOnce(() => |
| 251 | Effect.sync(() => { |
| 252 | canConfigureEnvironment = false; |
| 253 | }), |
| 254 | ); |
| 255 | |
| 256 | return Effect.gen(function* () { |
| 257 | yield* setLiveActivityUpdatesEnabled({ |
| 258 | enabled: false, |
| 259 | previousEnabled: true, |
| 260 | clerkToken: "clerk-token", |
| 261 | connections: [connection], |
| 262 | canConfigureEnvironment: () => canConfigureEnvironment, |
| 263 | }); |
| 264 | |
| 265 | expect(linkEnvironmentToCloudWithPreference).not.toHaveBeenCalled(); |
| 266 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenCalledTimes(1); |
| 267 | }).pipe(Effect.provide(layerTest)); |
| 268 | }); |
| 269 | |
| 270 | it.effect("does not retry environment changes after relay access is revoked", () => { |
| 271 | let canConfigureEnvironment = true; |
| 272 | vi.mocked(linkEnvironmentToCloudWithPreference).mockImplementationOnce(() => |
| 273 | Effect.sync(() => { |
| 274 | canConfigureEnvironment = false; |
| 275 | }).pipe( |
| 276 | Effect.andThen( |
| 277 | Effect.fail(new CloudEnvironmentLinkError({ message: "relay access revoked" })), |
| 278 | ), |
| 279 | ), |
| 280 | ); |
| 281 | |
| 282 | return Effect.gen(function* () { |
| 283 | const exit = yield* Effect.exit( |
| 284 | setLiveActivityUpdatesEnabled({ |
| 285 | enabled: false, |
| 286 | previousEnabled: true, |
| 287 | clerkToken: "clerk-token", |
| 288 | connections: [connection], |
| 289 | canConfigureEnvironment: () => canConfigureEnvironment, |
| 290 | }), |
| 291 | ); |
| 292 | |
| 293 | expect(exit._tag).toBe("Failure"); |
| 294 | expect(linkEnvironmentToCloudWithPreference).toHaveBeenCalledTimes(1); |
| 295 | expect(updateAgentAwarenessRegistrationPreferences).toHaveBeenNthCalledWith(2, { |
| 296 | liveActivitiesEnabled: true, |
| 297 | }); |
| 298 | }).pipe(Effect.provide(layerTest)); |
| 299 | }); |
| 300 | }); |