177 lines · 5.7 KB · TypeScript
RawDownload
import { describe, expect, it } from "vite-plus/test";import { MessageId } from "@t3tools/contracts";import * as Cause from "effect/Cause";import { AsyncResult } from "effect/unstable/reactivity"; import {  beginCodexFeedbackSubmission,  codexFeedbackMessage,  parseCodexFeedbackCommand,  submitCodexFeedback,  type CodexFeedbackSubmission,} from "./threadFeedback.ts"; describe("beginCodexFeedbackSubmission", () => {  it("allows only one upload per thread until the active upload releases its guard", () => {    const inFlight = new Set<string>();    const finish = beginCodexFeedbackSubmission(inFlight, "environment:thread");     expect(finish).not.toBeNull();    expect(beginCodexFeedbackSubmission(inFlight, "environment:thread")).toBeNull();    expect(beginCodexFeedbackSubmission(inFlight, "environment:other-thread")).not.toBeNull();     finish?.();    expect(beginCodexFeedbackSubmission(inFlight, "environment:thread")).not.toBeNull();  });}); describe("parseCodexFeedbackCommand", () => {  it("accepts feedback without a reason", () => {    expect(parseCodexFeedbackCommand(" /feedback ")).toEqual({});  });   it("preserves a feedback description", () => {    expect(parseCodexFeedbackCommand("/feedback The agent stopped early.")).toEqual({      reason: "The agent stopped early.",    });  });   it("accepts mixed-case feedback commands", () => {    expect(parseCodexFeedbackCommand("/Feedback Retry failed.")).toEqual({      reason: "Retry failed.",    });  });   it("ignores other slash commands and ordinary messages", () => {    expect(parseCodexFeedbackCommand("/feedback-status")).toBeNull();    expect(parseCodexFeedbackCommand("Please send /feedback")).toBeNull();  });}); describe("submitCodexFeedback", () => {  const submission = {    id: MessageId.make("feedback-message-1"),    command: "/feedback The agent stopped early.",    createdAt: "2026-08-23T00:00:00.000Z",  } as const;   it("shows the command and clears the draft before the upload finishes", async () => {    let draft: string = submission.command;    let finishUpload:      | ((result: ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>) => void)      | undefined;    const states: CodexFeedbackSubmission[] = [];    const upload = new Promise<ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>>(      (resolve) => {        finishUpload = resolve;      },    );     const result = submitCodexFeedback({      submission,      clearDraft: () => {        draft = "";      },      onUpdate: (state) => states.push(state),      upload: () => {        expect(draft).toBe("");        return upload;      },    });     expect(draft).toBe("");    expect(states).toEqual([{ ...submission, status: "uploading" }]);    expect(codexFeedbackMessage(states[0]!)).toMatchObject({      id: submission.id,      role: "user",      text: submission.command,    });    expect(codexFeedbackMessage(states[0]!, "assistant").text).toBe(      "Sending feedback to OpenAI...",    );     draft = "Keep this newer message.";    finishUpload?.(AsyncResult.success({ feedbackId: "codex-thread-1" }));    await result;     expect(draft).toBe("Keep this newer message.");    expect(states.at(-1)).toEqual({      ...submission,      status: "sent",      feedbackId: "codex-thread-1",    });    expect(codexFeedbackMessage(states.at(-1)!, "assistant").text).toContain("codex-thread-1");  });   it("records a failed upload without losing its user-facing error", async () => {    const states: CodexFeedbackSubmission[] = [];    const error = new Error("Upload rejected.");     await submitCodexFeedback({      submission,      clearDraft: () => undefined,      onUpdate: (state) => states.push(state),      upload: () =>        Promise.resolve(AsyncResult.failure<{ feedbackId: string }, Error>(Cause.fail(error))),    });     expect(states.at(-1)).toEqual({      ...submission,      status: "failed",      errorMessage: "Upload rejected.",    });  });   it("marks interruptions without reporting them as upload failures", async () => {    const states: CodexFeedbackSubmission[] = [];     await submitCodexFeedback({      submission,      clearDraft: () => undefined,      onUpdate: (state) => states.push(state),      upload: () =>        Promise.resolve(AsyncResult.failure<{ feedbackId: string }, never>(Cause.interrupt(1))),    });     expect(states.at(-1)).toEqual({ ...submission, status: "interrupted" });  });   it("lets another feedback submission finish while the first remains in flight", async () => {    let finishFirstUpload:      | ((result: ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>) => void)      | undefined;    const firstUpload = new Promise<ReturnType<typeof AsyncResult.success<{ feedbackId: string }>>>(      (resolve) => {        finishFirstUpload = resolve;      },    );    const firstStates: CodexFeedbackSubmission[] = [];    const secondStates: CodexFeedbackSubmission[] = [];     const first = submitCodexFeedback({      submission,      clearDraft: () => undefined,      onUpdate: (state) => firstStates.push(state),      upload: () => firstUpload,    });    const second = await submitCodexFeedback({      submission: {        ...submission,        id: MessageId.make("feedback-message-2"),      },      clearDraft: () => undefined,      onUpdate: (state) => secondStates.push(state),      upload: () => Promise.resolve(AsyncResult.success({ feedbackId: "codex-thread-2" })),    });     expect(firstStates.at(-1)?.status).toBe("uploading");    expect(second._tag).toBe("Success");    expect(secondStates.at(-1)).toMatchObject({      status: "sent",      feedbackId: "codex-thread-2",    });     finishFirstUpload?.(AsyncResult.success({ feedbackId: "codex-thread-1" }));    await first;  });});