@rozenite/tanstack-query-plugin 1.5.1 → 1.7.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +22 -0
  3. package/dist/{tanstack-query.html → devtools/tanstack-query.html} +1 -1
  4. package/dist/react-native/chunks/useTanStackQueryDevTools.require.cjs +1 -0
  5. package/dist/react-native/chunks/useTanStackQueryDevTools.require.js +862 -0
  6. package/dist/{react-native.cjs → react-native/index.cjs} +1 -1
  7. package/dist/react-native/index.d.ts +7 -0
  8. package/dist/react-native/index.js +6 -0
  9. package/dist/rozenite.json +1 -1
  10. package/package.json +27 -7
  11. package/src/react-native/agent/__tests__/tanstack-query-agent.test.ts +508 -0
  12. package/src/react-native/agent/tanstack-query-agent.ts +750 -0
  13. package/src/react-native/agent/useTanStackQueryAgentTools.ts +147 -0
  14. package/src/react-native/devtools-actions.ts +197 -0
  15. package/src/react-native/useHandleDevToolsMessages.ts +9 -104
  16. package/src/react-native/useTanStackQueryDevTools.ts +3 -0
  17. package/tsconfig.json +3 -0
  18. package/dist/react-native.d.ts +0 -1
  19. package/dist/react-native.js +0 -6
  20. package/dist/rozenite.config.d.ts +0 -7
  21. package/dist/src/react-native/useHandleDevToolsMessages.d.ts +0 -3
  22. package/dist/src/react-native/useHandleInitialData.d.ts +0 -3
  23. package/dist/src/react-native/useSyncTanStackCache.d.ts +0 -3
  24. package/dist/src/react-native/useTanStackQueryDevTools.d.ts +0 -2
  25. package/dist/src/shared/dehydrate.d.ts +0 -6
  26. package/dist/src/shared/hydrate.d.ts +0 -10
  27. package/dist/src/shared/messaging.d.ts +0 -34
  28. package/dist/src/shared/types.d.ts +0 -25
  29. package/dist/src/shared/useSyncOnlineStatus.d.ts +0 -2
  30. package/dist/src/ui/tanstack-query.d.ts +0 -1
  31. package/dist/src/ui/useHandleSyncMessages.d.ts +0 -2
  32. package/dist/src/ui/useSyncDevToolsEvents.d.ts +0 -2
  33. package/dist/src/ui/useSyncInitialData.d.ts +0 -2
  34. package/dist/useTanStackQueryDevTools.cjs +0 -1
  35. package/dist/useTanStackQueryDevTools.js +0 -312
  36. /package/dist/{assets → devtools/assets}/CXEL7IU7-DJlSmt0p.js +0 -0
  37. /package/dist/{assets → devtools/assets}/tanstack-query-_leiFatc.js +0 -0
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});exports.useTanStackQueryDevTools=void 0;const e=process.env.NODE_ENV!=="production",o=typeof window>"u";e&&!o?exports.useTanStackQueryDevTools=require("./useTanStackQueryDevTools.cjs").useTanStackQueryDevTools:exports.useTanStackQueryDevTools=()=>({isConnected:!1});
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});exports.useTanStackQueryDevTools=void 0;const e=process.env.NODE_ENV!=="production",o=typeof window>"u";e&&!o?exports.useTanStackQueryDevTools=require("./chunks/useTanStackQueryDevTools.require.cjs").useTanStackQueryDevTools:exports.useTanStackQueryDevTools=()=>({isConnected:!1});
@@ -0,0 +1,7 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+
3
+ export declare let useTanStackQueryDevTools: useTanStackQueryDevTools_2;
4
+
5
+ declare const useTanStackQueryDevTools_2: (queryClient: QueryClient) => void;
6
+
7
+ export { }
@@ -0,0 +1,6 @@
1
+ let e;
2
+ const o = process.env.NODE_ENV !== "production", s = typeof window > "u";
3
+ o && !s ? e = require("./chunks/useTanStackQueryDevTools.require.js").useTanStackQueryDevTools : e = () => ({ isConnected: !1 });
4
+ export {
5
+ e as useTanStackQueryDevTools
6
+ };
@@ -1 +1 @@
1
- {"name":"@rozenite/tanstack-query-plugin","version":"1.5.0","description":"TanStack Query for Rozenite.","panels":[{"name":"Tanstack Query","source":"/tanstack-query.html"}]}
1
+ {"name":"@rozenite/tanstack-query-plugin","version":"1.7.0-rc.0","description":"TanStack Query for Rozenite.","panels":[{"name":"Tanstack Query","source":"/devtools/tanstack-query.html"}]}
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@rozenite/tanstack-query-plugin",
3
- "version": "1.5.1",
3
+ "version": "1.7.0-rc.0",
4
4
  "description": "TanStack Query for Rozenite.",
