mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-04 17:18:35 +02:00
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>
98 lines
3.6 KiB
TypeScript
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([]);
|
|
});
|
|
});
|