refactor(app): introduce attachment storage layer and draft lifecycle management
Replace inline data-URL image handling with a persistent attachment storage service, add draft lifecycle states (active/abandoned/sent) with generation tracking, and update fetchAgent to return project placement alongside agent snapshots.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectImageFilesFromClipboardData,
|
||||
filesToImageAttachments,
|
||||
} from "./image-attachments-from-files";
|
||||
import { __setAttachmentStoreForTests } from "@/attachments/store";
|
||||
import type { AttachmentStore } from "@/attachments/types";
|
||||
|
||||
function createClipboardItem(params: {
|
||||
kind: string;
|
||||
@@ -16,6 +18,52 @@ function createClipboardItem(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function createTestStore(): AttachmentStore {
|
||||
let sequence = 0;
|
||||
return {
|
||||
storageType: "web-indexeddb",
|
||||
async save(input) {
|
||||
sequence += 1;
|
||||
const id = input.id ?? `att-${sequence}`;
|
||||
const mimeType = input.mimeType ?? "image/jpeg";
|
||||
const fileName = input.fileName ?? null;
|
||||
let byteSize = 0;
|
||||
if (input.source.kind === "blob") {
|
||||
byteSize = input.source.blob.size;
|
||||
} else if (input.source.kind === "data_url") {
|
||||
byteSize = input.source.dataUrl.length;
|
||||
} else {
|
||||
byteSize = input.source.uri.length;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
mimeType,
|
||||
storageType: "web-indexeddb",
|
||||
storageKey: id,
|
||||
fileName,
|
||||
byteSize,
|
||||
createdAt: 1700000000000 + sequence,
|
||||
};
|
||||
},
|
||||
async encodeBase64() {
|
||||
throw new Error("not used in this test");
|
||||
},
|
||||
async resolvePreviewUrl() {
|
||||
throw new Error("not used in this test");
|
||||
},
|
||||
async delete() {},
|
||||
async garbageCollect() {},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
__setAttachmentStoreForTests(createTestStore());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__setAttachmentStoreForTests(null);
|
||||
});
|
||||
|
||||
describe("collectImageFilesFromClipboardData", () => {
|
||||
it("returns only image files from clipboard items", () => {
|
||||
const imagePng = new File([new Uint8Array([0, 1, 2, 3])], "paste.png", {
|
||||
@@ -55,7 +103,7 @@ describe("collectImageFilesFromClipboardData", () => {
|
||||
});
|
||||
|
||||
describe("filesToImageAttachments", () => {
|
||||
it("converts files into data URI image attachments and keeps order", async () => {
|
||||
it("persists files as attachment metadata in order", async () => {
|
||||
const pngFile = new File([new Uint8Array([0, 1, 2, 3])], "first.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
@@ -67,13 +115,34 @@ describe("filesToImageAttachments", () => {
|
||||
|
||||
expect(attachments).toEqual([
|
||||
{
|
||||
uri: "data:image/png;base64,AAECAw==",
|
||||
id: "att-1",
|
||||
mimeType: "image/png",
|
||||
storageType: "web-indexeddb",
|
||||
storageKey: "att-1",
|
||||
fileName: "first.png",
|
||||
byteSize: 4,
|
||||
createdAt: 1700000000001,
|
||||
},
|
||||
{
|
||||
uri: "data:image/jpeg;base64,BAUGBw==",
|
||||
id: "att-2",
|
||||
mimeType: "image/jpeg",
|
||||
storageType: "web-indexeddb",
|
||||
storageKey: "att-2",
|
||||
fileName: "second",
|
||||
byteSize: 4,
|
||||
createdAt: 1700000000002,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles large files without creating data URLs", async () => {
|
||||
const large = new File([new Uint8Array(4 * 1024 * 1024)], "large.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
|
||||
const [attachment] = await filesToImageAttachments([large]);
|
||||
|
||||
expect(attachment?.storageType).toBe("web-indexeddb");
|
||||
expect(attachment?.byteSize).toBe(4 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user