5
5
  "type": "module",
6
- "main": "./dist/react-native.cjs",
7
- "module": "./dist/react-native.js",
8
- "types": "./dist/react-native.d.ts",
6
+ "main": "./dist/react-native/index.cjs",
7
+ "module": "./dist/react-native/index.js",
8
+ "types": "./dist/react-native/index.d.ts",
9
+ "homepage": "https://github.com/callstackincubator/rozenite#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/callstackincubator/rozenite/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/callstackincubator/rozenite.git"
16
+ },
9
17
  "dependencies": {
10
18
  "fast-deep-equal": "^3.1.3",
11
- "@rozenite/plugin-bridge": "1.5.1"
19
+ "@rozenite/agent-bridge": "1.7.0-rc.0",
20
+ "@rozenite/plugin-bridge": "1.7.0-rc.0"
12
21
  },
13
22
  "devDependencies": {
14
23
  "@tanstack/react-query": "^5.0.0",
@@ -24,8 +33,8 @@
24
33
  "tailwindcss-animate": "^1.0.7",
25
34
  "typescript": "~5.9.3",
26
35
  "vite": "^7.3.1",
27
- "@rozenite/vite-plugin": "1.5.1",
28
- "rozenite": "1.5.1"
36
+ "@rozenite/vite-plugin": "1.7.0-rc.0",
37
+ "rozenite": "1.7.0-rc.0"
29
38
  },
30
39
  "peerDependencies": {
31
40
  "@tanstack/react-query": "^5.0.0",
@@ -33,6 +42,17 @@
33
42
  "react-native": "*"
34
43
  },
