@open-mercato/ui 0.6.8-develop.6878.1.ab817e58ac → 0.6.8-develop.6881.1.8ea75912f2
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/dist/ai/AiChat.js +34 -11
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiChatSessions.js +11 -2
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/parts/useAiPendingActionPolling.js +12 -2
- package/dist/ai/parts/useAiPendingActionPolling.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/AiChat.tsx +56 -27
- package/src/ai/AiChatSessions.tsx +16 -4
- package/src/ai/__tests__/AiChat.test.tsx +147 -2
- package/src/ai/__tests__/AiChatSessions.test.tsx +62 -0
- package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +61 -0
- package/src/ai/parts/useAiPendingActionPolling.ts +21 -6
|
@@ -24,6 +24,19 @@ jest.mock('../conversation-store', () => {
|
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
28
|
+
const mocked = {
|
|
29
|
+
debug: jest.fn(),
|
|
30
|
+
info: jest.fn(),
|
|
31
|
+
warn: jest.fn(),
|
|
32
|
+
error: jest.fn(),
|
|
33
|
+
child: jest.fn(),
|
|
34
|
+
}
|
|
35
|
+
mocked.child.mockImplementation(() => mocked)
|
|
36
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
27
40
|
import {
|
|
28
41
|
listAiServerConversations,
|
|
29
42
|
createAiServerConversation,
|
|
@@ -31,6 +44,8 @@ import {
|
|
|
31
44
|
|
|
32
45
|
const listMock = listAiServerConversations as jest.MockedFunction<typeof listAiServerConversations>
|
|
33
46
|
const createMock = createAiServerConversation as jest.MockedFunction<typeof createAiServerConversation>
|
|
47
|
+
const loggerError = createLogger('ui').error as jest.Mock
|
|
48
|
+
const loggerWarn = createLogger('ui').warn as jest.Mock
|
|
34
49
|
|
|
35
50
|
const KEY_PREFIX = 'om-ai-chat-sessions-v1'
|
|
36
51
|
|
|
@@ -69,6 +84,8 @@ describe('<AiChatSessionsProvider> — tenant/org scope isolation', () => {
|
|
|
69
84
|
listMock.mockResolvedValue(null)
|
|
70
85
|
createMock.mockReset()
|
|
71
86
|
createMock.mockResolvedValue(null)
|
|
87
|
+
loggerError.mockClear()
|
|
88
|
+
loggerWarn.mockClear()
|
|
72
89
|
// Reset scope to a known starting point. The module-level state in
|
|
73
90
|
// organizationEvents persists across tests in the same file.
|
|
74
91
|
act(() => {
|
|
@@ -212,6 +229,51 @@ describe('<AiChatSessionsProvider> — tenant/org scope isolation', () => {
|
|
|
212
229
|
})
|
|
213
230
|
})
|
|
214
231
|
|
|
232
|
+
it('warns when the server conversation list is unavailable', async () => {
|
|
233
|
+
listMock.mockReset()
|
|
234
|
+
listMock.mockResolvedValue(null)
|
|
235
|
+
|
|
236
|
+
renderWithProviders(<Harness agentId="assistant" />)
|
|
237
|
+
|
|
238
|
+
await waitFor(() => {
|
|
239
|
+
expect(loggerWarn).toHaveBeenCalledWith(
|
|
240
|
+
'Could not load server conversations; keeping the locally persisted sessions',
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('logs and keeps the locally persisted sessions when the server list rejects', async () => {
|
|
246
|
+
const failure = new Error('[internal] conversations endpoint unreachable')
|
|
247
|
+
listMock.mockReset()
|
|
248
|
+
listMock.mockRejectedValue(failure)
|
|
249
|
+
window.localStorage.setItem(
|
|
250
|
+
scopedKey('T1', 'O1'),
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
sessions: [
|
|
253
|
+
{
|
|
254
|
+
id: 'local-session',
|
|
255
|
+
agentId: 'assistant',
|
|
256
|
+
conversationId: 'local-conv',
|
|
257
|
+
createdAt: 1,
|
|
258
|
+
lastUsedAt: 1,
|
|
259
|
+
status: 'open',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
activeByAgent: { assistant: 'local-session' },
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
renderWithProviders(<Harness agentId="assistant" />)
|
|
267
|
+
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(loggerError).toHaveBeenCalledWith('Failed to load server conversations', {
|
|
270
|
+
err: failure,
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
expect(screen.getByTestId('session-count').textContent).toBe('1')
|
|
275
|
+
})
|
|
276
|
+
|
|
215
277
|
it('falls back to the no-tenant/no-org bucket when scope is unresolved', async () => {
|
|
216
278
|
act(() => {
|
|
217
279
|
emitOrganizationScopeChanged({ tenantId: null, organizationId: null })
|
|
@@ -9,6 +9,19 @@ jest.mock('../../../backend/utils/apiCall', () => ({
|
|
|
9
9
|
apiCallOrThrow: jest.fn(),
|
|
10
10
|
}))
|
|
11
11
|
|
|
12
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
13
|
+
const mocked = {
|
|
14
|
+
debug: jest.fn(),
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
error: jest.fn(),
|
|
18
|
+
child: jest.fn(),
|
|
19
|
+
}
|
|
20
|
+
mocked.child.mockImplementation(() => mocked)
|
|
21
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
25
|
import { apiCallOrThrow } from '../../../backend/utils/apiCall'
|
|
13
26
|
import { useAiPendingActionPolling } from '../useAiPendingActionPolling'
|
|
14
27
|
import type { AiPendingActionCardAction, AiPendingActionCardStatus } from '../types'
|
|
@@ -48,9 +61,12 @@ function mockResponse(action: AiPendingActionCardAction) {
|
|
|
48
61
|
}))
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
const loggerError = createLogger('ui').error as jest.Mock
|
|
65
|
+
|
|
51
66
|
describe('useAiPendingActionPolling', () => {
|
|
52
67
|
beforeEach(() => {
|
|
53
68
|
;(apiCallOrThrow as jest.Mock).mockReset()
|
|
69
|
+
loggerError.mockClear()
|
|
54
70
|
})
|
|
55
71
|
|
|
56
72
|
it('fetches on mount and continues polling while status is pending', async () => {
|
|
@@ -148,4 +164,49 @@ describe('useAiPendingActionPolling', () => {
|
|
|
148
164
|
jest.useRealTimers()
|
|
149
165
|
}
|
|
150
166
|
})
|
|
167
|
+
|
|
168
|
+
it('logs a failed fetch and keeps polling until it recovers', async () => {
|
|
169
|
+
const failure = new Error('[internal] pending action endpoint unreachable')
|
|
170
|
+
;(apiCallOrThrow as jest.Mock)
|
|
171
|
+
.mockImplementationOnce(async () => {
|
|
172
|
+
throw failure
|
|
173
|
+
})
|
|
174
|
+
.mockImplementation(async () => ({
|
|
175
|
+
ok: true,
|
|
176
|
+
status: 200,
|
|
177
|
+
result: { pendingAction: makeAction({ status: 'pending' }) },
|
|
178
|
+
response: {},
|
|
179
|
+
cacheStatus: null,
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
jest.useFakeTimers()
|
|
183
|
+
try {
|
|
184
|
+
const { result, unmount } = renderHook(() =>
|
|
185
|
+
useAiPendingActionPolling({ pendingActionId: 'pa-1', intervalMs: 1000 }),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await Promise.resolve()
|
|
190
|
+
await Promise.resolve()
|
|
191
|
+
})
|
|
192
|
+
expect(loggerError).toHaveBeenCalledWith('Failed to load the pending AI action', {
|
|
193
|
+
err: failure,
|
|
194
|
+
pendingActionId: 'pa-1',
|
|
195
|
+
})
|
|
196
|
+
expect(result.current.error?.message).toBe(failure.message)
|
|
197
|
+
expect(result.current.action).toBeNull()
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
jest.advanceTimersByTime(1000)
|
|
201
|
+
await Promise.resolve()
|
|
202
|
+
await Promise.resolve()
|
|
203
|
+
})
|
|
204
|
+
expect(result.current.action?.status).toBe('pending')
|
|
205
|
+
expect(result.current.error).toBeNull()
|
|
206
|
+
|
|
207
|
+
unmount()
|
|
208
|
+
} finally {
|
|
209
|
+
jest.useRealTimers()
|
|
210
|
+
}
|
|
211
|
+
})
|
|
151
212
|
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
5
|
import { apiCallOrThrow } from '../../backend/utils/apiCall'
|
|
5
6
|
import type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'
|
|
6
7
|
|
|
8
|
+
const logger = createLogger('ui').child({ component: 'useAiPendingActionPolling' })
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Shared polling hook for the Phase 3 mutation-approval cards (Step 5.10).
|
|
9
12
|
*
|
|
@@ -148,6 +151,7 @@ export function useAiPendingActionPolling(
|
|
|
148
151
|
}
|
|
149
152
|
return result.pendingAction
|
|
150
153
|
} catch (err) {
|
|
154
|
+
logger.error('Failed to load the pending AI action', { err, pendingActionId })
|
|
151
155
|
if (!mountedRef.current) return null
|
|
152
156
|
const message = err instanceof Error ? err.message : 'Failed to load pending action.'
|
|
153
157
|
setError({ message })
|
|
@@ -168,7 +172,11 @@ export function useAiPendingActionPolling(
|
|
|
168
172
|
}
|
|
169
173
|
setIsPolling(true)
|
|
170
174
|
timerRef.current = setTimeout(async () => {
|
|
171
|
-
|
|
175
|
+
try {
|
|
176
|
+
await refresh()
|
|
177
|
+
} catch (err) {
|
|
178
|
+
logger.error('Polling the pending AI action failed unexpectedly', { err })
|
|
179
|
+
}
|
|
172
180
|
scheduleNext()
|
|
173
181
|
}, intervalMs)
|
|
174
182
|
}, [clearTimer, disabled, intervalMs, refresh])
|
|
@@ -184,11 +192,18 @@ export function useAiPendingActionPolling(
|
|
|
184
192
|
}
|
|
185
193
|
}
|
|
186
194
|
setIsPolling(true)
|
|
187
|
-
// Always fetch on mount — the "reconnect behavior" guarantee.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
// Always fetch on mount — the "reconnect behavior" guarantee. The catch
|
|
196
|
+
// runs before the scheduling step so a rejected first fetch degrades to a
|
|
197
|
+
// logged error instead of silently killing the polling loop.
|
|
198
|
+
void refresh()
|
|
199
|
+
.catch((err) => {
|
|
200
|
+
logger.error('Loading the pending AI action failed unexpectedly', { err })
|
|
201
|
+
return null
|
|
202
|
+
})
|
|
203
|
+
.then(() => {
|
|
204
|
+
if (!mountedRef.current) return
|
|
205
|
+
scheduleNext()
|
|
206
|
+
})
|
|
192
207
|
return () => {
|
|
193
208
|
mountedRef.current = false
|
|
194
209
|
clearTimer()
|