import { describe, expect, it, vi } from "@effect/vitest";import { PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,} from "@t3tools/contracts";import type { ResolvedSharePayload, SharePayload } from "expo-sharing"; import { buildIncomingShareDraft, hasIncomingShareContent } from "./incoming-share-model"; describe("incoming native shares", () => { it("converts shared text, URLs, and images into a durable composer draft", async () => { const image: SharePayload = { shareType: "image", value: "file:///shared/Screenshot.png", mimeType: "image/png", }; const payloads: SharePayload[] = [ { shareType: "text", value: "Please explain this error" }, { shareType: "url", value: "https://example.com/issue/1" }, { shareType: "text", value: "Please explain this error" }, image, ]; const resolvedImage: ResolvedSharePayload = { ...image, contentUri: image.value, contentType: "image", contentMimeType: "image/png", contentSize: 3, originalName: "Screenshot.png", }; const removeOwnedFile = vi.fn(() => Promise.resolve()); const result = await buildIncomingShareDraft({ id: "share-1", createdAt: "2026-07-15T10:00:00.000Z", payloads, resolvedPayloads: [resolvedImage], fileReader: { readBase64: async () => "YWJj", removeOwnedFile, }, }); expect(result).toEqual({ schemaVersion: 1, id: "share-1", createdAt: "2026-07-15T10:00:00.000Z", text: "Please explain this error\n\nhttps://example.com/issue/1", attachments: [ { id: "share-1:image:3", type: "image", name: "Screenshot.png", mimeType: "image/png", sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj", previewUri: "data:image/png;base64,YWJj", }, ], warnings: [], }); expect(removeOwnedFile).toHaveBeenCalledWith(image.value); expect(hasIncomingShareContent(result)).toBe(true); }); it("skips oversized images and releases the temporary native file", async () => { const image: SharePayload = { shareType: "image", value: "file:///shared/huge.png", mimeType: "image/png", }; const readBase64 = vi.fn(async () => "unused"); const removeOwnedFile = vi.fn(() => Promise.resolve()); const result = await buildIncomingShareDraft({ id: "share-2", createdAt: "2026-07-15T10:00:00.000Z", payloads: [image], resolvedPayloads: [ { ...image, contentUri: image.value, contentType: "image", contentMimeType: "image/png", contentSize: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1, originalName: "huge.png", }, ], fileReader: { readBase64, removeOwnedFile }, }); expect(result.attachments).toEqual([]); expect(result.warnings).toEqual(["'huge.png' exceeds the 10 MB attachment limit."]); expect(readBase64).not.toHaveBeenCalled(); expect(removeOwnedFile).toHaveBeenCalledWith(image.value); expect(hasIncomingShareContent(result)).toBe(false); }); it("releases every temporary file when a share exceeds the attachment limit", async () => { const payloads = Array.from({ length: PROVIDER_SEND_TURN_MAX_ATTACHMENTS + 1 }, (_, index) => ({ shareType: "image" as const, value: `file:///shared/${index}.png`, mimeType: "image/png", })); const removeOwnedFile = vi.fn(() => Promise.resolve()); const readBase64 = vi.fn(async () => "YWJj"); const result = await buildIncomingShareDraft({ id: "share-3", createdAt: "2026-07-15T10:00:00.000Z", payloads, resolvedPayloads: [], fileReader: { readBase64, removeOwnedFile }, }); expect(result.attachments).toHaveLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS); expect(result.warnings).toEqual([ `Only the first ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} shared images were attached.`, ]); expect(readBase64).toHaveBeenCalledTimes(PROVIDER_SEND_TURN_MAX_ATTACHMENTS); expect(removeOwnedFile).toHaveBeenCalledTimes(payloads.length); }); it("maps duplicate image payloads to distinct resolved files", async () => { const duplicate: SharePayload = { shareType: "image", value: "content://shared/screenshot", mimeType: "image/png", }; const resolvedPayloads: ResolvedSharePayload[] = [ { ...duplicate, contentUri: "file:///cache/first.png", contentType: "image", contentMimeType: "image/png", contentSize: 3, originalName: "first.png", }, { ...duplicate, contentUri: "file:///cache/second.png", contentType: "image", contentMimeType: "image/png", contentSize: 3, originalName: "second.png", }, ]; const readBase64 = vi.fn(async (uri: string) => uri.includes("first") ? "Zmlyc3Q=" : "c2Vjb25k", ); const removeOwnedFile = vi.fn(async () => undefined); const result = await buildIncomingShareDraft({ id: "share-duplicates", createdAt: "2026-07-16T08:00:00.000Z", payloads: [duplicate, duplicate], resolvedPayloads, fileReader: { readBase64, removeOwnedFile }, }); expect(readBase64.mock.calls.map(([uri]) => uri)).toEqual([ "file:///cache/first.png", "file:///cache/second.png", ]); expect(result.attachments.map((attachment) => attachment.name)).toEqual([ "first.png", "second.png", ]); expect(removeOwnedFile).toHaveBeenCalledWith("file:///cache/first.png"); expect(removeOwnedFile).toHaveBeenCalledWith("file:///cache/second.png"); }); it("keeps imported content when temporary-file cleanup fails", async () => { const image: SharePayload = { shareType: "image", value: "file:///shared/screenshot.png", mimeType: "image/png", }; const result = await buildIncomingShareDraft({ id: "share-cleanup-failure", createdAt: "2026-07-16T08:00:00.000Z", payloads: [image], resolvedPayloads: [], fileReader: { readBase64: async () => "YWJj", removeOwnedFile: async () => { throw new Error("file is busy"); }, }, }); expect(result.attachments).toHaveLength(1); expect(result.warnings).toEqual([]); });});
import { describe, expect, it, vi } from "@effect/vitest";
import {
PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
} from "@t3tools/contracts";
import type { ResolvedSharePayload, SharePayload } from "expo-sharing";
import { buildIncomingShareDraft, hasIncomingShareContent } from "./incoming-share-model";
describe("incoming native shares", () => {
it("converts shared text, URLs, and images into a durable composer draft", async () => {
const image: SharePayload = {
shareType: "image",
value: "file:///shared/Screenshot.png",
mimeType: "image/png",
};
const payloads: SharePayload[] = [
{ shareType: "text", value: "Please explain this error" },
{ shareType: "url", value: "https://example.com/issue/1" },
{ shareType: "text", value: "Please explain this error" },
image,
];
const resolvedImage: ResolvedSharePayload = {
...image,
contentUri: image.value,
contentType: "image",
contentMimeType: "image/png",
contentSize: 3,
originalName: "Screenshot.png",
};
const removeOwnedFile = vi.fn(() => Promise.resolve());
const result = await buildIncomingShareDraft({
id: "share-1",
createdAt: "2026-07-15T10:00:00.000Z",
payloads,
resolvedPayloads: [resolvedImage],
fileReader: {
readBase64: async () => "YWJj",
removeOwnedFile,
},
});
expect(result).toEqual({
schemaVersion: 1,
id: "share-1",
createdAt: "2026-07-15T10:00:00.000Z",
text: "Please explain this error\n\nhttps://example.com/issue/1",
attachments: [
{
id: "share-1:image:3",
type: "image",
name: "Screenshot.png",
mimeType: "image/png",
sizeBytes: 3,
dataUrl: "data:image/png;base64,YWJj",
previewUri: "data:image/png;base64,YWJj",
},
],
warnings: [],
});
expect(removeOwnedFile).toHaveBeenCalledWith(image.value);
expect(hasIncomingShareContent(result)).toBe(true);
});
it("skips oversized images and releases the temporary native file", async () => {
const image: SharePayload = {
shareType: "image",
value: "file:///shared/huge.png",
mimeType: "image/png",
};
const readBase64 = vi.fn(async () => "unused");
const removeOwnedFile = vi.fn(() => Promise.resolve());
const result = await buildIncomingShareDraft({
id: "share-2",
createdAt: "2026-07-15T10:00:00.000Z",
payloads: [image],
resolvedPayloads: [
{
...image,
contentUri: image.value,
contentType: "image",
contentMimeType: "image/png",
contentSize: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1,
originalName: "huge.png",
},
],
fileReader: { readBase64, removeOwnedFile },
});
expect(result.attachments).toEqual([]);
expect(result.warnings).toEqual(["'huge.png' exceeds the 10 MB attachment limit."]);
expect(readBase64).not.toHaveBeenCalled();
expect(removeOwnedFile).toHaveBeenCalledWith(image.value);
expect(hasIncomingShareContent(result)).toBe(false);
});
it("releases every temporary file when a share exceeds the attachment limit", async () => {
const payloads = Array.from({ length: PROVIDER_SEND_TURN_MAX_ATTACHMENTS + 1 }, (_, index) => ({
shareType: "image" as const,
value: `file:///shared/${index}.png`,
mimeType: "image/png",
}));
const removeOwnedFile = vi.fn(() => Promise.resolve());
const readBase64 = vi.fn(async () => "YWJj");
const result = await buildIncomingShareDraft({
id: "share-3",
createdAt: "2026-07-15T10:00:00.000Z",
payloads,
resolvedPayloads: [],
fileReader: { readBase64, removeOwnedFile },
});
expect(result.attachments).toHaveLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS);
expect(result.warnings).toEqual([
`Only the first ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} shared images were attached.`,
]);
expect(readBase64).toHaveBeenCalledTimes(PROVIDER_SEND_TURN_MAX_ATTACHMENTS);
expect(removeOwnedFile).toHaveBeenCalledTimes(payloads.length);
});
it("maps duplicate image payloads to distinct resolved files", async () => {
const duplicate: SharePayload = {
shareType: "image",
value: "content://shared/screenshot",
mimeType: "image/png",
};
const resolvedPayloads: ResolvedSharePayload[] = [
{
...duplicate,
contentUri: "file:///cache/first.png",
contentType: "image",
contentMimeType: "image/png",
contentSize: 3,
originalName: "first.png",
},
{
...duplicate,
contentUri: "file:///cache/second.png",
contentType: "image",
contentMimeType: "image/png",
contentSize: 3,
originalName: "second.png",
},
];
const readBase64 = vi.fn(async (uri: string) =>
uri.includes("first") ? "Zmlyc3Q=" : "c2Vjb25k",
);
const removeOwnedFile = vi.fn(async () => undefined);
const result = await buildIncomingShareDraft({
id: "share-duplicates",
createdAt: "2026-07-16T08:00:00.000Z",
payloads: [duplicate, duplicate],
resolvedPayloads,
fileReader: { readBase64, removeOwnedFile },
});
expect(readBase64.mock.calls.map(([uri]) => uri)).toEqual([
"file:///cache/first.png",
"file:///cache/second.png",
]);
expect(result.attachments.map((attachment) => attachment.name)).toEqual([
"first.png",
"second.png",
]);
expect(removeOwnedFile).toHaveBeenCalledWith("file:///cache/first.png");
expect(removeOwnedFile).toHaveBeenCalledWith("file:///cache/second.png");
});
it("keeps imported content when temporary-file cleanup fails", async () => {
const image: SharePayload = {
shareType: "image",
value: "file:///shared/screenshot.png",
mimeType: "image/png",
};
const result = await buildIncomingShareDraft({
id: "share-cleanup-failure",
createdAt: "2026-07-16T08:00:00.000Z",
payloads: [image],
resolvedPayloads: [],
fileReader: {
readBase64: async () => "YWJj",
removeOwnedFile: async () => {
throw new Error("file is busy");
},
},
});
expect(result.attachments).toHaveLength(1);
expect(result.warnings).toEqual([]);
});
});