Skip to content

Commit 7fdf5bf

Browse files
committed
Add tests for reply-thread dedupe and thread_id repoints
1 parent cc27b6b commit 7fdf5bf

1 file changed

Lines changed: 65 additions & 5 deletions

File tree

‎packages/chat/test/migrations.test.ts‎

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ describeIfDb("0025_workbench_threads_unique_key dedupe", () => {
264264
}
265265
}, 20000);
266266

267-
test("collapses two seeded root threads, repointing thread membership at the kept row", async () => {
267+
test("collapses duplicate root and reply threads, repointing thread_id and parent_thread_id references at the kept row", async () => {
268268
// Replay every migration through 0023 by hand, recording each in
269269
// the same ledger table `applyChatMigrations` reads, so the two
270270
// duplicate roots below can be seeded *before* 0025 exists to
@@ -291,16 +291,40 @@ describeIfDb("0025_workbench_threads_unique_key dedupe", () => {
291291

292292
await seed.unsafe(`
293293
INSERT INTO "chat"."workbench_threads"
294-
(id, tenant_id, workbench_id, kind, created_at)
294+
(id, tenant_id, workbench_id, kind, parent_message_id, created_at)
295295
VALUES
296-
('thr_root_old', 'tnt_dedupe', 'wb_dedupe', 'root', now() - interval '1 hour'),
297-
('thr_root_new', 'tnt_dedupe', 'wb_dedupe', 'root', now())
296+
('thr_root_old', 'tnt_dedupe', 'wb_dedupe', 'root', NULL, now() - interval '1 hour'),
297+
('thr_root_new', 'tnt_dedupe', 'wb_dedupe', 'root', NULL, now()),
298+
('thr_reply_old', 'tnt_dedupe', 'wb_dedupe', 'reply', 'msg_parent', now() - interval '1 hour'),
299+
('thr_reply_new', 'tnt_dedupe', 'wb_dedupe', 'reply', 'msg_parent', now())
298300
`);
299301
await seed.unsafe(`
300302
INSERT INTO "chat"."workbench_thread_messages"
301303
(tenant_id, workbench_id, thread_id, message_id)
302304
VALUES ('tnt_dedupe', 'wb_dedupe', 'thr_root_new', 'msg_dedupe')
303305
`);
306+
// A message parked in the dropped reply duplicate — proves
307+
// `workbench_messages.thread_id` is repointed, not just
308+
// `workbench_thread_messages.thread_id`.
309+
await seed.unsafe(`
310+
INSERT INTO "chat"."workbench_messages"
311+
(id, tenant_id, workbench_id, sender_address, thread_id, parts)
312+
VALUES (
313+
'msg_in_dropped_reply', 'tnt_dedupe', 'wb_dedupe', 'addr_1',
314+
'thr_reply_new', '[]'::jsonb
315+
)
316+
`);
317+
// A depth-2 thread anchored off the dropped root duplicate —
318+
// proves `workbench_threads.parent_thread_id` is repointed too,
319+
// not just message-facing references.
320+
await seed.unsafe(`
321+
INSERT INTO "chat"."workbench_threads"
322+
(id, tenant_id, workbench_id, kind, parent_message_id, parent_thread_id, created_at)
323+
VALUES (
324+
'thr_child_of_dropped_root', 'tnt_dedupe', 'wb_dedupe', 'reply',
325+
'msg_child', 'thr_root_new', now()
326+
)
327+
`);
304328
} finally {
305329
await seed.end();
306330
}
@@ -320,8 +344,44 @@ describeIfDb("0025_workbench_threads_unique_key dedupe", () => {
320344
`SELECT thread_id FROM "chat"."workbench_thread_messages" WHERE message_id = 'msg_dedupe'`,
321345
);
322346
expect(String(membership[0]?.["thread_id"])).toBe("thr_root_old");
347+
348+
const replies = await verify.unsafe(
349+
`SELECT id FROM "chat"."workbench_threads" ` +
350+
`WHERE tenant_id = 'tnt_dedupe' AND workbench_id = 'wb_dedupe' ` +
351+
`AND kind = 'reply' AND parent_message_id = 'msg_parent'`,
352+
);
353+
expect(replies.map((row) => String(row["id"]))).toEqual([
354+
"thr_reply_old",
355+
]);
356+
357+
const messageThreadId = await verify.unsafe(
358+
`SELECT thread_id FROM "chat"."workbench_messages" WHERE id = 'msg_in_dropped_reply'`,
359+
);
360+
expect(String(messageThreadId[0]?.["thread_id"])).toBe("thr_reply_old");
361+
362+
const childParentThreadId = await verify.unsafe(
363+
`SELECT parent_thread_id FROM "chat"."workbench_threads" WHERE id = 'thr_child_of_dropped_root'`,
364+
);
365+
expect(String(childParentThreadId[0]?.["parent_thread_id"])).toBe(
366+
"thr_root_old",
367+
);
368+
369+
// The reply unique index holds: a second reply row for the same
370+
// (tenant, workbench, parent_message_id) key is now rejected
371+
// rather than silently accepted as a second duplicate.
372+
// postgres.js queries are lazy thenables, not Promises; wrap so
373+
// `expect(...).rejects` actually runs the statement.
374+
await expect(
375+
(async () => {
376+
await verify.unsafe(
377+
`INSERT INTO "chat"."workbench_threads" ` +
378+
`(id, tenant_id, workbench_id, kind, parent_message_id) ` +
379+
`VALUES ('thr_reply_conflict', 'tnt_dedupe', 'wb_dedupe', 'reply', 'msg_parent')`,
380+
);
381+
})(),
382+
).rejects.toThrow();
323383
} finally {
324384
await verify.end();
325385
}
326-
});
386+
}, 120000);
327387
});

0 commit comments

Comments
 (0)