Skip to content
Source
Sign inSign up

threadFeedback.test.ts

136 lines4.4 KB

1import { describe, expect, it } from "vite-plus/test";
2import { MessageId } from "@t3tools/contracts";
3import * as Cause from "effect/Cause";
4import { AsyncResult } from "effect/reactivity";
5​
6import {
7 codexFeedbackNotice,
8 submitCodexFeedback,
9 type CodexFeedbackSubmission,
10} from "./threadFeedback.ts";
11​
12describe("submitCodexFeedback", () => {
13 const submission = {
14 id: MessageId.make("feedback-message-1"),
15 command: "/feedback The agent stopped early.",
16 createdAt: "2026-08-23T00:00:00.000Z",
17 } as const;
18​
19 it("reports upload progress and clears the draft before the upload finishes", async () => {
20 let draft: string = submission.command;
21 let finishUpload:
22 | ((result: ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>) => void)
23 | undefined;
24 const states: CodexFeedbackSubmission[] = [];
25 const upload = new Promise<ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>>(
26 (resolve) => {
27 finishUpload = resolve;
28 },
29 );
30​
31 const result = submitCodexFeedback({
32 submission,
33 clearDraft: () => {
34 draft = "";
35 },
36 onUpdate: (state) => states.push(state),
37 upload: () => {
38 expect(draft).toBe("");
39 return upload;
40 },
41 });
42​
43 expect(draft).toBe("");
44 expect(states).toEqual([{ ...submission, status: "uploading" }]);
45 expect(codexFeedbackNotice(states[0]!)).toEqual({
46 title: "Sending feedback to OpenAI...",
47 description: undefined,
48 });
49​
50 draft = "Keep this newer message.";
51 finishUpload?.(AsyncResult.success({ feedbackId: "codex-thread-1" }));
52 await result;
53​
54 expect(draft).toBe("Keep this newer message.");
55 expect(states.at(-1)).toEqual({
56 ...submission,
57 status: "sent",
58 feedbackId: "codex-thread-1",
59 });
60 expect(codexFeedbackNotice(states.at(-1)!)?.description).toContain("codex-thread-1");
61 });
62​
63 it("records a failed upload without losing its user-facing error", async () => {
64 const states: CodexFeedbackSubmission[] = [];
65 const error = new Error("Upload rejected.");
66​
67 await submitCodexFeedback({
68 submission,
69 clearDraft: () => undefined,
70 onUpdate: (state) => states.push(state),
71 upload: () =>
72 Promise.resolve(AsyncResult.failure<{ feedbackId: string }, Error>(Cause.fail(error))),
73 });
74​
75 expect(states.at(-1)).toEqual({
76 ...submission,
77 status: "failed",
78 errorMessage: "Upload rejected.",
79 });
80 expect(codexFeedbackNotice(states.at(-1)!)?.description).toBe("Upload rejected.");
81 });
82​
83 it("marks interruptions without reporting them as upload failures", async () => {
84 const states: CodexFeedbackSubmission[] = [];
85​
86 await submitCodexFeedback({
87 submission,
88 clearDraft: () => undefined,
89 onUpdate: (state) => states.push(state),
90 upload: () =>
91 Promise.resolve(AsyncResult.failure<{ feedbackId: string }, never>(Cause.interrupt(1))),
92 });
93​
94 expect(states.at(-1)).toEqual({ ...submission, status: "interrupted" });
95 expect(codexFeedbackNotice(states.at(-1)!)).toBeNull();
96 });
97​
98 it("lets another feedback submission finish while the first remains in flight", async () => {
99 let finishFirstUpload:
100 | ((result: ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>) => void)
101 | undefined;
102 const firstUpload = new Promise<ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>>(
103 (resolve) => {
104 finishFirstUpload = resolve;
105 },
106 );
107 const firstStates: CodexFeedbackSubmission[] = [];
108 const secondStates: CodexFeedbackSubmission[] = [];
109​
110 const first = submitCodexFeedback({
111 submission,
112 clearDraft: () => undefined,
113 onUpdate: (state) => firstStates.push(state),
114 upload: () => firstUpload,
115 });
116 const second = await submitCodexFeedback({
117 submission: {
118 ...submission,
119 id: MessageId.make("feedback-message-2"),
120 },
121 clearDraft: () => undefined,
122 onUpdate: (state) => secondStates.push(state),
123 upload: () => Promise.resolve(AsyncResult.success({ feedbackId: "codex-thread-2" })),
124 });
125​
126 expect(firstStates.at(-1)?.status).toBe("uploading");
127 expect(second._tag).toBe("Success");
128 expect(secondStates.at(-1)).toMatchObject({
129 status: "sent",
130 feedbackId: "codex-thread-2",
131 });
132​
133 finishFirstUpload?.(AsyncResult.success({ feedbackId: "codex-thread-1" }));
134 await first;
135 });
136});