assistant-cloud 0.1.41 → 0.1.43
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/README.md +3 -3
- package/dist/AssistantCloud.js +2 -2
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +5 -5
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +68 -17
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +3 -13
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +19 -11
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/generateThreadTitle.d.ts +15 -0
- package/dist/generateThreadTitle.d.ts.map +1 -0
- package/dist/generateThreadTitle.js +25 -0
- package/dist/generateThreadTitle.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/runTelemetry.d.ts +61 -0
- package/dist/runTelemetry.d.ts.map +1 -0
- package/dist/runTelemetry.js +82 -0
- package/dist/runTelemetry.js.map +1 -0
- package/package.json +5 -5
- package/src/AssistantCloud.ts +1 -1
- package/src/AssistantCloudAPI.ts +9 -8
- package/src/AssistantCloudAuthStrategy.ts +140 -46
- package/src/AssistantCloudRuns.ts +3 -14
- package/src/CloudMessagePersistence.ts +23 -19
- package/src/generateThreadTitle.test.ts +71 -0
- package/src/generateThreadTitle.ts +38 -0
- package/src/index.ts +10 -0
- package/src/runTelemetry.test.ts +171 -0
- package/src/runTelemetry.ts +144 -0
- package/src/tests/AssistantCloud.test.ts +39 -0
- package/src/tests/AssistantCloudAPI.test.ts +25 -0
- package/src/tests/AssistantCloudAuthStrategy.test.ts +284 -10
- package/src/tests/CloudMessagePersistence.test.ts +93 -0
|
@@ -43,6 +43,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
46
47
|
vi.unstubAllGlobals();
|
|
47
48
|
if (originalLocalStorageDescriptor) {
|
|
48
49
|
Object.defineProperty(
|
|
@@ -83,7 +84,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
83
84
|
expect(values.get(refreshTokenKey)).toBe(JSON.stringify(nextRefreshToken));
|
|
84
85
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
85
86
|
`${baseUrl}/v1/auth/tokens/anonymous`,
|
|
86
|
-
{ method: "POST" },
|
|
87
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
87
88
|
);
|
|
88
89
|
});
|
|
89
90
|
|
|
@@ -106,6 +107,276 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
106
107
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
it("deduplicates anonymous token requests across strategy instances", async () => {
|
|
111
|
+
const values = new Map<string, string>();
|
|
112
|
+
installLocalStorage({
|
|
113
|
+
getItem: (key) => values.get(key) ?? null,
|
|
114
|
+
setItem: (key, value) => {
|
|
115
|
+
values.set(key, value);
|
|
116
|
+
},
|
|
117
|
+
removeItem: (key) => {
|
|
118
|
+
values.delete(key);
|
|
119
|
+
},
|
|
120
|
+
} as Storage);
|
|
121
|
+
const fetchMock = mockAnonymousTokenFetch();
|
|
122
|
+
const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
123
|
+
const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]),
|
|
127
|
+
).resolves.toEqual([
|
|
128
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
129
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
130
|
+
]);
|
|
131
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("coordinates anonymous token requests across realms", async () => {
|
|
136
|
+
const values = new Map<string, string>();
|
|
137
|
+
installLocalStorage({
|
|
138
|
+
getItem: (key) => values.get(key) ?? null,
|
|
139
|
+
setItem: (key, value) => {
|
|
140
|
+
values.set(key, value);
|
|
141
|
+
},
|
|
142
|
+
removeItem: (key) => {
|
|
143
|
+
values.delete(key);
|
|
144
|
+
},
|
|
145
|
+
} as Storage);
|
|
146
|
+
let lockTail: Promise<unknown> = Promise.resolve();
|
|
147
|
+
const lockRequest = vi.fn(
|
|
148
|
+
(_name: string, callback: () => Promise<string | null>) => {
|
|
149
|
+
const request = lockTail.then(callback);
|
|
150
|
+
lockTail = request.then(
|
|
151
|
+
() => undefined,
|
|
152
|
+
() => undefined,
|
|
153
|
+
);
|
|
154
|
+
return request;
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
vi.stubGlobal("navigator", { locks: { request: lockRequest } });
|
|
158
|
+
const rotatedRefreshToken = { token: "r2", expires_at: "2099-01-01" };
|
|
159
|
+
const response = (refreshTokenValue: typeof refreshToken) => ({
|
|
160
|
+
ok: true,
|
|
161
|
+
json: vi.fn().mockResolvedValue({
|
|
162
|
+
access_token: accessToken,
|
|
163
|
+
refresh_token: refreshTokenValue,
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
const fetchMock = vi
|
|
167
|
+
.fn()
|
|
168
|
+
.mockResolvedValueOnce(response(refreshToken))
|
|
169
|
+
.mockResolvedValueOnce(response(rotatedRefreshToken));
|
|
170
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
171
|
+
|
|
172
|
+
const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
173
|
+
vi.resetModules();
|
|
174
|
+
const { AssistantCloudAnonymousAuthStrategy: Second } =
|
|
175
|
+
await import("../AssistantCloudAuthStrategy");
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
Promise.all([
|
|
179
|
+
first.getAuthHeaders(),
|
|
180
|
+
new Second(baseUrl).getAuthHeaders(),
|
|
181
|
+
]),
|
|
182
|
+
).resolves.toEqual([
|
|
183
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
184
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
185
|
+
]);
|
|
186
|
+
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
187
|
+
1,
|
|
188
|
+
`${baseUrl}/v1/auth/tokens/anonymous`,
|
|
189
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
190
|
+
);
|
|
191
|
+
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
192
|
+
2,
|
|
193
|
+
`${baseUrl}/v1/auth/tokens/refresh`,
|
|
194
|
+
{
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({ refresh_token: refreshToken.token }),
|
|
198
|
+
signal: expect.any(AbortSignal),
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
expect(lockRequest).toHaveBeenCalledTimes(2);
|
|
202
|
+
expect(values.get(refreshTokenKey)).toBe(
|
|
203
|
+
JSON.stringify(rotatedRefreshToken),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("retries shared anonymous token requests after a failure", async () => {
|
|
208
|
+
const values = new Map<string, string>();
|
|
209
|
+
installLocalStorage({
|
|
210
|
+
getItem: (key) => values.get(key) ?? null,
|
|
211
|
+
setItem: (key, value) => {
|
|
212
|
+
values.set(key, value);
|
|
213
|
+
},
|
|
214
|
+
removeItem: (key) => {
|
|
215
|
+
values.delete(key);
|
|
216
|
+
},
|
|
217
|
+
} as Storage);
|
|
218
|
+
const failure = new Error("authentication unavailable");
|
|
219
|
+
const fetchMock = vi
|
|
220
|
+
.fn()
|
|
221
|
+
.mockRejectedValueOnce(failure)
|
|
222
|
+
.mockResolvedValueOnce({
|
|
223
|
+
ok: true,
|
|
224
|
+
json: vi.fn().mockResolvedValue({
|
|
225
|
+
access_token: accessToken,
|
|
226
|
+
refresh_token: refreshToken,
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
230
|
+
const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
231
|
+
const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
232
|
+
|
|
233
|
+
await Promise.all([
|
|
234
|
+
expect(first.getAuthHeaders()).rejects.toBe(failure),
|
|
235
|
+
expect(second.getAuthHeaders()).rejects.toBe(failure),
|
|
236
|
+
]);
|
|
237
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
|
|
241
|
+
).resolves.toEqual({ Authorization: `Bearer ${accessToken}` });
|
|
242
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("aborts timed out shared anonymous token requests before retrying", async () => {
|
|
246
|
+
vi.useFakeTimers();
|
|
247
|
+
const values = new Map<string, string>();
|
|
248
|
+
installLocalStorage({
|
|
249
|
+
getItem: (key) => values.get(key) ?? null,
|
|
250
|
+
setItem: (key, value) => {
|
|
251
|
+
values.set(key, value);
|
|
252
|
+
},
|
|
253
|
+
removeItem: (key) => {
|
|
254
|
+
values.delete(key);
|
|
255
|
+
},
|
|
256
|
+
} as Storage);
|
|
257
|
+
let requestSignal: AbortSignal | null | undefined;
|
|
258
|
+
const fetchMock = vi
|
|
259
|
+
.fn()
|
|
260
|
+
.mockImplementationOnce((_input: RequestInfo | URL, init?: RequestInit) =>
|
|
261
|
+
Promise.resolve({
|
|
262
|
+
ok: true,
|
|
263
|
+
json: () =>
|
|
264
|
+
new Promise<never>((_resolve, reject) => {
|
|
265
|
+
requestSignal = init?.signal;
|
|
266
|
+
requestSignal?.addEventListener(
|
|
267
|
+
"abort",
|
|
268
|
+
() => reject(requestSignal?.reason),
|
|
269
|
+
{ once: true },
|
|
270
|
+
);
|
|
271
|
+
}),
|
|
272
|
+
} as Response),
|
|
273
|
+
)
|
|
274
|
+
.mockResolvedValueOnce({
|
|
275
|
+
ok: true,
|
|
276
|
+
json: vi.fn().mockResolvedValue({
|
|
277
|
+
access_token: accessToken,
|
|
278
|
+
refresh_token: refreshToken,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
282
|
+
const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
283
|
+
const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
284
|
+
|
|
285
|
+
const firstRequest = expect(first.getAuthHeaders()).rejects.toThrow(
|
|
286
|
+
"Assistant Cloud anonymous token request timed out after 30000ms",
|
|
287
|
+
);
|
|
288
|
+
const secondRequest = expect(second.getAuthHeaders()).rejects.toThrow(
|
|
289
|
+
"Assistant Cloud anonymous token request timed out after 30000ms",
|
|
290
|
+
);
|
|
291
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
292
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
293
|
+
expect(requestSignal).toBeInstanceOf(AbortSignal);
|
|
294
|
+
|
|
295
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
296
|
+
await Promise.all([firstRequest, secondRequest]);
|
|
297
|
+
expect(requestSignal?.aborted).toBe(true);
|
|
298
|
+
expect(values.has(refreshTokenKey)).toBe(false);
|
|
299
|
+
|
|
300
|
+
await expect(
|
|
301
|
+
new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
|
|
302
|
+
).resolves.toEqual({ Authorization: `Bearer ${accessToken}` });
|
|
303
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("aborts timed out refresh requests without replacing the identity", async () => {
|
|
307
|
+
vi.useFakeTimers();
|
|
308
|
+
const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]);
|
|
309
|
+
installLocalStorage({
|
|
310
|
+
getItem: (key) => values.get(key) ?? null,
|
|
311
|
+
setItem: (key, value) => {
|
|
312
|
+
values.set(key, value);
|
|
313
|
+
},
|
|
314
|
+
removeItem: (key) => {
|
|
315
|
+
values.delete(key);
|
|
316
|
+
},
|
|
317
|
+
} as Storage);
|
|
318
|
+
let requestSignal: AbortSignal | null | undefined;
|
|
319
|
+
const rotatedRefreshToken = { token: "r2", expires_at: "2099-02-01" };
|
|
320
|
+
const fetchMock = vi
|
|
321
|
+
.fn()
|
|
322
|
+
.mockImplementationOnce(
|
|
323
|
+
(_input: RequestInfo | URL, init?: RequestInit) =>
|
|
324
|
+
new Promise<Response>((_resolve, reject) => {
|
|
325
|
+
requestSignal = init?.signal;
|
|
326
|
+
requestSignal?.addEventListener(
|
|
327
|
+
"abort",
|
|
328
|
+
() => reject(requestSignal?.reason),
|
|
329
|
+
{ once: true },
|
|
330
|
+
);
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
.mockResolvedValueOnce({
|
|
334
|
+
ok: true,
|
|
335
|
+
json: vi.fn().mockResolvedValue({
|
|
336
|
+
access_token: accessToken,
|
|
337
|
+
refresh_token: rotatedRefreshToken,
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
341
|
+
const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
342
|
+
|
|
343
|
+
const request = expect(strategy.getAuthHeaders()).rejects.toThrow(
|
|
344
|
+
"Assistant Cloud refresh token request timed out after 30000ms",
|
|
345
|
+
);
|
|
346
|
+
expect(requestSignal).toBeInstanceOf(AbortSignal);
|
|
347
|
+
|
|
348
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
349
|
+
await request;
|
|
350
|
+
expect(requestSignal?.aborted).toBe(true);
|
|
351
|
+
expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
|
|
352
|
+
|
|
353
|
+
await expect(strategy.getAuthHeaders()).resolves.toEqual({
|
|
354
|
+
Authorization: `Bearer ${accessToken}`,
|
|
355
|
+
});
|
|
356
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
357
|
+
expect(values.get(refreshTokenKey)).toBe(
|
|
358
|
+
JSON.stringify(rotatedRefreshToken),
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("keeps anonymous token requests independent without localStorage", async () => {
|
|
363
|
+
delete (globalThis as { localStorage?: Storage }).localStorage;
|
|
364
|
+
const lockRequest = vi.fn();
|
|
365
|
+
vi.stubGlobal("navigator", { locks: { request: lockRequest } });
|
|
366
|
+
const fetchMock = mockAnonymousTokenFetch();
|
|
367
|
+
const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
368
|
+
const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
|
|
369
|
+
|
|
370
|
+
await expect(
|
|
371
|
+
Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]),
|
|
372
|
+
).resolves.toEqual([
|
|
373
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
374
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
375
|
+
]);
|
|
376
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
377
|
+
expect(lockRequest).not.toHaveBeenCalled();
|
|
378
|
+
});
|
|
379
|
+
|
|
109
380
|
it("scopes anonymous refresh tokens by backend", async () => {
|
|
110
381
|
const secondBaseUrl = "https://other.example.com";
|
|
111
382
|
const values = new Map<string, string>();
|
|
@@ -140,20 +411,20 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
140
411
|
});
|
|
141
412
|
vi.stubGlobal("fetch", fetchMock);
|
|
142
413
|
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
secondBaseUrl,
|
|
146
|
-
)
|
|
414
|
+
await Promise.all([
|
|
415
|
+
new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
|
|
416
|
+
new AssistantCloudAnonymousAuthStrategy(secondBaseUrl).getAuthHeaders(),
|
|
417
|
+
]);
|
|
147
418
|
|
|
148
419
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
149
420
|
1,
|
|
150
421
|
`${baseUrl}/v1/auth/tokens/anonymous`,
|
|
151
|
-
{ method: "POST" },
|
|
422
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
152
423
|
);
|
|
153
424
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
154
425
|
2,
|
|
155
426
|
`${secondBaseUrl}/v1/auth/tokens/anonymous`,
|
|
156
|
-
{ method: "POST" },
|
|
427
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
157
428
|
);
|
|
158
429
|
expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
|
|
159
430
|
expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
|
|
@@ -206,12 +477,13 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
206
477
|
method: "POST",
|
|
207
478
|
headers: { "Content-Type": "application/json" },
|
|
208
479
|
body: JSON.stringify({ refresh_token: refreshToken.token }),
|
|
480
|
+
signal: expect.any(AbortSignal),
|
|
209
481
|
},
|
|
210
482
|
);
|
|
211
483
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
212
484
|
2,
|
|
213
485
|
`${secondBaseUrl}/v1/auth/tokens/anonymous`,
|
|
214
|
-
{ method: "POST" },
|
|
486
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
215
487
|
);
|
|
216
488
|
expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
|
|
217
489
|
expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
|
|
@@ -270,12 +542,13 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
270
542
|
method: "POST",
|
|
271
543
|
headers: { "Content-Type": "application/json" },
|
|
272
544
|
body: JSON.stringify({ refresh_token: scopedRefreshToken.token }),
|
|
545
|
+
signal: expect.any(AbortSignal),
|
|
273
546
|
},
|
|
274
547
|
);
|
|
275
548
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
276
549
|
2,
|
|
277
550
|
`${secondBaseUrl}/v1/auth/tokens/anonymous`,
|
|
278
|
-
{ method: "POST" },
|
|
551
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
279
552
|
);
|
|
280
553
|
expect(values.has("aui:refresh_token")).toBe(false);
|
|
281
554
|
expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
|
|
@@ -470,6 +743,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
470
743
|
method: "POST",
|
|
471
744
|
headers: { "Content-Type": "application/json" },
|
|
472
745
|
body: JSON.stringify({ refresh_token: refreshToken.token }),
|
|
746
|
+
signal: expect.any(AbortSignal),
|
|
473
747
|
},
|
|
474
748
|
);
|
|
475
749
|
expect(setItem).not.toHaveBeenCalled();
|
|
@@ -538,7 +812,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
|
|
|
538
812
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
539
813
|
2,
|
|
540
814
|
`${baseUrl}/v1/auth/tokens/anonymous`,
|
|
541
|
-
{ method: "POST" },
|
|
815
|
+
{ method: "POST", signal: expect.any(AbortSignal) },
|
|
542
816
|
);
|
|
543
817
|
expect(values.get(refreshTokenKey)).toBe(
|
|
544
818
|
JSON.stringify(replacementRefreshToken),
|
|
@@ -108,6 +108,99 @@ describe("CloudMessagePersistence", () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("deduplicates concurrent child appends while the parent is pending", async () => {
|
|
112
|
+
let resolveParent!: (value: { message_id: string }) => void;
|
|
113
|
+
vi.mocked(cloud.threads.messages.create)
|
|
114
|
+
.mockImplementationOnce(
|
|
115
|
+
() =>
|
|
116
|
+
new Promise((resolve) => {
|
|
117
|
+
resolveParent = resolve;
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
.mockResolvedValue({ message_id: "remote-child" });
|
|
121
|
+
|
|
122
|
+
const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
|
|
123
|
+
text: "parent",
|
|
124
|
+
});
|
|
125
|
+
const firstChild = persistence.append(
|
|
126
|
+
"thread-1",
|
|
127
|
+
"child",
|
|
128
|
+
"parent",
|
|
129
|
+
"aui/v0",
|
|
130
|
+
{ text: "child" },
|
|
131
|
+
);
|
|
132
|
+
const secondChild = persistence.append(
|
|
133
|
+
"thread-1",
|
|
134
|
+
"child",
|
|
135
|
+
"parent",
|
|
136
|
+
"aui/v0",
|
|
137
|
+
{ text: "child" },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(persistence.isPersisted("child")).toBe(true);
|
|
141
|
+
resolveParent({ message_id: "remote-parent" });
|
|
142
|
+
await Promise.all([parent, firstChild, secondChild]);
|
|
143
|
+
|
|
144
|
+
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("resolves remote IDs throughout a concurrent message chain", async () => {
|
|
148
|
+
let resolveParent!: (value: { message_id: string }) => void;
|
|
149
|
+
vi.mocked(cloud.threads.messages.create)
|
|
150
|
+
.mockImplementationOnce(
|
|
151
|
+
() =>
|
|
152
|
+
new Promise((resolve) => {
|
|
153
|
+
resolveParent = resolve;
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
.mockResolvedValueOnce({ message_id: "remote-child" })
|
|
157
|
+
.mockResolvedValueOnce({ message_id: "remote-grandchild" });
|
|
158
|
+
|
|
159
|
+
const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
|
|
160
|
+
text: "parent",
|
|
161
|
+
});
|
|
162
|
+
const child = persistence.append("thread-1", "child", "parent", "aui/v0", {
|
|
163
|
+
text: "child",
|
|
164
|
+
});
|
|
165
|
+
const grandchild = persistence.append(
|
|
166
|
+
"thread-1",
|
|
167
|
+
"grandchild",
|
|
168
|
+
"child",
|
|
169
|
+
"aui/v0",
|
|
170
|
+
{ text: "grandchild" },
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
resolveParent({ message_id: "remote-parent" });
|
|
174
|
+
await Promise.all([parent, child, grandchild]);
|
|
175
|
+
|
|
176
|
+
expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
|
|
177
|
+
parent_id: "remote-parent",
|
|
178
|
+
format: "aui/v0",
|
|
179
|
+
content: { text: "child" },
|
|
180
|
+
});
|
|
181
|
+
expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
|
|
182
|
+
parent_id: "remote-child",
|
|
183
|
+
format: "aui/v0",
|
|
184
|
+
content: { text: "grandchild" },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("re-appends a message after its mapping has settled", async () => {
|
|
189
|
+
vi.mocked(cloud.threads.messages.create)
|
|
190
|
+
.mockResolvedValueOnce({ message_id: "remote-1" })
|
|
191
|
+
.mockResolvedValueOnce({ message_id: "remote-2" });
|
|
192
|
+
|
|
193
|
+
await persistence.append("thread-1", "local-1", null, "aui/v0", {
|
|
194
|
+
text: "first",
|
|
195
|
+
});
|
|
196
|
+
await persistence.append("thread-1", "local-1", null, "aui/v0", {
|
|
197
|
+
text: "second",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
|
|
201
|
+
expect(await persistence.getRemoteId("local-1")).toBe("remote-2");
|
|
202
|
+
});
|
|
203
|
+
|
|
111
204
|
it("loaded messages are marked as persisted and not re-created", async () => {
|
|
112
205
|
vi.mocked(cloud.threads.messages.list).mockResolvedValue({
|
|
113
206
|
messages: [
|