@robosystems/client 0.2.46 → 0.2.48
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.
- package/extensions/AgentClient.test.ts +403 -0
- package/extensions/LedgerClient.d.ts +16 -1
- package/extensions/LedgerClient.js +33 -0
- package/extensions/LedgerClient.test.ts +1138 -0
- package/extensions/LedgerClient.ts +47 -0
- package/extensions/OperationClient.test.ts +199 -1
- package/extensions/QueryClient.test.ts +169 -0
- package/extensions/ReportClient.test.ts +828 -0
- package/extensions/hooks.test.ts +373 -0
- package/extensions/index.test.ts +279 -60
- package/index.ts +2 -2
- package/package.json +1 -1
- package/sdk/index.d.ts +2 -2
- package/sdk/index.js +7 -5
- package/sdk/index.ts +2 -2
- package/sdk/sdk.gen.d.ts +35 -10
- package/sdk/sdk.gen.js +47 -12
- package/sdk/sdk.gen.ts +45 -10
- package/sdk/types.gen.d.ts +249 -7
- package/sdk/types.gen.ts +265 -7
- package/sdk-extensions/AgentClient.test.ts +403 -0
- package/sdk-extensions/LedgerClient.d.ts +16 -1
- package/sdk-extensions/LedgerClient.js +33 -0
- package/sdk-extensions/LedgerClient.test.ts +1138 -0
- package/sdk-extensions/LedgerClient.ts +47 -0
- package/sdk-extensions/OperationClient.test.ts +199 -1
- package/sdk-extensions/QueryClient.test.ts +169 -0
- package/sdk-extensions/ReportClient.test.ts +828 -0
- package/sdk-extensions/hooks.test.ts +373 -0
- package/sdk-extensions/index.test.ts +279 -60
- package/sdk.gen.d.ts +35 -10
- package/sdk.gen.js +47 -12
- package/sdk.gen.ts +45 -10
- package/types.gen.d.ts +249 -7
- package/types.gen.ts +265 -7
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
useMultipleOperations,
|
|
5
|
+
useOperation,
|
|
6
|
+
useQuery,
|
|
7
|
+
useSDKClients,
|
|
8
|
+
useStreamingQuery,
|
|
9
|
+
} from './hooks'
|
|
10
|
+
|
|
11
|
+
// Mock the SDK client
|
|
12
|
+
vi.mock('../sdk/client.gen', () => ({
|
|
13
|
+
client: {
|
|
14
|
+
getConfig: vi.fn(() => ({
|
|
15
|
+
baseUrl: 'http://mock-sdk.com',
|
|
16
|
+
headers: {},
|
|
17
|
+
})),
|
|
18
|
+
},
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
// Mock the config module
|
|
22
|
+
vi.mock('./config', () => ({
|
|
23
|
+
getSDKExtensionsConfig: vi.fn(() => ({
|
|
24
|
+
baseUrl: 'http://test-api.com',
|
|
25
|
+
credentials: 'include',
|
|
26
|
+
headers: {},
|
|
27
|
+
token: 'test-token',
|
|
28
|
+
maxRetries: 3,
|
|
29
|
+
retryDelay: 1000,
|
|
30
|
+
})),
|
|
31
|
+
extractTokenFromSDKClient: vi.fn(() => 'test-token'),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
// Mock the SDK gen functions to prevent real HTTP calls
|
|
35
|
+
vi.mock('../sdk/sdk.gen', () => ({
|
|
36
|
+
executeCypherQuery: vi.fn(),
|
|
37
|
+
getOperationStatus: vi.fn(),
|
|
38
|
+
cancelOperation: vi.fn(),
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
// Create a mock response helper
|
|
42
|
+
function createMockResponse(data: any, options: { ok?: boolean; status?: number } = {}) {
|
|
43
|
+
return {
|
|
44
|
+
ok: options.ok ?? true,
|
|
45
|
+
status: options.status ?? 200,
|
|
46
|
+
statusText: 'OK',
|
|
47
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
48
|
+
json: async () => data,
|
|
49
|
+
text: async () => JSON.stringify(data),
|
|
50
|
+
blob: async () => new Blob([JSON.stringify(data)]),
|
|
51
|
+
arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(data)).buffer,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('useQuery', () => {
|
|
56
|
+
let mockFetch: any
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks()
|
|
60
|
+
mockFetch = vi.fn()
|
|
61
|
+
global.fetch = mockFetch
|
|
62
|
+
globalThis.fetch = mockFetch
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should initialize with default state', () => {
|
|
66
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
67
|
+
|
|
68
|
+
expect(result.current.loading).toBe(false)
|
|
69
|
+
expect(result.current.error).toBeNull()
|
|
70
|
+
expect(result.current.data).toBeNull()
|
|
71
|
+
expect(result.current.queuePosition).toBeNull()
|
|
72
|
+
expect(typeof result.current.execute).toBe('function')
|
|
73
|
+
expect(typeof result.current.query).toBe('function')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should execute a query and return data', async () => {
|
|
77
|
+
const mockResult = {
|
|
78
|
+
data: [{ name: 'Alice' }],
|
|
79
|
+
columns: ['name'],
|
|
80
|
+
row_count: 1,
|
|
81
|
+
execution_time_ms: 10,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
mockFetch.mockResolvedValue(createMockResponse(mockResult))
|
|
85
|
+
|
|
86
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
87
|
+
|
|
88
|
+
await act(async () => {
|
|
89
|
+
const queryResult = await result.current.execute('MATCH (n) RETURN n')
|
|
90
|
+
expect(queryResult).not.toBeNull()
|
|
91
|
+
expect(queryResult!.data).toEqual([{ name: 'Alice' }])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(result.current.loading).toBe(false)
|
|
95
|
+
expect(result.current.error).toBeNull()
|
|
96
|
+
expect(result.current.data).not.toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should set loading state during execution', async () => {
|
|
100
|
+
let resolvePromise: (value: any) => void
|
|
101
|
+
const pending = new Promise((resolve) => {
|
|
102
|
+
resolvePromise = resolve
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
mockFetch.mockReturnValue(pending)
|
|
106
|
+
|
|
107
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
108
|
+
|
|
109
|
+
// Start the query (don't await)
|
|
110
|
+
act(() => {
|
|
111
|
+
result.current.execute('MATCH (n) RETURN n')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Should be loading while pending
|
|
115
|
+
expect(result.current.loading).toBe(true)
|
|
116
|
+
|
|
117
|
+
// Resolve
|
|
118
|
+
await act(async () => {
|
|
119
|
+
resolvePromise!(
|
|
120
|
+
createMockResponse({
|
|
121
|
+
data: [],
|
|
122
|
+
columns: [],
|
|
123
|
+
row_count: 0,
|
|
124
|
+
execution_time_ms: 0,
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(result.current.loading).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should set error state on failure', async () => {
|
|
133
|
+
mockFetch.mockRejectedValue(new Error('Network error'))
|
|
134
|
+
|
|
135
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
const queryResult = await result.current.execute('BAD QUERY')
|
|
139
|
+
expect(queryResult).toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(result.current.error).not.toBeNull()
|
|
143
|
+
expect(result.current.loading).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should provide a simple query method that returns data array', async () => {
|
|
147
|
+
mockFetch.mockResolvedValue(
|
|
148
|
+
createMockResponse({
|
|
149
|
+
data: [{ id: 1 }, { id: 2 }],
|
|
150
|
+
columns: ['id'],
|
|
151
|
+
row_count: 2,
|
|
152
|
+
execution_time_ms: 5,
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
157
|
+
|
|
158
|
+
let data: any[]
|
|
159
|
+
await act(async () => {
|
|
160
|
+
data = await result.current.query('MATCH (n) RETURN n')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(data!).toEqual([{ id: 1 }, { id: 2 }])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should return empty array from query on failure', async () => {
|
|
167
|
+
mockFetch.mockRejectedValue(new Error('fail'))
|
|
168
|
+
|
|
169
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
170
|
+
|
|
171
|
+
let data: any[]
|
|
172
|
+
await act(async () => {
|
|
173
|
+
data = await result.current.query('BAD')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(data!).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should clear previous data on new execute', async () => {
|
|
180
|
+
mockFetch.mockResolvedValueOnce(
|
|
181
|
+
createMockResponse({
|
|
182
|
+
data: [{ id: 1 }],
|
|
183
|
+
columns: ['id'],
|
|
184
|
+
row_count: 1,
|
|
185
|
+
execution_time_ms: 5,
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const { result } = renderHook(() => useQuery('graph_1'))
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await result.current.execute('FIRST QUERY')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(result.current.data).not.toBeNull()
|
|
196
|
+
|
|
197
|
+
// Now the next query will also succeed
|
|
198
|
+
mockFetch.mockResolvedValueOnce(
|
|
199
|
+
createMockResponse({
|
|
200
|
+
data: [{ id: 2 }],
|
|
201
|
+
columns: ['id'],
|
|
202
|
+
row_count: 1,
|
|
203
|
+
execution_time_ms: 3,
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
await act(async () => {
|
|
208
|
+
await result.current.execute('SECOND QUERY')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
expect(result.current.data!.data).toEqual([{ id: 2 }])
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('useStreamingQuery', () => {
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
vi.clearAllMocks()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should initialize with default state', () => {
|
|
221
|
+
const { result } = renderHook(() => useStreamingQuery('graph_1'))
|
|
222
|
+
|
|
223
|
+
expect(result.current.isStreaming).toBe(false)
|
|
224
|
+
expect(result.current.error).toBeNull()
|
|
225
|
+
expect(result.current.rowsReceived).toBe(0)
|
|
226
|
+
expect(typeof result.current.stream).toBe('function')
|
|
227
|
+
expect(typeof result.current.cancel).toBe('function')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should cancel and reset streaming state', () => {
|
|
231
|
+
const { result } = renderHook(() => useStreamingQuery('graph_1'))
|
|
232
|
+
|
|
233
|
+
act(() => {
|
|
234
|
+
result.current.cancel()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
expect(result.current.isStreaming).toBe(false)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('useOperation', () => {
|
|
242
|
+
let mockFetch: any
|
|
243
|
+
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
vi.clearAllMocks()
|
|
246
|
+
mockFetch = vi.fn()
|
|
247
|
+
global.fetch = mockFetch
|
|
248
|
+
globalThis.fetch = mockFetch
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should initialize with idle state', () => {
|
|
252
|
+
const { result } = renderHook(() => useOperation())
|
|
253
|
+
|
|
254
|
+
expect(result.current.status).toBe('idle')
|
|
255
|
+
expect(result.current.progress).toBeNull()
|
|
256
|
+
expect(result.current.error).toBeNull()
|
|
257
|
+
expect(result.current.result).toBeNull()
|
|
258
|
+
expect(typeof result.current.monitor).toBe('function')
|
|
259
|
+
expect(typeof result.current.cancel).toBe('function')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should set running state when monitoring starts', async () => {
|
|
263
|
+
// Mock EventSource for SSE
|
|
264
|
+
class MockEventSource {
|
|
265
|
+
onopen: any = null
|
|
266
|
+
onerror: any = null
|
|
267
|
+
onmessage: any = null
|
|
268
|
+
readyState = 0
|
|
269
|
+
url: string
|
|
270
|
+
withCredentials = false
|
|
271
|
+
static CONNECTING = 0
|
|
272
|
+
static OPEN = 1
|
|
273
|
+
static CLOSED = 2
|
|
274
|
+
constructor(url: string) {
|
|
275
|
+
this.url = url
|
|
276
|
+
// Never connect - we just want to test the state transition
|
|
277
|
+
}
|
|
278
|
+
addEventListener() {}
|
|
279
|
+
removeEventListener() {}
|
|
280
|
+
close() {
|
|
281
|
+
this.readyState = 2
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
global.EventSource = MockEventSource as any
|
|
285
|
+
|
|
286
|
+
const { result } = renderHook(() => useOperation())
|
|
287
|
+
|
|
288
|
+
// Start monitoring (will hang since SSE never connects, but state should transition)
|
|
289
|
+
act(() => {
|
|
290
|
+
result.current.monitor('op_123', 1000) // short timeout
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
expect(result.current.status).toBe('running')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should handle cancel', async () => {
|
|
297
|
+
// Mock the cancel endpoint
|
|
298
|
+
mockFetch.mockResolvedValue(createMockResponse({ status: 'cancelled' }))
|
|
299
|
+
|
|
300
|
+
const { result } = renderHook(() => useOperation())
|
|
301
|
+
|
|
302
|
+
await act(async () => {
|
|
303
|
+
await result.current.cancel('op_123')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
expect(result.current.status).toBe('idle')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should not throw when cancel is called without a client', () => {
|
|
310
|
+
const { result } = renderHook(() => useOperation())
|
|
311
|
+
|
|
312
|
+
// Cancel should not throw even on first render
|
|
313
|
+
expect(async () => {
|
|
314
|
+
await result.current.cancel('op_123')
|
|
315
|
+
}).not.toThrow()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('useMultipleOperations', () => {
|
|
320
|
+
beforeEach(() => {
|
|
321
|
+
vi.clearAllMocks()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should initialize with default state', () => {
|
|
325
|
+
const { result } = renderHook(() => useMultipleOperations())
|
|
326
|
+
|
|
327
|
+
expect(result.current.results.size).toBe(0)
|
|
328
|
+
expect(result.current.loading).toBe(false)
|
|
329
|
+
expect(result.current.errors.size).toBe(0)
|
|
330
|
+
expect(result.current.allCompleted).toBe(false)
|
|
331
|
+
expect(result.current.hasErrors).toBe(false)
|
|
332
|
+
expect(typeof result.current.monitorAll).toBe('function')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('should report allCompleted false when no results', () => {
|
|
336
|
+
const { result } = renderHook(() => useMultipleOperations())
|
|
337
|
+
|
|
338
|
+
expect(result.current.allCompleted).toBe(false)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should report hasErrors false initially', () => {
|
|
342
|
+
const { result } = renderHook(() => useMultipleOperations())
|
|
343
|
+
|
|
344
|
+
expect(result.current.hasErrors).toBe(false)
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('useSDKClients', () => {
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
vi.clearAllMocks()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('should eventually provide initialized clients', async () => {
|
|
354
|
+
const { result } = renderHook(() => useSDKClients())
|
|
355
|
+
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(result.current.query).not.toBeNull()
|
|
358
|
+
expect(result.current.operations).not.toBeNull()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
expect(typeof result.current.query!.executeQuery).toBe('function')
|
|
362
|
+
expect(typeof result.current.operations!.monitorOperation).toBe('function')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should start with null clients', () => {
|
|
366
|
+
// Before the useEffect fires, clients are null
|
|
367
|
+
const { result } = renderHook(() => useSDKClients())
|
|
368
|
+
|
|
369
|
+
// The hook returns an object - it may or may not have initialized by now
|
|
370
|
+
expect(result.current).toBeDefined()
|
|
371
|
+
expect(typeof result.current).toBe('object')
|
|
372
|
+
})
|
|
373
|
+
})
|