35
44
  "license": "MIT",
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/react-native/index.d.ts",
48
+ "import": "./dist/react-native/index.js",
49
+ "require": "./dist/react-native/index.cjs"
50
+ },
51
+ "./package.json": "./package.json"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
36
56
  "scripts": {
37
57
  "build": "rozenite build",
38
58
  "dev": "rozenite dev",
@@ -0,0 +1,508 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ Mutation,
4
+ QueryClient,
5
+ QueryObserver,
6
+ onlineManager,
7
+ } from '@tanstack/react-query';
8
+ import {
9
+ applyTanStackQueryDevtoolsAction,
10
+ } from '../../devtools-actions';
11
+ import {
12
+ createTanStackQueryAgentController,
13
+ serializeForAgent,
14
+ TANSTACK_QUERY_AGENT_TOOLS,
15
+ } from '../tanstack-query-agent';
16
+
17
+ const createQueryClient = () =>
18
+ new QueryClient({
19
+ defaultOptions: {
20
+ queries: {
21
+ retry: false,
22
+ gcTime: Infinity,
23
+ },
24
+ mutations: {
25
+ retry: false,
26
+ gcTime: Infinity,
27
+ },
28
+ },
29
+ });
30
+
31
+ const subscribeController = (
32
+ queryClient: QueryClient,
33
+ controller: ReturnType<typeof createTanStackQueryAgentController>
34
+ ) => {
35
+ const unsubscribeQuery = queryClient
36
+ .getQueryCache()
37
+ .subscribe((event) => controller.handleQueryCacheEvent(event));
38
+ const unsubscribeMutation = queryClient
39
+ .getMutationCache()
40
+ .subscribe((event) => controller.handleMutationCacheEvent(event));
41
+
42
+ return () => {
43
+ unsubscribeQuery();
44
+ unsubscribeMutation();
45
+ };
46
+ };
47
+
48
+ const createMutation = (
49
+ queryClient: QueryClient,
50
+ options?: {
51
+ mutationKey?: unknown[];
52
+ status?: 'idle' | 'pending' | 'success' | 'error';
53
+ submittedAt?: number;
54
+ isPaused?: boolean;
55
+ variables?: unknown;
56
+ data?: unknown;
57
+ error?: Error | null;
58
+ context?: unknown;
59
+ failureCount?: number;
60
+ failureReason?: Error | null;
61
+ }
62
+ ) => {
63
+ return queryClient.getMutationCache().build(
64
+ queryClient,
65
+ {
66
+ mutationKey: options?.mutationKey ?? ['mutation'],
67
+ mutationFn: async () => ({ ok: true }),
68
+ onSuccess: () => undefined,
69
+ meta: { source: 'test' },
70
+ },
71
+ {
72
+ context: options?.context,
73
+ data: options?.data,
74
+ error: options?.error ?? null,
75
+ failureCount: options?.failureCount ?? 0,
76
+ failureReason: options?.failureReason ?? null,
77
+ isPaused: options?.isPaused ?? false,
78
+ status: options?.status ?? 'success',
79
+ submittedAt: options?.submittedAt ?? Date.now(),
80
+ variables: options?.variables,
81
+ } as any
82
+ ) as Mutation<unknown, Error, unknown, unknown>;
83
+ };
84
+
85
+ afterEach(() => {
86
+ vi.restoreAllMocks();
87
+ onlineManager.setOnline(true);
88
+ });
89
+
90
+ describe('tanstack query agent controller', () => {
91
+ it('exposes the expected tool names', () => {
92
+ expect(TANSTACK_QUERY_AGENT_TOOLS.map((tool) => tool.name)).toEqual([
93
+ 'get-cache-summary',
94
+ 'get-online-status',
95
+ 'set-online-status',
96
+ 'list-queries',
97
+ 'get-query-details',
98
+ 'refetch-query',
99
+ 'set-query-loading',
100
+ 'set-query-error',
101
+ 'invalidate-query',
102
+ 'reset-query',
103
+ 'remove-query',
104
+ 'clear-query-cache',
105
+ 'list-mutations',
106
+ 'get-mutation-details',
107
+ 'clear-mutation-cache',
108
+ ]);
109
+ });
110
+
111
+ it('lists queries with cursor pagination and invalidates stale cursors after additions', async () => {
112
+ const queryClient = createQueryClient();
113
+ const controller = createTanStackQueryAgentController(queryClient);
114
+ const unsubscribe = subscribeController(queryClient, controller);
115
+
116
+ queryClient.setQueryData(['first'], { value: 1 });
117
+ queryClient.setQueryData(['second'], { value: 2 });
118
+
119
+ const firstPage = controller.listQueries({ limit: 1 });
120
+ expect(firstPage.items).toHaveLength(1);
121
+ expect(firstPage.page.hasMore).toBe(true);
122
+
123
+ queryClient.setQueryData(['third'], { value: 3 });
124
+
125
+ expect(() =>
126
+ controller.listQueries({
127
+ limit: 1,
128
+ cursor: firstPage.page.nextCursor,
129
+ })
130
+ ).toThrow('Cursor does not match the requested listing. Run the command again.');
131
+
132
+ unsubscribe();
133
+ });
134
+
135
+ it('invalidates query cursors after removals and cache clears', () => {
136
+ const queryClient = createQueryClient();
137
+ const controller = createTanStackQueryAgentController(queryClient);
138
+ const unsubscribe = subscribeController(queryClient, controller);
139
+
140
+ queryClient.setQueryData(['first'], { value: 1 });
141
+ queryClient.setQueryData(['second'], { value: 2 });
142
+
143
+ const firstPage = controller.listQueries({ limit: 1 });
144
+ queryClient.removeQueries({ queryKey: ['second'] });
145
+
146
+ expect(() =>
147
+ controller.listQueries({
148
+ limit: 1,
149
+ cursor: firstPage.page.nextCursor,
150
+ })
151
+ ).toThrow('Cursor does not match the requested listing. Run the command again.');
152
+
153
+ queryClient.setQueryData(['third'], { value: 3 });
154
+ queryClient.setQueryData(['fourth'], { value: 4 });
155
+ const secondPageSeed = controller.listQueries({ limit: 1 });
156
+ queryClient.getQueryCache().clear();
157
+
158
+ expect(() =>
159
+ controller.listQueries({
160
+ limit: 1,
161
+ cursor: secondPageSeed.page.nextCursor,
162
+ })
163
+ ).toThrow('Cursor does not match the requested listing. Run the command again.');
164
+
165
+ unsubscribe();
166
+ });
167
+
168
+ it('lists mutations with cursor pagination and invalidates stale cursors after structural changes', () => {
169
+ const queryClient = createQueryClient();
170
+ const controller = createTanStackQueryAgentController(queryClient);
171
+ const unsubscribe = subscribeController(queryClient, controller);
172
+
173
+ createMutation(queryClient, {
174
+ mutationKey: ['first'],
175
+ submittedAt: 100,
176
+ data: { ok: true },
177
+ });
178
+ createMutation(queryClient, {
179
+ mutationKey: ['second'],
180
+ submittedAt: 200,
181
+ data: { ok: true },
182
+ });
183
+
184
+ const firstPage = controller.listMutations({ limit: 1 });
185
+ expect(firstPage.items).toHaveLength(1);
186
+
187
+ createMutation(queryClient, {
188
+ mutationKey: ['third'],
189
+ submittedAt: 300,
190
+ data: { ok: true },
191
+ });
192
+
193
+ expect(() =>
194
+ controller.listMutations({
195
+ limit: 1,
196
+ cursor: firstPage.page.nextCursor,
197
+ })
198
+ ).toThrow('Cursor does not match the requested listing. Run the command again.');
199
+
200
+ const mutation = queryClient.getMutationCache().getAll()[0];
201
+ queryClient.getMutationCache().remove(mutation);
202
+
203
+ const secondPageSeed = controller.listMutations({ limit: 1 });
204
+ queryClient.getMutationCache().clear();
205
+
206
+ expect(() =>
207
+ controller.listMutations({
208
+ limit: 1,
209
+ cursor: secondPageSeed.page.nextCursor,
210
+ })
211
+ ).toThrow('Cursor does not match the requested listing. Run the command again.');
212
+
213
+ unsubscribe();
214
+ });
215
+
216
+ it('returns JSON-safe query details with curated observer summaries', async () => {
217
+ const queryClient = createQueryClient();
218
+ const controller = createTanStackQueryAgentController(queryClient);
219
+ const unsubscribe = subscribeController(queryClient, controller);
220
+ const queryFn = vi.fn(async () => ({
221
+ ok: true,
222
+ nested: { fn: () => 'ignored' },
223
+ }));
224
+
225
+ const observer = new QueryObserver(queryClient, {
226
+ queryKey: ['todos', 1],
227
+ queryFn,
228
+ select: (data) => data,
229
+ meta: { source: 'observer' },
230
+ });
231
+ const observerUnsubscribe = observer.subscribe(() => undefined);
232
+ await queryClient.fetchQuery({
233
+ queryKey: ['todos', 1],
234
+ queryFn,
235
+ });
236
+
237
+ const queryHash = queryClient.getQueryCache().find({ queryKey: ['todos', 1] })!
238
+ .queryHash;
239
+ const details = controller.getQueryDetails({ queryHash });
240
+
241
+ expect(details.query).toMatchObject({
242
+ queryHash,
243
+ queryKey: ['todos', 1],
244
+ observersCount: 1,
245
+ data: {
246
+ ok: true,
247
+ nested: {
248
+ fn: '[non-serializable:function]',
249
+ },
250
+ },
251
+ });
252
+ expect(details.query.observers[0]?.options).toMatchObject({
253
+ hasQueryFn: true,
254
+ hasSelect: true,
255
+ meta: { source: 'observer' },
256
+ });
257
+
258
+ observerUnsubscribe();
259
+ unsubscribe();
260
+ });
261
+
262
+ it('returns JSON-safe mutation details and validates unknown ids', () => {
263
+ const queryClient = createQueryClient();
264
+ const controller = createTanStackQueryAgentController(queryClient);
265
+ const unsubscribe = subscribeController(queryClient, controller);
266
+
267
+ const mutation = createMutation(queryClient, {
268
+ mutationKey: ['save'],
269
+ status: 'error',
270
+ submittedAt: 123,
271
+ variables: { id: 1, fn: () => undefined },
272
+ context: { optimistic: true },
273
+ error: new Error('boom'),
274
+ failureCount: 2,
275
+ failureReason: new Error('network'),
276
+ });
277
+
278
+ const details = controller.getMutationDetails({
279
+ mutationId: mutation.mutationId,
280
+ });
281
+
282
+ expect(details.mutation).toMatchObject({
283
+ mutationId: mutation.mutationId,
284
+ mutationKey: ['save'],
285
+ status: 'error',
286
+ variables: {
287
+ id: 1,
288
+ fn: '[non-serializable:function]',
289
+ },
290
+ error: {
291
+ message: 'boom',
292
+ },
293
+ failureReason: {
294
+ message: 'network',
295
+ },
296
+ options: {
297
+ hasMutationFn: true,
298
+ hasOnSuccess: true,
299
+ },
300
+ });
301
+
302
+ expect(() =>
303
+ controller.getMutationDetails({ mutationId: 9999 })
304
+ ).toThrow('Unknown mutationId "9999"');
305
+
306
+ unsubscribe();
307
+ });
308
+
309
+ it('validates unknown query hashes', () => {
310
+ const queryClient = createQueryClient();
311
+ const controller = createTanStackQueryAgentController(queryClient);
312
+
313
+ expect(() =>
314
+ controller.getQueryDetails({ queryHash: 'missing' })
315
+ ).toThrow('Unknown queryHash "missing"');
316
+ });
317
+
318
+ it('gets and sets the online manager status', () => {
319
+ const queryClient = createQueryClient();
320
+ const controller = createTanStackQueryAgentController(queryClient);
321
+
322
+ expect(controller.getOnlineStatus()).toEqual({ online: true });
323
+ expect(controller.setOnlineStatus({ online: false })).toEqual({
324
+ online: false,
325
+ });
326
+ expect(controller.getOnlineStatus()).toEqual({ online: false });
327
+ });
328
+ });
329
+
330
+ describe('tanstack query agent actions', () => {
331
+ it('refetches a query', async () => {
332
+ const queryClient = createQueryClient();
333
+ const queryFn = vi.fn(async () => ({ ok: true }));
334
+
335
+ await queryClient.fetchQuery({
336
+ queryKey: ['refetch-me'],
337
+ queryFn,
338
+ });
339
+
340
+ const queryHash = queryClient.getQueryCache().find({
341
+ queryKey: ['refetch-me'],
342
+ })!.queryHash;
343
+
344
+ const result = await applyTanStackQueryDevtoolsAction(queryClient, {
345
+ type: 'REFETCH',
346
+ queryHash,
347
+ });
348
+
349
+ expect(result).toMatchObject({
350
+ applied: true,
351
+ action: 'REFETCH',
352
+ queryHash,
353
+ });
354
+ expect(queryFn).toHaveBeenCalledTimes(2);
355
+ });
356
+
357
+ it('sets and restores query loading and error states through boolean tools', async () => {
358
+ const queryClient = createQueryClient();
359
+ const queryFn = vi.fn(async () => ({ ok: true }));
360
+ const controller = createTanStackQueryAgentController(queryClient);
361
+
362
+ await queryClient.fetchQuery({
363
+ queryKey: ['simulate-me'],
364
+ queryFn,
365
+ });
366
+
367
+ const query = queryClient.getQueryCache().find({
368
+ queryKey: ['simulate-me'],
369
+ })!;
370
+ const queryHash = query.queryHash;
371
+
372
+ const loadingResult = await controller.setQueryLoading({
373
+ queryHash,
374
+ enabled: true,
375
+ });
376
+ expect(loadingResult).toMatchObject({
377
+ applied: true,
378
+ action: 'TRIGGER_LOADING',
379
+ queryHash,
380
+ });
381
+ expect(query.state.status).toBe('pending');
382
+
383
+ const restoreLoadingResult = await controller.setQueryLoading({
384
+ queryHash,
385
+ enabled: false,
386
+ });
387
+ expect(restoreLoadingResult).toMatchObject({
388
+ applied: true,
389
+ action: 'RESTORE_LOADING',
390
+ queryHash,
391
+ });
392
+
393
+ const errorResult = await controller.setQueryError({
394
+ queryHash,
395
+ enabled: true,
396
+ });
397
+ expect(errorResult).toMatchObject({
398
+ applied: true,
399
+ action: 'TRIGGER_ERROR',
400
+ queryHash,
401
+ });
402
+ expect(query.state.status).toBe('error');
403
+
404
+ const restoreErrorResult = await controller.setQueryError({
405
+ queryHash,
406
+ enabled: false,
407
+ });
408
+ expect(restoreErrorResult).toMatchObject({
409
+ applied: true,
410
+ action: 'RESTORE_ERROR',
411
+ queryHash,
412
+ });
413
+ });
414
+
415
+ it('invalidates, resets, removes, and clears query cache entries', async () => {
416
+ const queryClient = createQueryClient();
417
+ const queryFn = vi.fn(async () => 'fresh');
418
+
419
+ await queryClient.fetchQuery({
420
+ queryKey: ['query-actions'],
421
+ queryFn,
422
+ initialData: 'seed',
423
+ });
424
+
425
+ const query = queryClient.getQueryCache().find({
426
+ queryKey: ['query-actions'],
427
+ })!;
428
+ const queryHash = query.queryHash;
429
+
430
+ await applyTanStackQueryDevtoolsAction(queryClient, {
431
+ type: 'INVALIDATE',
432
+ queryHash,
433
+ });
434
+ expect(query.state.isInvalidated).toBe(true);
435
+
436
+ queryClient.setQueryData(['query-actions'], 'changed');
437
+ await applyTanStackQueryDevtoolsAction(queryClient, {
438
+ type: 'RESET',
439
+ queryHash,
440
+ });
441
+ expect(queryClient.getQueryCache().get(queryHash)).toBeTruthy();
442
+
443
+ await applyTanStackQueryDevtoolsAction(queryClient, {
444
+ type: 'REMOVE',
445
+ queryHash,
446
+ });
447
+ expect(queryClient.getQueryCache().get(queryHash)).toBeUndefined();
448
+
449
+ queryClient.setQueryData(['clear-one'], 1);
450
+ queryClient.setQueryData(['clear-two'], 2);
451
+
452
+ const result = await applyTanStackQueryDevtoolsAction(queryClient, {
453
+ type: 'CLEAR_QUERY_CACHE',
454
+ });
455
+ expect(result).toEqual({
456
+ applied: true,
457
+ action: 'CLEAR_QUERY_CACHE',
458
+ cleared: true,
459
+ queryCountBefore: 2,
460
+ queryCountAfter: 0,
461
+ });
462
+ });
463
+
464
+ it('clears mutation cache through the shared UI/agent dispatcher', async () => {
465
+ const queryClient = createQueryClient();
466
+ createMutation(queryClient, {
467
+ mutationKey: ['one'],
468
+ submittedAt: 1,
469
+ data: { ok: true },
470
+ });
471
+ createMutation(queryClient, {
472
+ mutationKey: ['two'],
473
+ submittedAt: 2,
474
+ data: { ok: true },
475
+ });
476
+
477
+ const result = await applyTanStackQueryDevtoolsAction(queryClient, {
478
+ type: 'CLEAR_MUTATION_CACHE',
479
+ });
480
+
481
+ expect(result).toEqual({
482
+ applied: true,
483
+ action: 'CLEAR_MUTATION_CACHE',
484
+ cleared: true,
485
+ mutationCountBefore: 2,
486
+ mutationCountAfter: 0,
487
+ });
488
+ });
489
+ });
490
+
491
+ describe('serializeForAgent', () => {
492
+ it('handles circular values and class instances safely', () => {
493
+ class Example {
494
+ value = 1;
495
+ }
496
+
497
+ const circular: Record<string, unknown> = { ok: true };
498
+ circular.self = circular;
499
+
500
+ expect(serializeForAgent(circular)).toEqual({
501
+ ok: true,
502
+ self: '[circular]',
503
+ });
504
+ expect(serializeForAgent(new Example())).toBe(
505
+ '[non-serializable:Example]'
506
+ );
507
+ });
508
+ });