paperclip/ui/src/components/task-chat/task-chat-attachments.test.ts

207 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
extractAttachmentRefs,
extractImageRefs,
fileKindForAttachment,
fileKindForName,
formatFileSize,
hydrateAttachmentRefs,
isImageAttachment,
isImageFilename,
stripStandaloneImageEmbeds,
} from "./task-chat-attachments";
describe("fileKindForName", () => {
it("maps known extensions to kind labels", () => {
expect(fileKindForName("report.pdf").label).toBe("PDF");
expect(fileKindForName("data.csv").label).toBe("CSV");
expect(fileKindForName("bundle.zip").label).toBe("ZIP");
expect(fileKindForName("script.ts").label).toBe("Code");
});
it("falls back to File for unknown or missing extensions", () => {
expect(fileKindForName("mystery.xyz").label).toBe("File");
expect(fileKindForName("README").label).toBe("File");
});
});
describe("isImageFilename", () => {
it("detects image extensions case-insensitively", () => {
expect(isImageFilename("shot.PNG")).toBe(true);
expect(isImageFilename("photo.jpeg")).toBe(true);
expect(isImageFilename("notes.txt")).toBe(false);
});
});
describe("attachment record metadata", () => {
it("detects an extensionless image from its content type", () => {
expect(
isImageAttachment({
name: "desktop Default",
url: "/api/attachments/screenshot/content",
contentType: "image/png",
}),
).toBe(true);
});
it("falls back to the extension for generic attachment content", () => {
expect(
isImageAttachment({
name: "shot.png",
url: "/api/attachments/screenshot/content",
contentType: "application/octet-stream",
}),
).toBe(true);
});
it("uses content type for an extensionless file kind", () => {
expect(
fileKindForAttachment({
name: "report",
url: "/api/attachments/report/content",
contentType: "application/pdf",
}).label,
).toBe("PDF");
});
it("hydrates refs with the stored type, size, and paths", () => {
const [ref] = hydrateAttachmentRefs(
[{ name: "desktop Default", url: "/api/attachments/a1/content" }],
[
{
id: "a1",
contentPath: "/api/attachments/a1/content",
openPath: "/api/attachments/a1/content",
downloadPath: "/api/attachments/a1/content?download=1",
contentType: "image/png",
byteSize: 2048,
originalFilename: "desktop Default",
},
],
);
expect(ref).toMatchObject({
id: "a1",
contentType: "image/png",
byteSize: 2048,
});
});
});
describe("formatFileSize", () => {
it("formats byte tiers", () => {
expect(formatFileSize(512)).toBe("512 B");
expect(formatFileSize(2048)).toBe("2.0 KB");
expect(formatFileSize(3 * 1024 * 1024)).toBe("3.0 MB");
});
it("returns empty for missing or non-positive sizes", () => {
expect(formatFileSize(undefined)).toBe("");
expect(formatFileSize(0)).toBe("");
});
});
describe("extractAttachmentRefs", () => {
it("extracts attachment links and strips link-only lines", () => {
const body = "Here are the notes.\n\n[notes.txt](/api/attachments/abc/content) ";
const { refs, text } = extractAttachmentRefs(body);
expect(refs).toEqual([{ name: "notes.txt", url: "/api/attachments/abc/content" }]);
expect(text).toBe("Here are the notes.");
});
it("keeps links woven into prose while still chipping them", () => {
const body = "See [report.pdf](/api/attachments/r1/content) for details.";
const { refs, text } = extractAttachmentRefs(body);
expect(refs).toEqual([{ name: "report.pdf", url: "/api/attachments/r1/content" }]);
expect(text).toBe(body);
});
it("ignores image embeds but returns image-named attachment links for classification", () => {
const body =
"![shot.png](/api/attachments/img/content)\n[photo.jpg](/api/attachments/p/content)\n[notes.txt](/api/attachments/n/content)";
const { refs } = extractAttachmentRefs(body);
expect(refs).toEqual([
{ name: "photo.jpg", url: "/api/attachments/p/content" },
{ name: "notes.txt", url: "/api/attachments/n/content" },
]);
});
it("ignores non-attachment links entirely", () => {
const body = "[docs](https://example.com/content) and [PAP-1](/issues/PAP-1)";
const { refs, text } = extractAttachmentRefs(body);
expect(refs).toEqual([]);
expect(text).toBe(body);
});
it("dedupes repeated references to the same attachment", () => {
const body =
"[notes.txt](/api/attachments/n/content)\n[notes.txt](/api/attachments/n/content)";
const { refs, text } = extractAttachmentRefs(body);
expect(refs).toHaveLength(1);
expect(text).toBe("");
});
it("unescapes bracket-escaped labels and accepts asset content paths", () => {
const body = String.raw`[weird \[name\].txt](/api/assets/a1/content?download=1)`;
const { refs } = extractAttachmentRefs(body);
expect(refs).toEqual([
{ name: "weird [name].txt", url: "/api/assets/a1/content?download=1" },
]);
});
});
describe("extractImageRefs", () => {
it("collects image embeds in order, skipping plain links", () => {
const body =
"![a.png](/api/attachments/a/content)\nSee [notes.txt](/api/attachments/n/content)\n![b.jpg](https://example.com/b.jpg)";
expect(extractImageRefs(body)).toEqual([
{ name: "a.png", url: "/api/attachments/a/content" },
{ name: "b.jpg", url: "https://example.com/b.jpg" },
]);
});
it("dedupes repeated embeds and tolerates empty alt text", () => {
const body = "![](/api/attachments/a/content)\n![again](/api/attachments/a/content)";
expect(extractImageRefs(body)).toEqual([{ name: "", url: "/api/attachments/a/content" }]);
});
it("unescapes bracket-escaped alt text", () => {
const body = String.raw`![shot \[1\].png](/api/attachments/s/content)`;
expect(extractImageRefs(body)).toEqual([
{ name: "shot [1].png", url: "/api/attachments/s/content" },
]);
});
it("returns nothing for bodies without images", () => {
expect(extractImageRefs("just text and a [link](/api/attachments/x/content)")).toEqual([]);
});
it("promotes standalone embeds but leaves prose-woven embeds inline", () => {
const body = [
"Standalone embed on its own line:",
"",
"![shot one](/api/attachments/a/content)",
"",
"And here is an image ![inline two](/api/attachments/b/content) woven into this sentence.",
].join("\n");
expect(extractImageRefs(body)).toEqual([
{ name: "shot one", url: "/api/attachments/a/content" },
]);
expect(stripStandaloneImageEmbeds(body)).toBe(
[
"Standalone embed on its own line:",
"",
"And here is an image ![inline two](/api/attachments/b/content) woven into this sentence.",
].join("\n"),
);
});
});
describe("stripStandaloneImageEmbeds", () => {
it("removes tail embeds promoted to thumbnails but keeps prose", () => {
expect(
stripStandaloneImageEmbeds("Done.\n\n![desktop Default](/api/attachments/a/content)"),
).toBe("Done.");
});
});