@convex-dev/agent 0.1.8-alpha.2 → 0.1.9-alpha.0
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 -0
- package/dist/commonjs/component/_generated/api.d.ts +18 -0
- package/dist/commonjs/component/messages.d.ts.map +1 -1
- package/dist/commonjs/component/messages.js +1 -0
- package/dist/commonjs/component/messages.js.map +1 -1
- package/dist/commonjs/component/streams.d.ts.map +1 -1
- package/dist/commonjs/component/streams.js +0 -1
- package/dist/commonjs/component/streams.js.map +1 -1
- package/dist/commonjs/component/threads.d.ts.map +1 -1
- package/dist/commonjs/component/threads.js +6 -7
- package/dist/commonjs/component/threads.js.map +1 -1
- package/dist/commonjs/component/users.d.ts +9 -0
- package/dist/commonjs/component/users.d.ts.map +1 -1
- package/dist/commonjs/component/users.js +92 -21
- package/dist/commonjs/component/users.js.map +1 -1
- package/dist/commonjs.tsbuildinfo +1 -1
- package/dist/esm/component/_generated/api.d.ts +18 -0
- package/dist/esm/component/messages.d.ts.map +1 -1
- package/dist/esm/component/messages.js +1 -0
- package/dist/esm/component/messages.js.map +1 -1
- package/dist/esm/component/streams.d.ts.map +1 -1
- package/dist/esm/component/streams.js +0 -1
- package/dist/esm/component/streams.js.map +1 -1
- package/dist/esm/component/threads.d.ts.map +1 -1
- package/dist/esm/component/threads.js +6 -7
- package/dist/esm/component/threads.js.map +1 -1
- package/dist/esm/component/users.d.ts +9 -0
- package/dist/esm/component/users.d.ts.map +1 -1
- package/dist/esm/component/users.js +92 -21
- package/dist/esm/component/users.js.map +1 -1
- package/dist/esm.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/client/setup.test.ts +1 -2
- package/src/component/_generated/api.d.ts +18 -0
- package/src/component/messages.ts +1 -0
- package/src/component/streams.ts +1 -3
- package/src/component/threads.test.ts +566 -0
- package/src/component/threads.ts +10 -6
- package/src/component/users.test.ts +478 -0
- package/src/component/users.ts +100 -20
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { convexTest } from "convex-test";
|
|
4
|
+
import { describe, expect, test, vi } from "vitest";
|
|
5
|
+
import { api, internal } from "./_generated/api.js";
|
|
6
|
+
import type { Id } from "./_generated/dataModel.js";
|
|
7
|
+
import schema from "./schema.js";
|
|
8
|
+
import { modules } from "./setup.test.js";
|
|
9
|
+
|
|
10
|
+
describe("threads", () => {
|
|
11
|
+
test("createThread creates a thread with correct data", async () => {
|
|
12
|
+
const t = convexTest(schema, modules);
|
|
13
|
+
|
|
14
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
15
|
+
userId: "testUser",
|
|
16
|
+
title: "Test Thread",
|
|
17
|
+
summary: "A test thread",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(thread.userId).toBe("testUser");
|
|
21
|
+
expect(thread.title).toBe("Test Thread");
|
|
22
|
+
expect(thread.summary).toBe("A test thread");
|
|
23
|
+
expect(thread.status).toBe("active");
|
|
24
|
+
expect(thread._id).toBeTruthy();
|
|
25
|
+
expect(thread._creationTime).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("getThread returns thread data", async () => {
|
|
29
|
+
const t = convexTest(schema, modules);
|
|
30
|
+
|
|
31
|
+
const created = await t.mutation(api.threads.createThread, {
|
|
32
|
+
userId: "getUser",
|
|
33
|
+
title: "Get Thread",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const fetched = await t.query(api.threads.getThread, {
|
|
37
|
+
threadId: created._id as Id<"threads">,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(fetched).not.toBeNull();
|
|
41
|
+
expect(fetched!._id).toBe(created._id);
|
|
42
|
+
expect(fetched!.userId).toBe("getUser");
|
|
43
|
+
expect(fetched!.title).toBe("Get Thread");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("getThread returns null for non-existent thread", async () => {
|
|
47
|
+
const t = convexTest(schema, modules);
|
|
48
|
+
|
|
49
|
+
// Create a thread, then delete it to get a valid but non-existent ID
|
|
50
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
51
|
+
userId: "tempUser",
|
|
52
|
+
title: "Temp Thread",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Delete the thread so it no longer exists
|
|
56
|
+
await t.action(api.threads.deleteAllForThreadIdSync, {
|
|
57
|
+
threadId: thread._id as Id<"threads">,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Now test with the deleted thread's ID
|
|
61
|
+
const result = await t.query(api.threads.getThread, {
|
|
62
|
+
threadId: thread._id as Id<"threads">,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("listThreadsByUserId returns threads for user", async () => {
|
|
69
|
+
const t = convexTest(schema, modules);
|
|
70
|
+
|
|
71
|
+
// Create threads for different users
|
|
72
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
73
|
+
userId: "listUser",
|
|
74
|
+
title: "Thread 1",
|
|
75
|
+
});
|
|
76
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
77
|
+
userId: "listUser",
|
|
78
|
+
title: "Thread 2",
|
|
79
|
+
});
|
|
80
|
+
const thread3 = await t.mutation(api.threads.createThread, {
|
|
81
|
+
userId: "otherUser",
|
|
82
|
+
title: "Other Thread",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await t.query(api.threads.listThreadsByUserId, {
|
|
86
|
+
userId: "listUser",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.page).toHaveLength(2);
|
|
90
|
+
expect(result.page.map((t) => t._id)).toContain(thread1._id);
|
|
91
|
+
expect(result.page.map((t) => t._id)).toContain(thread2._id);
|
|
92
|
+
expect(result.page.map((t) => t._id)).not.toContain(thread3._id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("listThreadsByUserId pagination works", async () => {
|
|
96
|
+
const t = convexTest(schema, modules);
|
|
97
|
+
|
|
98
|
+
// Create multiple threads
|
|
99
|
+
const threads = [];
|
|
100
|
+
for (let i = 0; i < 5; i++) {
|
|
101
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
102
|
+
userId: "paginationUser",
|
|
103
|
+
title: `Thread ${i}`,
|
|
104
|
+
});
|
|
105
|
+
threads.push(thread);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const firstPage = await t.query(api.threads.listThreadsByUserId, {
|
|
109
|
+
userId: "paginationUser",
|
|
110
|
+
paginationOpts: { cursor: null, numItems: 2 },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(firstPage.page).toHaveLength(2);
|
|
114
|
+
expect(firstPage.isDone).toBe(false);
|
|
115
|
+
|
|
116
|
+
const secondPage = await t.query(api.threads.listThreadsByUserId, {
|
|
117
|
+
userId: "paginationUser",
|
|
118
|
+
paginationOpts: { cursor: firstPage.continueCursor, numItems: 2 },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(secondPage.page).toHaveLength(2);
|
|
122
|
+
// Should not have duplicate threads
|
|
123
|
+
const firstPageIds = firstPage.page.map((t) => t._id);
|
|
124
|
+
const secondPageIds = secondPage.page.map((t) => t._id);
|
|
125
|
+
expect(firstPageIds.every((id) => !secondPageIds.includes(id))).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("listThreadsByUserId ordering works", async () => {
|
|
129
|
+
const t = convexTest(schema, modules);
|
|
130
|
+
|
|
131
|
+
// Create threads with slight delays to ensure different creation times
|
|
132
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
133
|
+
userId: "orderUser",
|
|
134
|
+
title: "First Thread",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Small delay to ensure different creation times
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
139
|
+
|
|
140
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
141
|
+
userId: "orderUser",
|
|
142
|
+
title: "Second Thread",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Test descending order (default)
|
|
146
|
+
const descResult = await t.query(api.threads.listThreadsByUserId, {
|
|
147
|
+
userId: "orderUser",
|
|
148
|
+
order: "desc",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(descResult.page[0]._id).toBe(thread2._id);
|
|
152
|
+
expect(descResult.page[1]._id).toBe(thread1._id);
|
|
153
|
+
|
|
154
|
+
// Test ascending order
|
|
155
|
+
const ascResult = await t.query(api.threads.listThreadsByUserId, {
|
|
156
|
+
userId: "orderUser",
|
|
157
|
+
order: "asc",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(ascResult.page[0]._id).toBe(thread1._id);
|
|
161
|
+
expect(ascResult.page[1]._id).toBe(thread2._id);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("updateThread updates thread fields", async () => {
|
|
165
|
+
const t = convexTest(schema, modules);
|
|
166
|
+
|
|
167
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
168
|
+
userId: "updateUser",
|
|
169
|
+
title: "Original Title",
|
|
170
|
+
summary: "Original Summary",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const updated = await t.mutation(api.threads.updateThread, {
|
|
174
|
+
threadId: thread._id as Id<"threads">,
|
|
175
|
+
patch: {
|
|
176
|
+
title: "Updated Title",
|
|
177
|
+
summary: "Updated Summary",
|
|
178
|
+
status: "archived",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(updated.title).toBe("Updated Title");
|
|
183
|
+
expect(updated.summary).toBe("Updated Summary");
|
|
184
|
+
expect(updated.status).toBe("archived");
|
|
185
|
+
expect(updated._id).toBe(thread._id);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("updateThread with partial patch works", async () => {
|
|
189
|
+
const t = convexTest(schema, modules);
|
|
190
|
+
|
|
191
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
192
|
+
userId: "partialUser",
|
|
193
|
+
title: "Original Title",
|
|
194
|
+
summary: "Original Summary",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const updated = await t.mutation(api.threads.updateThread, {
|
|
198
|
+
threadId: thread._id as Id<"threads">,
|
|
199
|
+
patch: {
|
|
200
|
+
title: "New Title Only",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(updated.title).toBe("New Title Only");
|
|
205
|
+
expect(updated.summary).toBe("Original Summary"); // Unchanged
|
|
206
|
+
expect(updated.status).toBe("active"); // Unchanged
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("updateThread throws error for non-existent thread", async () => {
|
|
210
|
+
const t = convexTest(schema, modules);
|
|
211
|
+
|
|
212
|
+
// Create a thread, then delete it to get a valid but non-existent ID
|
|
213
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
214
|
+
userId: "tempUser",
|
|
215
|
+
title: "Temp Thread",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Delete the thread so it no longer exists
|
|
219
|
+
await t.action(api.threads.deleteAllForThreadIdSync, {
|
|
220
|
+
threadId: thread._id as Id<"threads">,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Now test updating the deleted thread
|
|
224
|
+
await expect(
|
|
225
|
+
t.mutation(api.threads.updateThread, {
|
|
226
|
+
threadId: thread._id as Id<"threads">,
|
|
227
|
+
patch: { title: "New Title" },
|
|
228
|
+
})
|
|
229
|
+
).rejects.toThrow();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("deleteAllForThreadIdSync deletes thread and all related data", async () => {
|
|
233
|
+
const t = convexTest(schema, modules);
|
|
234
|
+
|
|
235
|
+
// Create a thread with messages
|
|
236
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
237
|
+
userId: "deleteUser",
|
|
238
|
+
title: "Thread to Delete",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Add messages to the thread
|
|
242
|
+
await t.mutation(api.messages.addMessages, {
|
|
243
|
+
threadId: thread._id as Id<"threads">,
|
|
244
|
+
messages: [
|
|
245
|
+
{ message: { role: "user" as const, content: "Hello" } },
|
|
246
|
+
{ message: { role: "assistant" as const, content: "Hi there" } },
|
|
247
|
+
{ message: { role: "user" as const, content: "How are you?" } },
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Verify data exists before deletion
|
|
252
|
+
const beforeMessages = await t.query(api.messages.listMessagesByThreadId, {
|
|
253
|
+
threadId: thread._id as Id<"threads">,
|
|
254
|
+
order: "desc",
|
|
255
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
256
|
+
});
|
|
257
|
+
expect(beforeMessages.page).toHaveLength(3);
|
|
258
|
+
|
|
259
|
+
const beforeThread = await t.query(api.threads.getThread, {
|
|
260
|
+
threadId: thread._id as Id<"threads">,
|
|
261
|
+
});
|
|
262
|
+
expect(beforeThread).not.toBeNull();
|
|
263
|
+
|
|
264
|
+
// Delete the thread synchronously
|
|
265
|
+
await t.action(api.threads.deleteAllForThreadIdSync, {
|
|
266
|
+
threadId: thread._id as Id<"threads">,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Verify thread is deleted
|
|
270
|
+
const afterThread = await t.query(api.threads.getThread, {
|
|
271
|
+
threadId: thread._id as Id<"threads">,
|
|
272
|
+
});
|
|
273
|
+
expect(afterThread).toBeNull();
|
|
274
|
+
|
|
275
|
+
// Verify messages are deleted (this will return empty page since thread is gone)
|
|
276
|
+
const afterMessages = await t.query(api.messages.listMessagesByThreadId, {
|
|
277
|
+
threadId: thread._id as Id<"threads">,
|
|
278
|
+
order: "desc",
|
|
279
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
280
|
+
});
|
|
281
|
+
expect(afterMessages.page).toHaveLength(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("deleteAllForThreadIdAsync deletes thread asynchronously", async () => {
|
|
285
|
+
// Enable fake timers for scheduled function testing
|
|
286
|
+
vi.useFakeTimers();
|
|
287
|
+
|
|
288
|
+
const t = convexTest(schema, modules);
|
|
289
|
+
|
|
290
|
+
// Create a thread with messages
|
|
291
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
292
|
+
userId: "asyncDeleteUser",
|
|
293
|
+
title: "Async Thread to Delete",
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await t.mutation(api.messages.addMessages, {
|
|
297
|
+
threadId: thread._id as Id<"threads">,
|
|
298
|
+
messages: Array.from({ length: 100 }, (_, i) => ({
|
|
299
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
300
|
+
})),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Start async deletion
|
|
304
|
+
const result = await t.mutation(api.threads.deleteAllForThreadIdAsync, {
|
|
305
|
+
threadId: thread._id as Id<"threads">,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// If there's more work to do, advance timers and wait for scheduled functions
|
|
309
|
+
if (!result.isDone) {
|
|
310
|
+
// Run all pending timers to trigger scheduled functions
|
|
311
|
+
vi.runAllTimers();
|
|
312
|
+
|
|
313
|
+
// Wait for all scheduled functions to complete
|
|
314
|
+
await t.finishInProgressScheduledFunctions();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Verify thread is deleted
|
|
318
|
+
const afterThread = await t.query(api.threads.getThread, {
|
|
319
|
+
threadId: thread._id as Id<"threads">,
|
|
320
|
+
});
|
|
321
|
+
expect(afterThread).toBeNull();
|
|
322
|
+
|
|
323
|
+
// Verify streams are deleted
|
|
324
|
+
const afterStreams = await t.query(api.streams.list, {
|
|
325
|
+
threadId: thread._id as Id<"threads">,
|
|
326
|
+
});
|
|
327
|
+
expect(afterStreams).toHaveLength(0);
|
|
328
|
+
|
|
329
|
+
// Reset to normal timers
|
|
330
|
+
vi.useRealTimers();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("deletePageForThreadId handles pagination correctly", async () => {
|
|
334
|
+
const t = convexTest(schema, modules);
|
|
335
|
+
|
|
336
|
+
// Create a thread with many messages to test pagination
|
|
337
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
338
|
+
userId: "paginationDeleteUser",
|
|
339
|
+
title: "Pagination Delete Thread",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Add many messages to force pagination
|
|
343
|
+
const messages = [];
|
|
344
|
+
for (let i = 0; i < 150; i++) {
|
|
345
|
+
messages.push({
|
|
346
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
await t.mutation(api.messages.addMessages, {
|
|
350
|
+
threadId: thread._id as Id<"threads">,
|
|
351
|
+
messages,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Delete first page
|
|
355
|
+
const result1 = await t.mutation(internal.threads._deletePageForThreadId, {
|
|
356
|
+
threadId: thread._id as Id<"threads">,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(result1.isDone).toBe(false);
|
|
360
|
+
expect(result1.cursor).toBeTruthy();
|
|
361
|
+
|
|
362
|
+
// Continue deleting pages until done
|
|
363
|
+
let currentResult = result1;
|
|
364
|
+
let iterations = 0;
|
|
365
|
+
while (!currentResult.isDone && iterations < 10) {
|
|
366
|
+
currentResult = await t.mutation(
|
|
367
|
+
internal.threads._deletePageForThreadId,
|
|
368
|
+
{
|
|
369
|
+
threadId: thread._id as Id<"threads">,
|
|
370
|
+
cursor: currentResult.cursor,
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
iterations++;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Thread should be deleted when done
|
|
377
|
+
expect(currentResult.isDone).toBe(true);
|
|
378
|
+
const afterThread = await t.query(api.threads.getThread, {
|
|
379
|
+
threadId: thread._id as Id<"threads">,
|
|
380
|
+
});
|
|
381
|
+
expect(afterThread).toBeNull();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("deletePageForThreadId with custom limit works", async () => {
|
|
385
|
+
const t = convexTest(schema, modules);
|
|
386
|
+
|
|
387
|
+
// Create a thread with messages
|
|
388
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
389
|
+
userId: "limitUser",
|
|
390
|
+
title: "Limit Test Thread",
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Add messages
|
|
394
|
+
const messages = [];
|
|
395
|
+
for (let i = 0; i < 50; i++) {
|
|
396
|
+
messages.push({
|
|
397
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
await t.mutation(api.messages.addMessages, {
|
|
401
|
+
threadId: thread._id as Id<"threads">,
|
|
402
|
+
messages,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Delete with custom limit
|
|
406
|
+
const result = await t.mutation(internal.threads._deletePageForThreadId, {
|
|
407
|
+
threadId: thread._id as Id<"threads">,
|
|
408
|
+
limit: 25,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Should still have messages left with this limit
|
|
412
|
+
expect(result.isDone).toBe(false);
|
|
413
|
+
expect(result.cursor).toBeTruthy();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("deleteAllForThreadIdAsync with multiple scheduled iterations", async () => {
|
|
417
|
+
// Enable fake timers for scheduled function testing
|
|
418
|
+
vi.useFakeTimers();
|
|
419
|
+
|
|
420
|
+
const t = convexTest(schema, modules);
|
|
421
|
+
|
|
422
|
+
// Create a thread with many messages to force multiple scheduling iterations
|
|
423
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
424
|
+
userId: "multiIterDeleteUser",
|
|
425
|
+
title: "Multi Iter Delete Thread",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Add many messages to force pagination and multiple scheduled iterations
|
|
429
|
+
const messages = [];
|
|
430
|
+
for (let i = 0; i < 300; i++) {
|
|
431
|
+
messages.push({
|
|
432
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
await t.mutation(api.messages.addMessages, {
|
|
436
|
+
threadId: thread._id as Id<"threads">,
|
|
437
|
+
messages,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Start async deletion
|
|
441
|
+
const result = await t.mutation(api.threads.deleteAllForThreadIdAsync, {
|
|
442
|
+
threadId: thread._id as Id<"threads">,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Should not be done immediately with this many messages
|
|
446
|
+
expect(result.isDone).toBe(false);
|
|
447
|
+
|
|
448
|
+
// Use finishAllScheduledFunctions to handle recursive scheduling
|
|
449
|
+
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
|
450
|
+
|
|
451
|
+
// Verify thread is deleted
|
|
452
|
+
const afterThread = await t.query(api.threads.getThread, {
|
|
453
|
+
threadId: thread._id as Id<"threads">,
|
|
454
|
+
});
|
|
455
|
+
expect(afterThread).toBeNull();
|
|
456
|
+
|
|
457
|
+
// Reset to normal timers
|
|
458
|
+
vi.useRealTimers();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("deleteAllForThreadIdSync handles thread with no messages", async () => {
|
|
462
|
+
const t = convexTest(schema, modules);
|
|
463
|
+
|
|
464
|
+
// Create a thread with no messages
|
|
465
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
466
|
+
userId: "emptyUser",
|
|
467
|
+
title: "Empty Thread",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Delete should work without errors
|
|
471
|
+
await expect(
|
|
472
|
+
t.action(api.threads.deleteAllForThreadIdSync, {
|
|
473
|
+
threadId: thread._id as Id<"threads">,
|
|
474
|
+
})
|
|
475
|
+
).resolves.not.toThrow();
|
|
476
|
+
|
|
477
|
+
// Thread should be deleted
|
|
478
|
+
const afterThread = await t.query(api.threads.getThread, {
|
|
479
|
+
threadId: thread._id as Id<"threads">,
|
|
480
|
+
});
|
|
481
|
+
expect(afterThread).toBeNull();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("deleteAllForThreadIdSync handles non-existent thread", async () => {
|
|
485
|
+
const t = convexTest(schema, modules);
|
|
486
|
+
|
|
487
|
+
// Create a thread, then delete it to get a valid but non-existent ID
|
|
488
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
489
|
+
userId: "tempUser",
|
|
490
|
+
title: "Temp Thread",
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Delete the thread so it no longer exists
|
|
494
|
+
await t.action(api.threads.deleteAllForThreadIdSync, {
|
|
495
|
+
threadId: thread._id as Id<"threads">,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Try to delete the already-deleted thread - should not throw
|
|
499
|
+
await expect(
|
|
500
|
+
t.action(api.threads.deleteAllForThreadIdSync, {
|
|
501
|
+
threadId: thread._id as Id<"threads">,
|
|
502
|
+
})
|
|
503
|
+
).resolves.not.toThrow();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("thread creation sets status to active by default", async () => {
|
|
507
|
+
const t = convexTest(schema, modules);
|
|
508
|
+
|
|
509
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
510
|
+
userId: "statusUser",
|
|
511
|
+
title: "Status Test Thread",
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(thread.status).toBe("active");
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("multiple threads for same user work correctly", async () => {
|
|
518
|
+
const t = convexTest(schema, modules);
|
|
519
|
+
|
|
520
|
+
const threads = [];
|
|
521
|
+
for (let i = 0; i < 3; i++) {
|
|
522
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
523
|
+
userId: "multiThreadUser",
|
|
524
|
+
title: `Thread ${i + 1}`,
|
|
525
|
+
});
|
|
526
|
+
threads.push(thread);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const result = await t.query(api.threads.listThreadsByUserId, {
|
|
530
|
+
userId: "multiThreadUser",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(result.page).toHaveLength(3);
|
|
534
|
+
expect(result.page.map((t) => t.title)).toContain("Thread 1");
|
|
535
|
+
expect(result.page.map((t) => t.title)).toContain("Thread 2");
|
|
536
|
+
expect(result.page.map((t) => t.title)).toContain("Thread 3");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("threads with different users are isolated", async () => {
|
|
540
|
+
const t = convexTest(schema, modules);
|
|
541
|
+
|
|
542
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
543
|
+
userId: "user1",
|
|
544
|
+
title: "User 1 Thread",
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
548
|
+
userId: "user2",
|
|
549
|
+
title: "User 2 Thread",
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const user1Threads = await t.query(api.threads.listThreadsByUserId, {
|
|
553
|
+
userId: "user1",
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const user2Threads = await t.query(api.threads.listThreadsByUserId, {
|
|
557
|
+
userId: "user2",
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(user1Threads.page).toHaveLength(1);
|
|
561
|
+
expect(user1Threads.page[0]._id).toBe(thread1._id);
|
|
562
|
+
|
|
563
|
+
expect(user2Threads.page).toHaveLength(1);
|
|
564
|
+
expect(user2Threads.page[0]._id).toBe(thread2._id);
|
|
565
|
+
});
|
|
566
|
+
});
|
package/src/component/threads.ts
CHANGED
|
@@ -157,12 +157,13 @@ export const deleteAllForThreadIdAsync = mutation({
|
|
|
157
157
|
threadId: args.threadId,
|
|
158
158
|
cursor: result.cursor,
|
|
159
159
|
});
|
|
160
|
-
} else {
|
|
161
|
-
// Kick off the streams deletion
|
|
162
|
-
await ctx.scheduler.runAfter(0, api.threads.deleteAllForThreadIdSync, {
|
|
163
|
-
threadId: args.threadId,
|
|
164
|
-
});
|
|
165
160
|
}
|
|
161
|
+
// Kick off the streams deletion
|
|
162
|
+
await ctx.scheduler.runAfter(
|
|
163
|
+
0,
|
|
164
|
+
api.streams.deleteAllStreamsForThreadIdSync,
|
|
165
|
+
{ threadId: args.threadId }
|
|
166
|
+
);
|
|
166
167
|
return result;
|
|
167
168
|
},
|
|
168
169
|
returns: deleteThreadReturns,
|
|
@@ -183,7 +184,10 @@ async function deletePageForThreadIdHandler(
|
|
|
183
184
|
});
|
|
184
185
|
await Promise.all(messages.page.map((m) => deleteMessage(ctx, m)));
|
|
185
186
|
if (messages.isDone) {
|
|
186
|
-
await ctx.db.
|
|
187
|
+
const thread = await ctx.db.get(args.threadId);
|
|
188
|
+
if (thread) {
|
|
189
|
+
await ctx.db.delete(args.threadId);
|
|
190
|
+
}
|
|
187
191
|
}
|
|
188
192
|
return {
|
|
189
193
|
cursor: messages.continueCursor,
|