1import { beforeEach, vi } from "vite-plus/test";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import type { EnvironmentId } from "@t3tools/contracts";
5import { ManagedRelay } from "@t3tools/client-runtime/relay";
6import * as Layer from "effect/Layer";
7import { HttpClient } from "effect/http";
8
9import type { SavedRemoteConnection } from "../../lib/connection";
10import * as MobileStorage from "../../persistence/mobile-storage";
11import {
12 CloudEnvironmentLinkError,
13 linkEnvironmentToCloudWithPreference,
14} from "../cloud/linkEnvironment";
15import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
16import { updateAgentAwarenessRegistrationPreferences } from "./remoteRegistration";
17
18vi.mock("expo-secure-store", () => ({
19 deleteItemAsync: vi.fn(),
20 getItemAsync: vi.fn(),
21 setItemAsync: vi.fn(),
22}));
23
24vi.mock("react-native", () => ({
25 Platform: { OS: "ios" },
26}));
27
28vi.mock("expo-constants", () => ({
29 default: { expoConfig: {} },
30}));
31
32vi.mock("expo-device", () => ({}));
33
34vi.mock("../cloud/linkEnvironment", async (importOriginal) => ({
35 ...(await importOriginal<typeof import("../cloud/linkEnvironment")>()),
36 linkEnvironmentToCloudWithPreference: vi.fn(() => Effect.void),
37}));
38
39vi.mock("./remoteRegistration", () => ({
40 updateAgentAwarenessRegistrationPreferences: vi.fn(() => Effect.void),
41}));
42
43const 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
53const 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
76describe("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});