840 lines · 26.9 KB · TypeScript
RawDownload
import { EnvironmentId } from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import {  composerFileNeedsReattach,  DraftId,  useComposerDraftStore,  type ComposerFileAttachment,  type ComposerImageAttachment,} from "../composerDraftStore"; const mocks = vi.hoisted(() => ({  createAssetUrl: vi.fn(),  createUploadUrl: Symbol("create-upload-url"),  executeAtomQuery: vi.fn(),  removeUpload: Symbol("remove-upload"),  runAtomCommand: vi.fn(),  readPreparedConnection: vi.fn(),})); vi.mock("@t3tools/client-runtime/state/runtime", () => ({  executeAtomQuery: mocks.executeAtomQuery,  runAtomCommand: mocks.runAtomCommand,  squashAtomCommandFailure: (result: { readonly error: unknown }) => result.error,})); vi.mock("../rpc/atomRegistry", () => ({ appAtomRegistry: {} })); vi.mock("../state/assets", () => ({  assetEnvironment: { createUrl: mocks.createAssetUrl },})); vi.mock("../state/attachments", () => ({  attachmentEnvironment: {    createUploadUrl: mocks.createUploadUrl,    remove: mocks.removeUpload,  },})); vi.mock("../state/session", () => ({  readPreparedConnection: mocks.readPreparedConnection,})); import {  awaitAttachmentUploads,  getUploadedAttachments,  readAttachmentUpload,  releaseAttachmentUpload,  releaseDraftAttachment,  releaseDraftAttachments,  releasePersistedAttachmentUpload,  retryAttachmentUpload,  startAttachmentUpload,  useAttachmentUploadStore,} from "./attachmentUploadQueue"; type ProgressListener = (event: {  readonly lengthComputable: boolean;  readonly loaded: number;  readonly total: number;}) => void; class TestXmlHttpRequest {  static requests: TestXmlHttpRequest[] = [];   status = 0;  timeout = 0;  method: string | null = null;  url: string | null = null;  readonly headers = new Map<string, string>();  readonly listeners = new Map<string, () => void>();  progressListener: ProgressListener | null = null;   readonly upload = {    addEventListener: (_event: string, listener: ProgressListener) => {      this.progressListener = listener;    },  };   constructor() {    TestXmlHttpRequest.requests.push(this);  }   open(method: string, url: string): void {    this.method = method;    this.url = url;  }   setRequestHeader(name: string, value: string): void {    this.headers.set(name, value);  }   addEventListener(event: string, listener: () => void): void {    this.listeners.set(event, listener);  }   send(): void {}   abort(): void {    this.listeners.get("abort")?.();  }   progress(loaded: number, total: number): void {    this.progressListener?.({ lengthComputable: true, loaded, total });  }   complete(status = 204): void {    this.status = status;    this.listeners.get("load")?.();  }} const firstEnvironment = EnvironmentId.make("environment-1");const secondEnvironment = EnvironmentId.make("environment-2"); function makeImage(id: string): ComposerImageAttachment {  const file = new File([new Uint8Array([1, 2, 3])], `${id}.png`, { type: "image/png" });  return {    type: "image",    id,    name: file.name,    mimeType: file.type,    sizeBytes: file.size,    previewUrl: `blob:${id}`,    file,  };} function makeFile(id: string): ComposerFileAttachment {  const file = new File([new Uint8Array([1, 2, 3])], `${id}.pdf`, {    type: "application/pdf",  });  return {    type: "file",    id,    name: file.name,    mimeType: file.type,    sizeBytes: file.size,    file,  };} describe("attachmentUploadQueue", () => {  beforeEach(() => {    TestXmlHttpRequest.requests = [];    mocks.createAssetUrl.mockReset();    mocks.createAssetUrl.mockImplementation((target: unknown) => target);    mocks.executeAtomQuery.mockReset();    mocks.executeAtomQuery.mockResolvedValue({ _tag: "Success", value: {} });    mocks.runAtomCommand.mockReset();    mocks.readPreparedConnection.mockReset();    mocks.readPreparedConnection.mockReturnValue({ httpBaseUrl: "https://environment.test/" });    mocks.runAtomCommand.mockImplementation(      async (        _registry: unknown,        command: unknown,        target: {          readonly environmentId: EnvironmentId;          readonly input: { readonly name?: string };        },      ) => {        if (command === mocks.createUploadUrl) {          const attachmentId = `pending-${target.environmentId}-${target.input.name}`;          return {            _tag: "Success",            value: {              attachmentId,              relativeUrl: `/api/attachments/upload/${attachmentId}`,              expiresAt: 1,            },          };        }        return { _tag: "Success", value: undefined };      },    );    vi.stubGlobal("XMLHttpRequest", TestXmlHttpRequest);  });   afterEach(() => {    for (const imageId of Object.keys(useAttachmentUploadStore.getState().uploadsByImageId)) {      releaseAttachmentUpload(imageId);    }    vi.unstubAllGlobals();  });   it("uploads images immediately and sends attachment references", async () => {    const image = {      ...makeImage("image-1"),      source: {        kind: "snap-shot" as const,        capturedAt: "2026-08-24T11:00:00.000Z",        appName: "Terminal",        windowTitle: "Tests",      },    };    startAttachmentUpload({ environmentId: firstEnvironment, image });    await Promise.resolve();     const request = TestXmlHttpRequest.requests[0]!;    expect(request.method).toBe("POST");    expect(request.url).toBe(      "https://environment.test/api/attachments/upload/pending-environment-1-image-1.png",    );    request.progress(1, 3);    expect(readAttachmentUpload(image.id)).toMatchObject({ status: "uploading", progress: 1 / 3 });     const settled = awaitAttachmentUploads([image.id]);    request.complete();    await settled;     expect(getUploadedAttachments({ environmentId: firstEnvironment, images: [image] })).toEqual([      {        type: "image",        id: "pending-environment-1-image-1.png",        name: "image-1.png",        mimeType: "image/png",        sizeBytes: 3,        source: {          kind: "snap-shot",          capturedAt: "2026-08-24T11:00:00.000Z",          appName: "Terminal",          windowTitle: "Tests",        },      },    ]);     releaseDraftAttachments([image]);    expect(readAttachmentUpload(image.id)).toBeUndefined();    expect(mocks.runAtomCommand).toHaveBeenCalledWith(      expect.anything(),      mocks.removeUpload,      {        environmentId: firstEnvironment,        input: { attachmentId: "pending-environment-1-image-1.png" },      },      expect.anything(),    );  });   it("uploads generic files and sends file attachment references", async () => {    const file = makeFile("report");    startAttachmentUpload({ environmentId: firstEnvironment, image: file });    await Promise.resolve();     expect(mocks.runAtomCommand).toHaveBeenCalledWith(      expect.anything(),      mocks.createUploadUrl,      {        environmentId: firstEnvironment,        input: {          type: "file",          name: "report.pdf",          mimeType: "application/pdf",          sizeBytes: 3,        },      },      expect.anything(),    );     const settled = awaitAttachmentUploads([file.id]);    TestXmlHttpRequest.requests[0]!.complete();    await settled;     expect(getUploadedAttachments({ environmentId: firstEnvironment, images: [file] })).toEqual([      {        type: "file",        id: "pending-environment-1-report.pdf",        name: "report.pdf",        mimeType: "application/pdf",        sizeBytes: 3,      },    ]);  });   it("uses the fallback MIME type for both the upload claim and request header", async () => {    const bytes = new File([new Uint8Array([1, 2, 3])], "unknown.bin");    const file: ComposerFileAttachment = {      type: "file",      id: "file-without-browser-mime",      name: bytes.name,      mimeType: "application/octet-stream",      sizeBytes: bytes.size,      file: bytes,    };     startAttachmentUpload({ environmentId: firstEnvironment, image: file });    await Promise.resolve();     expect(mocks.runAtomCommand).toHaveBeenCalledWith(      expect.anything(),      mocks.createUploadUrl,      {        environmentId: firstEnvironment,        input: {          type: "file",          name: "unknown.bin",          mimeType: "application/octet-stream",          sizeBytes: 3,        },      },      expect.anything(),    );    expect(TestXmlHttpRequest.requests[0]?.headers.get("Content-Type")).toBe(      "application/octet-stream",    );     const settled = awaitAttachmentUploads([file.id]);    TestXmlHttpRequest.requests[0]!.complete();    await settled;  });   it("persists a background completion's ids to the draft with no composer mounted", async () => {    const draftId = DraftId.make("draft-background-upload");    const file = makeFile("background");    const draftStore = useComposerDraftStore.getState();    draftStore.addFiles(draftId, [file]);    try {      startAttachmentUpload({        environmentId: firstEnvironment,        image: file,        draftTarget: draftId,      });      await Promise.resolve();       // No composer effect is subscribed; only the queue can stamp the draft.      const settled = awaitAttachmentUploads([file.id]);      TestXmlHttpRequest.requests[0]!.complete();      await settled;       expect(useComposerDraftStore.getState().getComposerDraft(draftId)?.files).toMatchObject([        {          id: file.id,          uploadedAttachmentId: "pending-environment-1-background.pdf",          uploadEnvironmentId: firstEnvironment,        },      ]);    } finally {      useComposerDraftStore.getState().clearComposerContent(draftId);    }  });   it("persists a retried question upload without a mounted composer", async () => {    const draftId = DraftId.make("question-retry-upload");    const file = makeFile("question-retry");    const store = useComposerDraftStore.getState();    store.addFiles(draftId, [file]);    try {      startAttachmentUpload({ environmentId: firstEnvironment, image: file, draftTarget: draftId });      await Promise.resolve();      let settled = awaitAttachmentUploads([file.id]);      TestXmlHttpRequest.requests[0]!.complete(500);      await settled;      expect(store.getComposerDraft(draftId)?.files[0]?.uploadedAttachmentId).toBeUndefined();      retryAttachmentUpload({ environmentId: firstEnvironment, image: file, draftTarget: draftId });      await Promise.resolve();      settled = awaitAttachmentUploads([file.id]);      TestXmlHttpRequest.requests[1]!.complete();      await settled;      expect(store.getComposerDraft(draftId)?.files[0]).toMatchObject({        uploadedAttachmentId: "pending-environment-1-question-retry.pdf",        uploadEnvironmentId: firstEnvironment,      });    } finally {      store.clearComposerContent(draftId);    }  });   it("verifies an uploaded file reference before restoring it", async () => {    const file: ComposerFileAttachment = {      ...makeFile("restored"),      file: null,      uploadedAttachmentId: "pending-restored-pdf",      uploadEnvironmentId: firstEnvironment,    };     startAttachmentUpload({ environmentId: firstEnvironment, image: file });    await awaitAttachmentUploads([file.id]);     expect(readAttachmentUpload(file.id)).toEqual({      status: "ready",      environmentId: firstEnvironment,      attachmentId: "pending-restored-pdf",    });    expect(mocks.createAssetUrl).toHaveBeenCalledWith({      environmentId: firstEnvironment,      input: { resource: { _tag: "attachment", attachmentId: "pending-restored-pdf" } },    });    expect(TestXmlHttpRequest.requests).toHaveLength(0);  });   it("turns an expired persisted file into a marker that a re-pick can replace", async () => {    const draftId = DraftId.make("draft-expired-upload");    const file: ComposerFileAttachment = {      ...makeFile("expired"),      file: null,      uploadedAttachmentId: "pending-expired-pdf",      uploadEnvironmentId: firstEnvironment,    };    mocks.executeAtomQuery.mockResolvedValueOnce({      _tag: "Failure",      error: { _tag: "AssetAttachmentNotFoundError" },    });    useComposerDraftStore.getState().addFiles(draftId, [file]);     try {      startAttachmentUpload({        environmentId: firstEnvironment,        image: file,        draftTarget: draftId,      });      await awaitAttachmentUploads([file.id]);       const marker = useComposerDraftStore.getState().getComposerDraft(draftId)?.files[0];      expect(readAttachmentUpload(file.id)).toBeUndefined();      expect(marker?.uploadedAttachmentId).toBeUndefined();      expect(marker?.uploadEnvironmentId).toBeUndefined();      expect(marker && composerFileNeedsReattach(marker)).toBe(true);       const replacementBytes = new File([new Uint8Array([1, 2, 3])], file.name, {        type: file.mimeType,      });      const replacement: ComposerFileAttachment = {        type: "file",        id: "expired-repicked",        name: file.name,        mimeType: file.mimeType,        sizeBytes: file.sizeBytes,        file: replacementBytes,      };      useComposerDraftStore.getState().addFiles(draftId, [replacement]);      expect(useComposerDraftStore.getState().getComposerDraft(draftId)?.files).toMatchObject([        { id: replacement.id, file: replacementBytes },      ]);       startAttachmentUpload({        environmentId: firstEnvironment,        image: replacement,        draftTarget: draftId,      });      await Promise.resolve();      const settled = awaitAttachmentUploads([replacement.id]);      TestXmlHttpRequest.requests[0]!.complete();      await settled;       expect(useComposerDraftStore.getState().getComposerDraft(draftId)?.files).toMatchObject([        {          id: replacement.id,          uploadedAttachmentId: "pending-environment-1-expired.pdf",          uploadEnvironmentId: firstEnvironment,        },      ]);      const removeCalls = mocks.runAtomCommand.mock.calls.filter(        ([, command]) => command === mocks.removeUpload,      );      expect(removeCalls).toEqual([]);    } finally {      useComposerDraftStore.getState().clearComposerContent(draftId);    }  });   it("uploads the original file again when its persisted server upload expired", async () => {    const file: ComposerFileAttachment = {      ...makeFile("recoverable"),      uploadedAttachmentId: "pending-expired-pdf",      uploadEnvironmentId: firstEnvironment,    };    mocks.executeAtomQuery.mockResolvedValueOnce({      _tag: "Failure",      error: { _tag: "AssetAttachmentNotFoundError" },    });     startAttachmentUpload({ environmentId: firstEnvironment, image: file });    // The verify-then-reupload path crosses several awaits before the    // transfer starts; drain microtasks until the XHR exists.    for (let hop = 0; hop < 20 && TestXmlHttpRequest.requests.length === 0; hop += 1) {      await Promise.resolve();    }     const settled = awaitAttachmentUploads([file.id]);    TestXmlHttpRequest.requests[0]!.complete();    await settled;     expect(readAttachmentUpload(file.id)).toMatchObject({      status: "ready",      attachmentId: "pending-environment-1-recoverable.pdf",    });  });   it("removes a persisted upload when its draft is discarded during verification", async () => {    const file: ComposerFileAttachment = {      ...makeFile("checking"),      file: null,      uploadedAttachmentId: "pending-checking-pdf",      uploadEnvironmentId: firstEnvironment,    };    let resolveVerification: (result: {      readonly _tag: "Success";      readonly value: object;    }) => void = () => {};    mocks.executeAtomQuery.mockReturnValueOnce(

Lines 1–500 of 840.