Files
multica/packages/core/drafts/draft-upload.test.ts
Naiyuan Qing 6c9a59cc14 refactor(uploads): widen handleUpload contract, fix interrupted doc drift (#6035)
Follow-up to #6025 (MUL-5391), addressing the two non-blocking cleanups
raised in review.

1. `CoordinatedUploads.handleUpload` was still typed as a one-arg
   function while the real `ContentEditor` contract is `(file, uploadId)`.
   Runtime was already safe (the implementation accepts `uploadId?`), but
   the exported interface erased the second parameter at the boundary, so
   a mock or hand-rolled caller could silently drop the editor-minted id
   and mint a second one — breaking the one-id link between the document
   node and the draft record. Widened the type and documented why the id
   must be threaded through.

2. #6025 changed `normalizeStoredUploads` to DROP persisted `uploading`
   records instead of coercing them to `interrupted`, but eleven comments
   across core and views still described the old coercion. Corrected them
   to state what the code does. `interrupted` is now produced by no code
   path at all; it stays in the union and is still accepted, rendered and
   dismissable because builds before this change persisted such records.
   Marked it LEGACY at the type and in the test that pins the behaviour.

No runtime behaviour change: comments, one type widening, one test comment.

Verified: pnpm typecheck (6/6); packages/core Vitest 1133 tests;
packages/views Vitest 3178 tests; git diff --check.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 15:17:40 +08:00

98 lines
3.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Attachment } from "../types";
import {
type DraftUpload,
attachmentToDraftUpload,
hasUploadingDraft,
normalizeStoredUploads,
uploadedAttachments,
} from "./draft-upload";
function makeAttachment(id: string): Attachment {
return {
id,
workspace_id: "ws-1",
issue_id: "issue-1",
comment_id: null,
chat_session_id: null,
chat_message_id: null,
uploader_type: "member",
uploader_id: "alice",
filename: `${id}.png`,
url: `https://cdn.example.test/${id}.png`,
download_url: `https://cdn.example.test/${id}.png`,
markdown_url: `https://app.example.test/api/attachments/${id}/download`,
content_type: "image/png",
size_bytes: 10,
created_at: "2026-06-12T00:00:00Z",
};
}
const pending = (id: string): DraftUpload => ({
clientUploadId: id,
status: "uploading",
filename: `${id}.png`,
size: 5,
contentType: "image/png",
});
describe("draft-upload helpers", () => {
it("uploadedAttachments returns only completed rows, in order", () => {
const uploads: DraftUpload[] = [
pending("a"),
attachmentToDraftUpload(makeAttachment("att-1")),
{ clientUploadId: "c", status: "failed", filename: "x", size: 1 },
attachmentToDraftUpload(makeAttachment("att-2")),
];
expect(uploadedAttachments(uploads).map((a) => a.id)).toEqual(["att-1", "att-2"]);
});
it("hasUploadingDraft is true only while a placeholder is in flight", () => {
expect(hasUploadingDraft([pending("a")])).toBe(true);
expect(
hasUploadingDraft([
attachmentToDraftUpload(makeAttachment("att-1")),
{ clientUploadId: "c", status: "failed", filename: "x", size: 1 },
]),
).toBe(false);
});
it("wraps a bare persisted Attachment as an uploaded placeholder (pre-L2 migration)", () => {
const normalized = normalizeStoredUploads([makeAttachment("att-1")]);
expect(normalized).toHaveLength(1);
expect(normalized[0]?.status).toBe("uploaded");
expect(uploadedAttachments(normalized).map((a) => a.id)).toEqual(["att-1"]);
});
it("drops an `uploading` placeholder on load", () => {
// The bytes were never persisted, so this upload can neither resume nor
// be retried, and no surface can act on it: a placeholder is never
// serialised, so the document has no node for it either. Keeping the
// record only held an otherwise-empty draft alive for the full TTL. The
// attachment's absence from the body is what tells the user to re-attach.
expect(normalizeStoredUploads([pending("a")])).toEqual([]);
});
// `interrupted` is legacy: no code path produces it any more (builds before
// MUL-5391 coerced a reload-surviving `uploading` record into it). It stays
// accepted here so a blob one of those builds persisted still renders.
it("keeps failed/interrupted/uploaded placeholders across load", () => {
const stored: DraftUpload[] = [
{ clientUploadId: "f", status: "failed", filename: "f", size: 1 },
{ clientUploadId: "i", status: "interrupted", filename: "i", size: 1 },
attachmentToDraftUpload(makeAttachment("att-1")),
];
const normalized = normalizeStoredUploads(stored);
expect(normalized.map((u) => u.status)).toEqual(["failed", "interrupted", "uploaded"]);
});
it("drops junk entries and non-arrays", () => {
expect(normalizeStoredUploads(undefined)).toEqual([]);
expect(normalizeStoredUploads([null, 42, { foo: "bar" }])).toEqual([]);
// An `uploaded` entry with a broken attachment is not trusted.
expect(
normalizeStoredUploads([{ clientUploadId: "x", status: "uploaded", filename: "x", size: 1 }]),
).toEqual([]);
});
});