@adatechnology/conversations-ui 0.1.1 → 0.2.1
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/{ConversationSimulatorPanel--5fIzXWY.d.ts → ConversationSimulatorPanel-ikgmlciE.d.ts} +103 -1
- package/dist/{chunk-BJNRLLDO.js → chunk-NHQDQJL2.js} +769 -244
- package/dist/index.d.ts +304 -9
- package/dist/index.js +2181 -507
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +147 -1
- package/dist/styles.css +135 -0
- package/package.json +1 -1
- package/src/MessageComposer.test.tsx +14 -0
- package/src/MessageComposer.tsx +327 -73
- package/src/RichMessageComposer.test.tsx +22 -4
- package/src/RichMessageComposer.tsx +674 -370
- package/src/index.ts +28 -1
- package/src/preview/createMockConversationsApi.ts +184 -0
- package/src/providers/types.ts +34 -0
- package/src/quickReplies/AttachmentsFormSection.tsx +160 -0
- package/src/quickReplies/QuickRepliesPicker.test.tsx +114 -0
- package/src/quickReplies/QuickRepliesPicker.tsx +188 -0
- package/src/quickReplies/QuickRepliesWorkspace.test.tsx +32 -0
- package/src/quickReplies/QuickRepliesWorkspace.tsx +395 -0
- package/src/quickReplies/createUploadQueue.test.ts +106 -0
- package/src/quickReplies/createUploadQueue.ts +69 -0
- package/src/quickReplies/index.ts +5 -0
- package/src/quickReplies/labels.ts +124 -0
- package/src/quickReplies/quickReply.types.ts +74 -0
- package/src/quickReplies/quickReplyAttachmentUpload.test.ts +76 -0
- package/src/quickReplies/quickReplyAttachmentUpload.ts +81 -0
- package/src/quickReplies/quickReplyAttachments.test.ts +467 -0
- package/src/quickReplies/quickReplyAttachments.ts +328 -0
- package/src/quickReplies/quickReplySearch.test.ts +88 -0
- package/src/quickReplies/quickReplySearch.ts +104 -0
- package/src/quickReplies/quickReplyShortcut.test.ts +38 -0
- package/src/quickReplies/quickReplyShortcut.ts +46 -0
- package/src/quickReplies/resolveConversationVariables.test.ts +63 -0
- package/src/quickReplies/resolveConversationVariables.ts +41 -0
- package/src/quickReplies/useQuickRepliesPicker.test.ts +20 -0
- package/src/quickReplies/useQuickRepliesPicker.ts +121 -0
- package/src/quickReplies/useQuickRepliesWorkspace.test.ts +222 -0
- package/src/quickReplies/useQuickRepliesWorkspace.ts +426 -0
- package/src/quickReplies/useQuickReplyAttachmentUploads.ts +159 -0
- package/src/styles.css +87 -0
- package/src/workspace/ConversationPane.tsx +137 -73
- package/src/workspace/ConversationsWorkspace.tsx +16 -1
- package/src/workspace/QueuedAttachmentsList.test.ts +27 -0
- package/src/workspace/QueuedAttachmentsList.tsx +198 -0
- package/src/workspace/index.ts +1 -0
- package/src/workspace/labels.ts +14 -0
- package/src/workspace/useComposerAttachmentRetry.ts +169 -0
- package/src/workspace/useComposerQueue.ts +229 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applySendResults,
|
|
5
|
+
attachmentKey,
|
|
6
|
+
canAddAttachments,
|
|
7
|
+
excludeRetryingItems,
|
|
8
|
+
hasSentEveryStoredUpload,
|
|
9
|
+
orderOutgoingItems,
|
|
10
|
+
queuedAttachmentsFromQuickReply,
|
|
11
|
+
resolveIdempotencyKey,
|
|
12
|
+
resolveMaxAttachmentSizeBytes,
|
|
13
|
+
resolveRetryOutcome,
|
|
14
|
+
retryStoredAttachments,
|
|
15
|
+
sendQueuedMessage,
|
|
16
|
+
shouldResetRetryKey,
|
|
17
|
+
type IdempotencyKeyState,
|
|
18
|
+
} from './quickReplyAttachments'
|
|
19
|
+
import type { QueuedAttachment } from './quickReply.types'
|
|
20
|
+
|
|
21
|
+
const LOCAL: QueuedAttachment = {
|
|
22
|
+
kind: 'local',
|
|
23
|
+
localId: 'local-1',
|
|
24
|
+
file: new File(['x'], 'local.pdf', { type: 'application/pdf' }),
|
|
25
|
+
}
|
|
26
|
+
const FIRST: QueuedAttachment = {
|
|
27
|
+
kind: 'stored',
|
|
28
|
+
uploadId: 'a',
|
|
29
|
+
filename: 'a.pdf',
|
|
30
|
+
mimeType: 'application/pdf',
|
|
31
|
+
sizeBytes: 1,
|
|
32
|
+
}
|
|
33
|
+
const SECOND: QueuedAttachment = {
|
|
34
|
+
kind: 'stored',
|
|
35
|
+
uploadId: 'b',
|
|
36
|
+
filename: 'b.png',
|
|
37
|
+
mimeType: 'image/png',
|
|
38
|
+
sizeBytes: 1,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('orderOutgoingItems', () => {
|
|
42
|
+
it('põe guardados na ordem do cadastro antes dos locais', () => {
|
|
43
|
+
const items = orderOutgoingItems('oi', [LOCAL, FIRST, SECOND])
|
|
44
|
+
expect(items.text).toBe('oi')
|
|
45
|
+
expect(items.attachments).toEqual([FIRST, SECOND, LOCAL])
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('applySendResults', () => {
|
|
50
|
+
it('mantém só o que não foi enviado', () => {
|
|
51
|
+
const queue = applySendResults(
|
|
52
|
+
[FIRST, SECOND, LOCAL],
|
|
53
|
+
[
|
|
54
|
+
{ uploadId: 'a', status: 'sent' },
|
|
55
|
+
{ uploadId: 'b', status: 'failed', errorCode: 'X' },
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
expect(queue).toEqual([SECOND, LOCAL])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('mantém o pulado', () => {
|
|
62
|
+
expect(applySendResults([FIRST], [{ uploadId: 'a', status: 'skipped' }])).toEqual([FIRST])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('onAttachmentStatus: pulado distinto de falhou', () => {
|
|
67
|
+
it('sendQueuedMessage reporta skipped, não failed, para um item pulado no lote', async () => {
|
|
68
|
+
const statuses: { key: string; status: string }[] = []
|
|
69
|
+
await sendQueuedMessage({
|
|
70
|
+
text: '',
|
|
71
|
+
queue: [FIRST, SECOND],
|
|
72
|
+
idempotencyKey: 'k1',
|
|
73
|
+
sendText: async () => true,
|
|
74
|
+
sendStoredAttachments: async () => ({
|
|
75
|
+
results: [
|
|
76
|
+
{ uploadId: 'a', status: 'failed', errorCode: 'UPLOAD_FAILED' },
|
|
77
|
+
{ uploadId: 'b', status: 'skipped' },
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
onAttachmentStatus: (key, status) => statuses.push({ key, status }),
|
|
81
|
+
})
|
|
82
|
+
expect(statuses).toContainEqual({ key: 'a', status: 'failed' })
|
|
83
|
+
expect(statuses).toContainEqual({ key: 'b', status: 'skipped' })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('retryStoredAttachments reporta skipped, não failed, para o item pulado', async () => {
|
|
87
|
+
const statuses: { key: string; status: string }[] = []
|
|
88
|
+
await retryStoredAttachments({
|
|
89
|
+
queue: [FIRST],
|
|
90
|
+
uploadIds: ['a'],
|
|
91
|
+
idempotencyKey: 'k',
|
|
92
|
+
sendStoredAttachments: async () => ({ results: [{ uploadId: 'a', status: 'skipped' }] }),
|
|
93
|
+
onAttachmentStatus: (key, status) => statuses.push({ key, status }),
|
|
94
|
+
})
|
|
95
|
+
expect(statuses).toEqual([
|
|
96
|
+
{ key: 'a', status: 'sending' },
|
|
97
|
+
{ key: 'a', status: 'skipped' },
|
|
98
|
+
])
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('canAddAttachments', () => {
|
|
103
|
+
it('aceita até o teto e recusa acima', () => {
|
|
104
|
+
expect(canAddAttachments(8, 2)).toBe(true)
|
|
105
|
+
expect(canAddAttachments(9, 2)).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('resolveMaxAttachmentSizeBytes', () => {
|
|
110
|
+
it('usa o teto por tipo', () => {
|
|
111
|
+
expect(resolveMaxAttachmentSizeBytes('image/png')).toBe(5 * 1024 * 1024)
|
|
112
|
+
expect(resolveMaxAttachmentSizeBytes('audio/ogg')).toBe(16 * 1024 * 1024)
|
|
113
|
+
expect(resolveMaxAttachmentSizeBytes('video/mp4')).toBe(16 * 1024 * 1024)
|
|
114
|
+
expect(resolveMaxAttachmentSizeBytes('application/pdf')).toBe(100 * 1024 * 1024)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('aceita limites do host', () => {
|
|
118
|
+
expect(resolveMaxAttachmentSizeBytes('image/png', { document: 1, image: 2, audio: 3, video: 4 })).toBe(2)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('sendQueuedMessage', () => {
|
|
123
|
+
it('manda texto, depois guardados, depois locais, e esvazia a fila quando tudo sai', async () => {
|
|
124
|
+
const order: string[] = []
|
|
125
|
+
const result = await sendQueuedMessage({
|
|
126
|
+
text: 'oi',
|
|
127
|
+
queue: [LOCAL, FIRST, SECOND],
|
|
128
|
+
idempotencyKey: 'k1',
|
|
129
|
+
sendText: async (text) => {
|
|
130
|
+
order.push(`text:${text}`)
|
|
131
|
+
return true
|
|
132
|
+
},
|
|
133
|
+
sendStoredAttachments: async ({ uploadIds, idempotencyKey }) => {
|
|
134
|
+
order.push(`stored:${uploadIds.join(',')}:${idempotencyKey}`)
|
|
135
|
+
return { results: uploadIds.map((uploadId) => ({ uploadId, status: 'sent' as const })) }
|
|
136
|
+
},
|
|
137
|
+
sendLocalAttachments: async (files) => {
|
|
138
|
+
order.push(`local:${files.map((file) => file.name).join(',')}`)
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
expect(order).toEqual(['text:oi', 'stored:a,b:k1', 'local:local.pdf'])
|
|
142
|
+
expect(result.textSent).toBe(true)
|
|
143
|
+
expect(result.remainingQueue).toEqual([])
|
|
144
|
+
// sentAttachmentKeys deixa o chamador remover por chave de um estado corrente em vez de
|
|
145
|
+
// sobrescrever com remainingQueue (calculado sobre a fila capturada antes do await) — o item
|
|
146
|
+
// adicionado durante o envio (MEDIUM 1) não pode se perder nessa troca.
|
|
147
|
+
expect([...result.sentAttachmentKeys].sort()).toEqual(['a', 'b', 'local-1'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('não manda anexo nenhum quando o texto falha (QR-43)', async () => {
|
|
151
|
+
let storedCalled = false
|
|
152
|
+
let localCalled = false
|
|
153
|
+
const result = await sendQueuedMessage({
|
|
154
|
+
text: 'oi',
|
|
155
|
+
queue: [FIRST, LOCAL],
|
|
156
|
+
idempotencyKey: 'k1',
|
|
157
|
+
sendText: async () => false,
|
|
158
|
+
sendStoredAttachments: async () => {
|
|
159
|
+
storedCalled = true
|
|
160
|
+
return { results: [] }
|
|
161
|
+
},
|
|
162
|
+
sendLocalAttachments: async () => {
|
|
163
|
+
localCalled = true
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
expect(storedCalled).toBe(false)
|
|
167
|
+
expect(localCalled).toBe(false)
|
|
168
|
+
expect(result.textSent).toBe(false)
|
|
169
|
+
expect(result.remainingQueue).toEqual([FIRST, LOCAL])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('mantém na fila só o que falhou ou não tem porta (QR-37)', async () => {
|
|
173
|
+
const result = await sendQueuedMessage({
|
|
174
|
+
text: '',
|
|
175
|
+
queue: [FIRST, SECOND, LOCAL],
|
|
176
|
+
idempotencyKey: 'k1',
|
|
177
|
+
sendText: async () => true,
|
|
178
|
+
sendStoredAttachments: async () => ({
|
|
179
|
+
results: [
|
|
180
|
+
{ uploadId: 'a', status: 'sent' },
|
|
181
|
+
{ uploadId: 'b', status: 'failed', errorCode: 'X' },
|
|
182
|
+
],
|
|
183
|
+
}),
|
|
184
|
+
})
|
|
185
|
+
// sem sendLocalAttachments, o item local não sai e continua na fila
|
|
186
|
+
expect(result.remainingQueue).toEqual([SECOND, LOCAL])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('marca todo guardado como falha quando o lote lança (H3)', async () => {
|
|
190
|
+
const result = await sendQueuedMessage({
|
|
191
|
+
text: '',
|
|
192
|
+
queue: [FIRST, SECOND],
|
|
193
|
+
idempotencyKey: 'k1',
|
|
194
|
+
sendText: async () => true,
|
|
195
|
+
sendStoredAttachments: async () => {
|
|
196
|
+
throw new Error('rede caiu')
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
expect(result.textSent).toBe(true)
|
|
200
|
+
expect(result.remainingQueue).toEqual([FIRST, SECOND])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('trata guardado ausente do resultado do lote como falha (M5)', async () => {
|
|
204
|
+
const result = await sendQueuedMessage({
|
|
205
|
+
text: '',
|
|
206
|
+
queue: [FIRST, SECOND],
|
|
207
|
+
idempotencyKey: 'k1',
|
|
208
|
+
sendText: async () => true,
|
|
209
|
+
sendStoredAttachments: async () => ({ results: [{ uploadId: 'a', status: 'sent' as const }] }),
|
|
210
|
+
})
|
|
211
|
+
expect(result.remainingQueue).toEqual([SECOND])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('reusa a mesma chave de idempotência ao reenviar o que sobrou', async () => {
|
|
215
|
+
const keys: string[] = []
|
|
216
|
+
await sendQueuedMessage({
|
|
217
|
+
text: '',
|
|
218
|
+
queue: [FIRST],
|
|
219
|
+
idempotencyKey: 'retry-key',
|
|
220
|
+
sendText: async () => true,
|
|
221
|
+
sendStoredAttachments: async ({ idempotencyKey }) => {
|
|
222
|
+
keys.push(idempotencyKey)
|
|
223
|
+
return { results: [{ uploadId: 'a', status: 'failed' as const }] }
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
await sendQueuedMessage({
|
|
227
|
+
text: '',
|
|
228
|
+
queue: [FIRST],
|
|
229
|
+
idempotencyKey: 'retry-key',
|
|
230
|
+
sendText: async () => true,
|
|
231
|
+
sendStoredAttachments: async ({ idempotencyKey }) => {
|
|
232
|
+
keys.push(idempotencyKey)
|
|
233
|
+
return { results: [{ uploadId: 'a', status: 'sent' as const }] }
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
expect(keys).toEqual(['retry-key', 'retry-key'])
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('resolveIdempotencyKey', () => {
|
|
241
|
+
it('gera chave nova sem estado anterior', () => {
|
|
242
|
+
const state = resolveIdempotencyKey(undefined, ['a', 'b'], () => 'new-key')
|
|
243
|
+
expect(state).toEqual({ key: 'new-key', uploadIds: ['a', 'b'] })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('reusa a chave quando o conjunto ordenado de uploadIds não muda', () => {
|
|
247
|
+
const previous = { key: 'k1', uploadIds: ['a', 'b'] }
|
|
248
|
+
const state = resolveIdempotencyKey(previous, ['a', 'b'], () => 'should-not-be-used')
|
|
249
|
+
expect(state).toBe(previous)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('gera chave nova quando um uploadId entra ou sai', () => {
|
|
253
|
+
const previous = { key: 'k1', uploadIds: ['a', 'b'] }
|
|
254
|
+
expect(resolveIdempotencyKey(previous, ['a'], () => 'k2')).toEqual({ key: 'k2', uploadIds: ['a'] })
|
|
255
|
+
expect(resolveIdempotencyKey(previous, ['a', 'b', 'c'], () => 'k3')).toEqual({
|
|
256
|
+
key: 'k3',
|
|
257
|
+
uploadIds: ['a', 'b', 'c'],
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('gera chave nova quando a ordem muda, mesmo com o mesmo conjunto', () => {
|
|
262
|
+
const previous = { key: 'k1', uploadIds: ['a', 'b'] }
|
|
263
|
+
expect(resolveIdempotencyKey(previous, ['b', 'a'], () => 'k2')).toEqual({ key: 'k2', uploadIds: ['b', 'a'] })
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('hasSentEveryStoredUpload', () => {
|
|
268
|
+
it('true quando não havia guardado nenhum nesta tentativa', () => {
|
|
269
|
+
expect(hasSentEveryStoredUpload([], [])).toBe(true)
|
|
270
|
+
expect(hasSentEveryStoredUpload([], ['a'])).toBe(true)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('true quando todo uploadId da tentativa saiu', () => {
|
|
274
|
+
expect(hasSentEveryStoredUpload(['a', 'b'], ['a', 'b'])).toBe(true)
|
|
275
|
+
expect(hasSentEveryStoredUpload(['a', 'b'], ['b', 'a'])).toBe(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('true mesmo com chave extra em sentAttachmentKeys (ex: anexo local também enviado)', () => {
|
|
279
|
+
expect(hasSentEveryStoredUpload(['a'], ['a', 'local-1'])).toBe(true)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('falso quando um uploadId falhou ou foi pulado', () => {
|
|
283
|
+
expect(hasSentEveryStoredUpload(['a', 'b'], ['a'])).toBe(false)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('falso quando nada saiu', () => {
|
|
287
|
+
expect(hasSentEveryStoredUpload(['a', 'b'], [])).toBe(false)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('shouldResetRetryKey', () => {
|
|
292
|
+
const stateA: IdempotencyKeyState = { key: 'key-a', uploadIds: ['upload-1'] }
|
|
293
|
+
const stateB: IdempotencyKeyState = { key: 'key-b', uploadIds: ['upload-1'] }
|
|
294
|
+
|
|
295
|
+
it('descarta quando o item saiu e o ref ainda é o mesmo da tentativa (duas tentativas seguidas do mesmo uploadId recebem chaves diferentes)', () => {
|
|
296
|
+
expect(
|
|
297
|
+
shouldResetRetryKey({
|
|
298
|
+
current: stateA,
|
|
299
|
+
attempted: stateA,
|
|
300
|
+
sentAttachmentKeys: ['upload-1'],
|
|
301
|
+
uploadId: 'upload-1',
|
|
302
|
+
}),
|
|
303
|
+
).toBe(true)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('mantém a chave quando o retry falhou (uploadId não está em sentAttachmentKeys)', () => {
|
|
307
|
+
expect(
|
|
308
|
+
shouldResetRetryKey({ current: stateA, attempted: stateA, sentAttachmentKeys: [], uploadId: 'upload-1' }),
|
|
309
|
+
).toBe(false)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('mantém a chave quando um retry concorrente já trocou o ref por identidade', () => {
|
|
313
|
+
expect(
|
|
314
|
+
shouldResetRetryKey({
|
|
315
|
+
current: stateB,
|
|
316
|
+
attempted: stateA,
|
|
317
|
+
sentAttachmentKeys: ['upload-1'],
|
|
318
|
+
uploadId: 'upload-1',
|
|
319
|
+
}),
|
|
320
|
+
).toBe(false)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('mantém a chave quando o ref já foi limpo (undefined) por outra resolução', () => {
|
|
324
|
+
expect(
|
|
325
|
+
shouldResetRetryKey({
|
|
326
|
+
current: undefined,
|
|
327
|
+
attempted: stateA,
|
|
328
|
+
sentAttachmentKeys: ['upload-1'],
|
|
329
|
+
uploadId: 'upload-1',
|
|
330
|
+
}),
|
|
331
|
+
).toBe(false)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('retryStoredAttachments', () => {
|
|
336
|
+
it('reenvia só o uploadId pedido, sem tocar no resto da fila', async () => {
|
|
337
|
+
const calls: { uploadIds: readonly string[]; idempotencyKey: string }[] = []
|
|
338
|
+
const result = await retryStoredAttachments({
|
|
339
|
+
queue: [FIRST, SECOND, LOCAL],
|
|
340
|
+
uploadIds: ['b'],
|
|
341
|
+
idempotencyKey: 'retry-b',
|
|
342
|
+
sendStoredAttachments: async ({ uploadIds, idempotencyKey }) => {
|
|
343
|
+
calls.push({ uploadIds, idempotencyKey })
|
|
344
|
+
return { results: [{ uploadId: 'b', status: 'sent' as const }] }
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
expect(calls).toEqual([{ uploadIds: ['b'], idempotencyKey: 'retry-b' }])
|
|
348
|
+
expect(result.remainingQueue).toEqual([FIRST, LOCAL])
|
|
349
|
+
expect(result.sentAttachmentKeys).toEqual(['b'])
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('sentAttachmentKeys vem vazio quando o reenvio falha (nada para remover por chave)', async () => {
|
|
353
|
+
const result = await retryStoredAttachments({
|
|
354
|
+
queue: [FIRST],
|
|
355
|
+
uploadIds: ['a'],
|
|
356
|
+
idempotencyKey: 'k',
|
|
357
|
+
sendStoredAttachments: async () => ({ results: [{ uploadId: 'a', status: 'failed' as const }] }),
|
|
358
|
+
})
|
|
359
|
+
expect(result.sentAttachmentKeys).toEqual([])
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('sem item correspondente na fila, não chama a porta e devolve a fila intacta', async () => {
|
|
363
|
+
let called = false
|
|
364
|
+
const result = await retryStoredAttachments({
|
|
365
|
+
queue: [FIRST],
|
|
366
|
+
uploadIds: ['nao-existe'],
|
|
367
|
+
idempotencyKey: 'k',
|
|
368
|
+
sendStoredAttachments: async () => {
|
|
369
|
+
called = true
|
|
370
|
+
return { results: [] }
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
expect(called).toBe(false)
|
|
374
|
+
expect(result.remainingQueue).toEqual([FIRST])
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('mantém o item na fila quando o reenvio falha', async () => {
|
|
378
|
+
const result = await retryStoredAttachments({
|
|
379
|
+
queue: [FIRST],
|
|
380
|
+
uploadIds: ['a'],
|
|
381
|
+
idempotencyKey: 'k',
|
|
382
|
+
sendStoredAttachments: async () => {
|
|
383
|
+
throw new Error('rede caiu')
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
expect(result.remainingQueue).toEqual([FIRST])
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
describe('queuedAttachmentsFromQuickReply', () => {
|
|
391
|
+
const quickReplyWithAttachments = {
|
|
392
|
+
attachments: [
|
|
393
|
+
{ uploadId: 'a', filename: 'a.pdf', mimeType: 'application/pdf', sizeBytes: 1 },
|
|
394
|
+
{ uploadId: 'b', filename: 'b.png', mimeType: 'image/png', sizeBytes: 2 },
|
|
395
|
+
],
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
it('empurra os anexos como itens guardados quando o host sabe mandar (QR-32)', () => {
|
|
399
|
+
expect(queuedAttachmentsFromQuickReply(quickReplyWithAttachments, true)).toEqual([
|
|
400
|
+
{ kind: 'stored', uploadId: 'a', filename: 'a.pdf', mimeType: 'application/pdf', sizeBytes: 1 },
|
|
401
|
+
{ kind: 'stored', uploadId: 'b', filename: 'b.png', mimeType: 'image/png', sizeBytes: 2 },
|
|
402
|
+
])
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('não empurra nada sem a porta (QR-33)', () => {
|
|
406
|
+
expect(queuedAttachmentsFromQuickReply(quickReplyWithAttachments, false)).toEqual([])
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('não empurra nada quando a mensagem não tem anexo', () => {
|
|
410
|
+
expect(queuedAttachmentsFromQuickReply({ attachments: [] }, true)).toEqual([])
|
|
411
|
+
expect(queuedAttachmentsFromQuickReply({ attachments: undefined }, true)).toEqual([])
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('attachmentKey', () => {
|
|
416
|
+
it('usa o uploadId para guardado e o localId para local', () => {
|
|
417
|
+
expect(attachmentKey(FIRST)).toBe('a')
|
|
418
|
+
expect(attachmentKey(LOCAL)).toBe('local-1')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('distingue duas cópias do mesmo arquivo local pelo localId gerado, não pela identidade do File', () => {
|
|
422
|
+
const file = new File(['x'], 'same.pdf', { type: 'application/pdf' })
|
|
423
|
+
const first: QueuedAttachment = { kind: 'local', localId: 'local-a', file }
|
|
424
|
+
const second: QueuedAttachment = { kind: 'local', localId: 'local-b', file }
|
|
425
|
+
expect(attachmentKey(first)).not.toBe(attachmentKey(second))
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe('resolveRetryOutcome', () => {
|
|
430
|
+
it('conversa diferente da do retry devolve undefined, deixando a fila corrente intocada', () => {
|
|
431
|
+
const outcome = resolveRetryOutcome({
|
|
432
|
+
conversationIdAtRetry: 'conversation-1',
|
|
433
|
+
currentConversationId: 'conversation-2',
|
|
434
|
+
sentAttachmentKeys: ['a'],
|
|
435
|
+
queue: [],
|
|
436
|
+
})
|
|
437
|
+
expect(outcome).toBeUndefined()
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('mesma conversa: tira só as chaves enviadas e mantém item adicionado durante o retry', () => {
|
|
441
|
+
const addedMidRetry: QueuedAttachment = {
|
|
442
|
+
kind: 'stored',
|
|
443
|
+
uploadId: 'c',
|
|
444
|
+
filename: 'c.pdf',
|
|
445
|
+
mimeType: 'application/pdf',
|
|
446
|
+
sizeBytes: 1,
|
|
447
|
+
}
|
|
448
|
+
const outcome = resolveRetryOutcome({
|
|
449
|
+
conversationIdAtRetry: 'conversation-1',
|
|
450
|
+
currentConversationId: 'conversation-1',
|
|
451
|
+
sentAttachmentKeys: ['a'],
|
|
452
|
+
queue: [FIRST, SECOND, addedMidRetry],
|
|
453
|
+
})
|
|
454
|
+
expect(outcome).toEqual([SECOND, addedMidRetry])
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe('excludeRetryingItems', () => {
|
|
459
|
+
it('sem chaves em retry devolve a fila como veio', () => {
|
|
460
|
+
expect(excludeRetryingItems([FIRST, LOCAL], new Set())).toEqual([FIRST, LOCAL])
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('tira só os itens cuja chave está em retry avulso', () => {
|
|
464
|
+
const result = excludeRetryingItems([FIRST, SECOND, LOCAL], new Set(['b']))
|
|
465
|
+
expect(result).toEqual([FIRST, LOCAL])
|
|
466
|
+
})
|
|
467
|
+
})
|