-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmessage-replay.test.ts
More file actions
336 lines (282 loc) · 11.4 KB
/
message-replay.test.ts
File metadata and controls
336 lines (282 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import { describe, test, expect, mock } from "bun:test"
import { filterPartsByTier, replayWithDegradation } from "./message-replay"
import type { MessagePart } from "./types"
const textPart: MessagePart = { type: "text", text: "hello world" }
const imagePart: MessagePart = { type: "image", url: "https://example.com/img.png" }
const toolResultPart: MessagePart = { type: "tool_result", text: "result data" }
const filePart: MessagePart = { type: "file", path: "/tmp/test.txt" }
const mixedParts: MessagePart[] = [textPart, imagePart, toolResultPart, filePart]
const textAndImageParts: MessagePart[] = [textPart, imagePart]
const textOnlyParts: MessagePart[] = [textPart]
describe("message-replay", () => {
describe("#given filterPartsByTier", () => {
describe("#when tier is 1 (all parts)", () => {
test("#then returns all parts unchanged", () => {
const result = filterPartsByTier(mixedParts, 1)
expect(result).toEqual(mixedParts)
expect(result.length).toBe(4)
})
})
describe("#when tier is 2 (text + image)", () => {
test("#then returns only text and image parts", () => {
const result = filterPartsByTier(mixedParts, 2)
expect(result.length).toBe(2)
expect(result[0].type).toBe("text")
expect(result[1].type).toBe("image")
})
})
describe("#when tier is 3 (text only)", () => {
test("#then returns only text parts", () => {
const result = filterPartsByTier(mixedParts, 3)
expect(result.length).toBe(1)
expect(result[0].type).toBe("text")
})
})
describe("#when parts is empty", () => {
test("#then returns empty array for any tier", () => {
expect(filterPartsByTier([], 1)).toEqual([])
expect(filterPartsByTier([], 2)).toEqual([])
expect(filterPartsByTier([], 3)).toEqual([])
})
})
describe("#when parts contain only text", () => {
test("#then all tiers return the same text parts", () => {
const t1 = filterPartsByTier(textOnlyParts, 1)
const t2 = filterPartsByTier(textOnlyParts, 2)
const t3 = filterPartsByTier(textOnlyParts, 3)
expect(t1).toEqual(textOnlyParts)
expect(t2).toEqual(textOnlyParts)
expect(t3).toEqual(textOnlyParts)
})
})
describe("#when parts contain text and image only", () => {
test("#then tier 1 and tier 2 return the same parts", () => {
const t1 = filterPartsByTier(textAndImageParts, 1)
const t2 = filterPartsByTier(textAndImageParts, 2)
expect(t1).toEqual(textAndImageParts)
expect(t2).toEqual(textAndImageParts)
})
test("#then tier 3 returns only text", () => {
const t3 = filterPartsByTier(textAndImageParts, 3)
expect(t3.length).toBe(1)
expect(t3[0].type).toBe("text")
})
})
})
describe("#given replayWithDegradation", () => {
describe("#when sendFn succeeds on first call (Tier 1)", () => {
test("#then returns success with tier 1 and no dropped types", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {})
const result = await replayWithDegradation(mixedParts, sendFn)
expect(result.success).toBe(true)
expect(result.tier).toBe(1)
expect(result.sentParts).toEqual(mixedParts)
expect(result.droppedTypes).toEqual([])
expect(sendFn).toHaveBeenCalledTimes(1)
})
})
describe("#when sendFn rejects Tier 1 but accepts Tier 2", () => {
test("#then returns success with tier 2 and reports dropped types", async () => {
let callCount = 0
const sendFn = mock(async (parts: MessagePart[]) => {
callCount++
// Reject tier 1 (all parts), accept tier 2 (text + image)
if (parts.length > 2) {
throw new Error("Unsupported part types")
}
})
const result = await replayWithDegradation(mixedParts, sendFn)
expect(result.success).toBe(true)
expect(result.tier).toBe(2)
expect(result.sentParts?.length).toBe(2)
expect(result.droppedTypes).toContain("tool_result")
expect(result.droppedTypes).toContain("file")
expect(sendFn).toHaveBeenCalledTimes(2)
})
})
describe("#when sendFn rejects Tier 1 and 2 but accepts Tier 3", () => {
test("#then returns success with tier 3 and reports all non-text types as dropped", async () => {
let callCount = 0
const sendFn = mock(async (parts: MessagePart[]) => {
callCount++
// Only accept text-only parts
if (parts.some((p) => p.type !== "text")) {
throw new Error("Unsupported part types")
}
})
const result = await replayWithDegradation(mixedParts, sendFn)
expect(result.success).toBe(true)
expect(result.tier).toBe(3)
expect(result.sentParts?.length).toBe(1)
expect(result.droppedTypes).toContain("image")
expect(result.droppedTypes).toContain("tool_result")
expect(result.droppedTypes).toContain("file")
})
})
describe("#when sendFn rejects all tiers", () => {
test("#then returns failure with last error message", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {
throw new Error("Always fails")
})
const result = await replayWithDegradation(mixedParts, sendFn)
expect(result.success).toBe(false)
expect(result.error).toBe("Always fails")
expect(result.tier).toBeUndefined()
expect(result.sentParts).toBeUndefined()
})
})
describe("#when parts array is empty", () => {
test("#then returns failure without calling sendFn", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {})
const result = await replayWithDegradation([], sendFn)
expect(result.success).toBe(false)
expect(result.error).toBe("No parts to replay")
expect(sendFn).not.toHaveBeenCalled()
})
})
describe("#when message is text-only and sendFn rejects", () => {
test("#then sendFn is called only once (duplicate tiers skipped)", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {
throw new Error("Rejected")
})
const result = await replayWithDegradation(textOnlyParts, sendFn)
expect(result.success).toBe(false)
// Tiers 1, 2, 3 all produce length 1 for text-only — only tier 1 calls sendFn
expect(sendFn).toHaveBeenCalledTimes(1)
})
})
describe("#when message is text+image and sendFn rejects Tier 1", () => {
test("#then tier 2 is skipped (same parts as tier 1) and tier 3 is tried", async () => {
let callCount = 0
const sendFn = mock(async (parts: MessagePart[]) => {
callCount++
// Reject unless text-only
if (parts.some((p) => p.type !== "text")) {
throw new Error("Rejected")
}
})
const result = await replayWithDegradation(textAndImageParts, sendFn)
expect(result.success).toBe(true)
expect(result.tier).toBe(3)
// Tier 1 = 2 parts, Tier 2 = 2 parts (skipped, same length), Tier 3 = 1 part
expect(sendFn).toHaveBeenCalledTimes(2)
})
})
describe("#when parts have no text parts (non-text only)", () => {
test("#then attempts Tier 1 and fails gracefully if rejected", async () => {
const nonTextParts: MessagePart[] = [imagePart, toolResultPart]
const sendFn = mock(async (_parts: MessagePart[]) => {
throw new Error("No text content")
})
const result = await replayWithDegradation(nonTextParts, sendFn)
expect(result.success).toBe(false)
// Tier 1 = 2 parts, Tier 2 = 1 (image), Tier 3 = 0 (skipped)
expect(sendFn).toHaveBeenCalledTimes(2)
})
})
describe("#when sendFn throws a non-Error object", () => {
test("#then error is stringified", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {
throw "string error"
})
const result = await replayWithDegradation(textOnlyParts, sendFn)
expect(result.success).toBe(false)
expect(result.error).toBe("string error")
})
})
describe("#when parts contain unknown types not in any tier set", () => {
test("#then tier 1 tries all, tier 2 drops unknowns, tier 3 drops unknowns and images", async () => {
const unknownParts: MessagePart[] = [
{ type: "custom_widget", data: "widget-data" },
{ type: "audio", url: "audio.mp3" },
]
const callHistory: number[] = []
const sendFn = mock(async (parts: MessagePart[]) => {
callHistory.push(parts.length)
throw new Error("Rejected")
})
const result = await replayWithDegradation(unknownParts, sendFn)
expect(result.success).toBe(false)
// Tier 1: 2 parts (all), Tier 2: 0 parts (skipped — no text/image), Tier 3: 0 parts (skipped — no text)
// Only 1 call because tiers 2 and 3 produce empty arrays
expect(callHistory).toEqual([2])
})
})
describe("#when parts have empty string type", () => {
test("#then empty type is treated as unknown (filtered out in tier 2 and 3)", () => {
const emptyTypeParts: MessagePart[] = [{ type: "", data: "something" }]
const t1 = filterPartsByTier(emptyTypeParts, 1)
const t2 = filterPartsByTier(emptyTypeParts, 2)
const t3 = filterPartsByTier(emptyTypeParts, 3)
expect(t1.length).toBe(1)
expect(t2.length).toBe(0)
expect(t3.length).toBe(0)
})
})
describe("#when sendFn throws undefined", () => {
test("#then error is stringified as 'undefined'", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {
throw undefined
})
const result = await replayWithDegradation(textOnlyParts, sendFn)
expect(result.success).toBe(false)
expect(result.error).toBe("undefined")
})
})
describe("#when sendFn throws null", () => {
test("#then error is stringified as 'null'", async () => {
const sendFn = mock(async (_parts: MessagePart[]) => {
throw null
})
const result = await replayWithDegradation(textOnlyParts, sendFn)
expect(result.success).toBe(false)
expect(result.error).toBe("null")
})
})
describe("#when all parts are images (no text)", () => {
test("#then tier 2 includes them, tier 3 produces empty", async () => {
const imageOnlyParts: MessagePart[] = [
{ type: "image", url: "img1.png" },
{ type: "image", url: "img2.png" },
]
const sendFn = mock(async (_parts: MessagePart[]) => {})
const result = await replayWithDegradation(imageOnlyParts, sendFn)
// Tier 1 succeeds (same as tier 2 for image-only)
expect(result.success).toBe(true)
expect(result.tier).toBe(1)
expect(result.sentParts?.length).toBe(2)
expect(result.droppedTypes).toEqual([])
})
})
describe("#when tier 1 and 2 have same length but tier 3 succeeds", () => {
test("#then tier 2 is skipped and tier 3 is the second attempt", async () => {
// text + image = 2 parts in both tier 1 and tier 2
const parts: MessagePart[] = [textPart, imagePart]
let callCount = 0
const sendFn = mock(async (p: MessagePart[]) => {
callCount++
if (p.some((x) => x.type !== "text")) throw new Error("No images!")
})
const result = await replayWithDegradation(parts, sendFn)
expect(result.success).toBe(true)
expect(result.tier).toBe(3)
// Only 2 calls: tier 1 (rejected), tier 2 skipped (same length), tier 3 (accepted)
expect(callCount).toBe(2)
expect(result.droppedTypes).toContain("image")
})
})
describe("#when multiple text parts exist", () => {
test("#then all text parts survive to tier 3", async () => {
const multiText: MessagePart[] = [
{ type: "text", text: "part 1" },
{ type: "text", text: "part 2" },
{ type: "text", text: "part 3" },
{ type: "image", url: "img.png" },
]
const t3 = filterPartsByTier(multiText, 3)
expect(t3.length).toBe(3)
expect(t3.every((p) => p.type === "text")).toBe(true)
})
})
})
})