1import { describe, expect, it } from "vite-plus/test";
2import {
3 ChatAttachmentId,
4 ComposerContextId,
5 MessageId,
6 RunId,
7 type ChatAttachment,
8 type OrchestrationMessageContext,
9} from "@t3tools/contracts";
10
11import { composerDraftEnvironmentId } from "../lib/composerAttachmentUploadQueue";
12import { isQueuedEditDraftKey, queuedEditDraftKey } from "./queued-edit-draft-key";
13import { resolveQueuedEditPayload, type QueuedRunEdit } from "./queued-run-edit";
14
15const image = (id: string): ChatAttachment => ({
16 type: "image",
17 id: ChatAttachmentId.make(id),
18 name: `${id}.png`,
19 mimeType: "image/png",
20 sizeBytes: 10,
21});
22
23function edit(attachments: ReadonlyArray<ChatAttachment>): QueuedRunEdit {
24 return {
25 runId: RunId.make("run-1"),
26 messageId: MessageId.make("message-1"),
27 originalText: "original",
28 existingAttachments: attachments,
29 };
30}
31
32function context(attachmentIds: ReadonlyArray<string>): OrchestrationMessageContext {
33 return {
34 version: 1,
35 records: attachmentIds.map((attachmentId, index) => ({
36 version: 1,
37 kind: "image",
38 contextId: ComposerContextId.make(`ctx-${index}`),
39 label: `${attachmentId}.png`,
40 attachmentId,
41 name: `${attachmentId}.png`,
42 mimeType: "image/png",
43 sizeBytes: 10,
44 })),
45 };
46}
47
48describe("queued edit draft key", () => {
49 it("keeps the environment readable so attachment uploads still resolve it", () => {
50 const key = queuedEditDraftKey("env-1:thread-9", "run-3");
51
52 expect(isQueuedEditDraftKey(key)).toBe(true);
53 expect(isQueuedEditDraftKey("env-1:thread-9")).toBe(false);
54 expect(composerDraftEnvironmentId(key, [])).toBe("env-1");
55 });
56});
57
58describe("resolveQueuedEditPayload", () => {
59 it("resends the kept server attachments ahead of the newly uploaded ones", () => {
60 const payload = resolveQueuedEditPayload({
61 edit: edit([image("kept")]),
62 draftContext: undefined,
63 draftAttachments: [{ id: "draft-1" }],
64 uploaded: [image("uploaded")],
65 });
66
67 expect(payload.attachments.map((attachment) => attachment.id)).toEqual(["kept", "uploaded"]);
68 });
69
70 it("rebinds context records from the draft's ids to the uploaded ones", () => {
71 const payload = resolveQueuedEditPayload({
72 edit: edit([]),
73 draftContext: context(["draft-1"]),
74 draftAttachments: [{ id: "draft-1" }],
75 uploaded: [image("uploaded")],
76 });
77
78 expect(payload.context?.records).toEqual([
79 expect.objectContaining({ attachmentId: "uploaded" }),
80 ]);
81 });
82
83 it("drops the record of an attachment the user removed", () => {
84 const payload = resolveQueuedEditPayload({
85 // "gone" was on the message and has been removed from the kept list.
86 edit: edit([image("kept")]),
87 draftContext: context(["kept", "gone"]),
88 draftAttachments: [],
89 uploaded: [],
90 });
91
92 expect(payload.attachments.map((attachment) => attachment.id)).toEqual(["kept"]);
93 expect(payload.context?.records).toEqual([expect.objectContaining({ attachmentId: "kept" })]);
94 });
95
96 it("returns no context when every record lost its attachment", () => {
97 const payload = resolveQueuedEditPayload({
98 edit: edit([]),
99 draftContext: context(["gone"]),
100 draftAttachments: [],
101 uploaded: [],
102 });
103
104 expect(payload.attachments).toEqual([]);
105 expect(payload.context).toBeUndefined();
106 });
107});