import { describe, expect, test } from "bun:test"; import type { AiPromptTemplate, JsonBackupMemo, JsonBackupNotebook, MemoDetail, Notebook, Resource } from "@edgeever/shared"; import { splitMarkdownFrontMatter } from "@edgeever/shared"; import { strFromU8, strToU8, unzipSync, zipSync } from "fflate"; import { createEdgeEverZip, parseEdgeEverZip, restoreEdgeEverZip, restoreEdgeEverZipAndRefresh, } from "../apps/web/src/lib/json-backup"; const notebook = (id: string, name: string, parentId: string | null = null): Notebook => ({ id, parentId, name, slug: id, icon: "notebook", color: null, sortOrder: parentId ? 20 : 10, memoCount: 1, lastMemoUpdatedAt: "2026-07-14T00:00:00.000Z", createdAt: "2026-07-13T00:00:00.000Z", updatedAt: "2026-07-14T00:00:00.000Z", }); const memo: MemoDetail = { id: "memo_backup", notebookId: "nb_child", title: "Backup note", excerpt: "Hello", tags: ["backup"], isPinned: false, isArchived: false, isDeleted: false, revision: 1, createdAt: "2026-07-13T00:00:00.000Z", updatedAt: "2026-07-14T00:00:00.000Z", deletedAt: null, contentJson: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }] }, contentMarkdown: "Hello\n\n![asset](/api/v1/resources/res_backup/blob)", contentText: "Hello", contentHash: "hash", sourceMemoIds: [], mergeSourceCount: 0, mergedIntoMemoId: null, }; const resource: Resource = { id: "res_backup", memoId: memo.id, originalMemoId: null, kind: "image", mimeType: "image/png", filename: "asset.png", byteSize: 3, sha256: "sha", width: 10, height: 10, createdAt: memo.createdAt, updatedAt: memo.updatedAt, url: "/api/v1/resources/res_backup/blob", }; const prompt: AiPromptTemplate = { id: "aiprompt_backup", origin: "custom", seedKey: null, action: "custom", parameterKind: "tone", resultMode: "replace", nameCustomized: true, descriptionCustomized: true, instructionCustomized: true, name: "Polish", description: "Polish selected text", instruction: "Polish this text.", createdAt: memo.createdAt, updatedAt: memo.updatedAt, }; describe("EdgeEver ZIP", () => { test("reports actionable archive validation errors", async () => { await expect(parseEdgeEverZip(new Blob(["not a zip"]))).rejects.toEqual( expect.objectContaining({ code: "invalidZip" }) ); const localHeaderOnly = new Uint8Array(64); localHeaderOnly.set([0x50, 0x4b, 0x03, 0x04]); await expect(parseEdgeEverZip(new Blob([localHeaderOnly]))).rejects.toEqual( expect.objectContaining({ code: "invalidZip" }) ); await expect(parseEdgeEverZip(new Blob([zipSync({ "notebooks.json": strToU8("[]") })]))).rejects.toEqual( expect.objectContaining({ code: "missingManifest" }) ); const manifest = { format: "another-app", formatVersion: 1, schemaVersion: 1, edgeeverVersion: "0.1.13", buildId: "test-build", exportedAt: "2026-07-14T00:00:00.000Z", includesTrash: false, counts: { notebooks: 0, memos: 0, revisions: 0, resources: 0 }, }; await expect(parseEdgeEverZip(new Blob([zipSync({ "manifest.json": strToU8(JSON.stringify(manifest)), })]))).rejects.toEqual(expect.objectContaining({ code: "unsupportedFormat" })); await expect(parseEdgeEverZip(new Blob([zipSync({ "manifest.json": strToU8(JSON.stringify({ ...manifest, format: "edgeever-zip", formatVersion: 99 })), })]))).rejects.toEqual(expect.objectContaining({ code: "unsupportedVersion" })); }); test("combines readable Markdown with versioned recovery data", async () => { const resourceChunks: number[] = []; const blob = await createEdgeEverZip( { listNotebooks: async () => ({ notebooks: [notebook("nb_child", "Child", "nb_root"), notebook("nb_root", "Root")] }), listPrompts: async () => ({ prompts: [prompt] }), getPage: async () => ({ memos: [memo], resources: [resource], revisions: [{ id: "rev_backup", memoId: memo.id, revision: 0, title: memo.title, tags: memo.tags, contentJson: memo.contentJson, contentMarkdown: "Hello", contentText: "Hello", contentHash: "old-hash", createdBy: "user", createdAt: memo.createdAt, }], totalCount: 1, nextOffset: null, }), getResourceResponse: async () => new Response(new ReadableStream({ start(controller) { for (const chunk of [new Uint8Array([1]), new Uint8Array([2, 3])]) { resourceChunks.push(chunk.byteLength); controller.enqueue(chunk); } controller.close(); }, }), { headers: { "Content-Type": "image/png" } }), }, { edgeeverVersion: "0.1.13", buildId: "test-build" } ); const backup = await parseEdgeEverZip(blob); expect(backup.manifest.format).toBe("edgeever-zip"); expect(backup.manifest.formatVersion).toBe(2); expect(backup.manifest.counts).toEqual({ notebooks: 2, memos: 1, revisions: 1, resources: 1, prompts: 1 }); expect(backup.prompts).toEqual([prompt]); expect(backup.notebooks.map((item) => item.id)).toEqual(["nb_root", "nb_child"]); expect(backup.memos[0].memo.contentMarkdown).toBe(memo.contentMarkdown); expect(backup.archive).toBe(blob); expect(resourceChunks).toEqual([1, 2]); const files = unzipSync(new Uint8Array(await blob.arrayBuffer())); expect(Array.from(files[backup.memos[0].resources[0].archivePath])).toEqual([1, 2, 3]); const markdown = strFromU8(files["notes/Root/Child/Backup note.md"]); expect(splitMarkdownFrontMatter(markdown)?.values?.edgeever_id).toBe("memo_backup"); expect(markdown).toContain("![asset](Backup%20note.assets/asset.png)"); expect(backup.memos[0].resources[0].archivePath).toBe("notes/Root/Child/Backup note.assets/asset.png"); }); test("keeps version 1 archives importable without prompt data", async () => { const legacyManifest = { format: "edgeever-zip", formatVersion: 1, schemaVersion: 1, edgeeverVersion: "1.13.3", buildId: "legacy-build", exportedAt: "2026-08-09T00:00:00.000Z", includesTrash: false, counts: { notebooks: 0, memos: 0, revisions: 0, resources: 0 }, }; const backup = await parseEdgeEverZip(new Blob([zipSync({ "manifest.json": strToU8(JSON.stringify(legacyManifest)), "notebooks.json": strToU8("[]"), })])); expect(backup.manifest.formatVersion).toBe(1); expect(backup.prompts).toEqual([]); }); test("reads large archives by bounded ranges instead of buffering the ZIP", async () => { const manifest = { format: "edgeever-zip", formatVersion: 1, schemaVersion: 1, edgeeverVersion: "1.13.3", buildId: "large-stream-test", exportedAt: "2026-08-31T00:00:00.000Z", includesTrash: false, counts: { notebooks: 0, memos: 0, revisions: 0, resources: 0 }, }; const storedPayload = new Uint8Array(1024 * 1024 + 10); const actualArchive = new Blob([zipSync({ "manifest.json": strToU8(JSON.stringify(manifest)), "notebooks.json": strToU8("[]"), "pad.bin": storedPayload, }, { level: 0 })]); const rangeLengths: number[] = []; let wholeArchiveBuffered = false; const largeStreamingArchive = { size: actualArchive.size, type: "application/zip", slice(start?: number, end?: number) { const from = start ?? 0; const to = end ?? actualArchive.size; const length = to - from; rangeLengths.push(length); if (length > 1024 * 1024) throw new Error(`Buffered ${length} bytes from the archive`); return actualArchive.slice(from, to); }, stream() { throw new Error("The whole archive must not be streamed"); }, arrayBuffer: async () => { wholeArchiveBuffered = true; throw new Error("The whole archive must not be buffered"); }, } as Blob; const backup = await parseEdgeEverZip(largeStreamingArchive); expect(backup.manifest.buildId).toBe("large-stream-test"); expect(backup.archive).toBe(largeStreamingArchive); expect(wholeArchiveBuffered).toBe(false); expect(rangeLengths).toContain(1024 * 1024); expect(Math.max(...rangeLengths)).toBeLessThanOrEqual(1024 * 1024); }); test("restores stored Office attachments that contain their own ZIP entries", async () => { const docx = zipSync({ "[Content_Types].xml": strToU8(""), "word/document.xml": strToU8("hello"), }); const xlsx = zipSync({ "[Content_Types].xml": strToU8(""), "xl/workbook.xml": strToU8(""), }); const officeNotebook = notebook("nb_office", "资料"); const officeMemo: MemoDetail = { ...memo, id: "memo_office", notebookId: officeNotebook.id, title: "Office", contentMarkdown: "office", }; const officeResource = ( id: string, filename: string, mimeType: string, bytes: Uint8Array, ): Resource => ({ ...resource, id, memoId: officeMemo.id, kind: "attachment", mimeType, filename, byteSize: bytes.byteLength, sha256: null, width: null, height: null, url: `/api/v1/resources/${id}/blob`, }); const docxResource = officeResource( "res_docx", "brief.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", docx, ); const xlsxResource = officeResource( "res_xlsx", "sheet.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xlsx, ); const files = new Map([ [docxResource.url, docx], [xlsxResource.url, xlsx], ]); const blob = await createEdgeEverZip({ listNotebooks: async () => ({ notebooks: [officeNotebook] }), listPrompts: async () => ({ prompts: [] }), getPage: async () => ({ memos: [officeMemo], resources: [docxResource, xlsxResource], revisions: [], totalCount: 1, nextOffset: null, }), getResourceResponse: async (url) => new Response(files.get(url)), }, { edgeeverVersion: "1.95.0", buildId: "office-zip" }); const backup = await parseEdgeEverZip(blob); const restored = new Map(); await restoreEdgeEverZip(backup, { restoreNotebooks: async () => {}, restoreMemos: async () => {}, restorePrompts: async () => {}, createResourceRestoreSink: async (resourceId) => { const chunks: Uint8Array[] = []; return { write: async (chunk) => { chunks.push(new Uint8Array(chunk)); }, close: async () => { const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.byteLength, 0)); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } restored.set(resourceId, bytes); }, abort: async () => {}, }; }, }); expect(backup.manifest.counts.resources).toBe(2); expect(backup.memos[0].resources.map((item) => item.archivePath)).toEqual([ "notes/资料/Office.assets/brief.docx", "notes/资料/Office.assets/sheet.xlsx", ]); expect(restored.get("res_docx")).toEqual(docx); expect(restored.get("res_xlsx")).toEqual(xlsx); }); test("restores notebooks before memos and binary resources", async () => { const calls: string[] = []; const { url: _url, ...resourceMetadata } = resource; const backupMemo: JsonBackupMemo = { memo, revisions: [], resources: [{ ...resourceMetadata, archivePath: "resources/res_backup/asset.png" }], }; const backup = { manifest: { format: "edgeever-zip" as const, formatVersion: 2 as const, schemaVersion: 1, edgeeverVersion: "0.1.13", buildId: "test-build", exportedAt: "2026-07-14T00:00:00.000Z", includesTrash: false as const, counts: { notebooks: 1, memos: 1, revisions: 0, resources: 1, prompts: 1 }, }, notebooks: [{ id: "nb_child", parentId: null, name: "Child", slug: null, icon: null, color: null, sortOrder: 0, createdAt: memo.createdAt, updatedAt: memo.updatedAt, }] satisfies JsonBackupNotebook[], memos: [backupMemo], prompts: [prompt], archive: new Blob([zipSync({ "resources/res_backup/asset.png": new Uint8Array([1, 2, 3]) })]), }; await restoreEdgeEverZip(backup, { restoreNotebooks: async () => { calls.push("notebooks"); }, restoreMemos: async () => { calls.push("memos"); }, restorePrompts: async () => { calls.push("prompts"); }, createResourceRestoreSink: async () => { let byteSize = 0; return { write: async (chunk) => { byteSize += chunk.byteLength; }, close: async () => { calls.push(`resource:${byteSize}`); }, abort: async () => {}, }; }, }); expect(calls).toEqual(["prompts", "notebooks", "memos", "resource:3"]); }); test("refreshes the workspace after all imported data is restored", async () => { const calls: string[] = []; const backup = { manifest: { format: "edgeever-zip" as const, formatVersion: 1 as const, schemaVersion: 1, edgeeverVersion: "1.13.3", buildId: "test-build", exportedAt: "2026-08-09T00:00:00.000Z", includesTrash: false as const, counts: { notebooks: 1, memos: 1, revisions: 0, resources: 0 }, }, notebooks: [{ id: "nb_child", parentId: null, name: "Child", slug: null, icon: null, color: null, sortOrder: 0, createdAt: memo.createdAt, updatedAt: memo.updatedAt, }] satisfies JsonBackupNotebook[], memos: [{ memo, revisions: [], resources: [] }] satisfies JsonBackupMemo[], prompts: [], archive: new Blob([zipSync({})]), }; await restoreEdgeEverZipAndRefresh( backup, { restoreNotebooks: async () => { calls.push("notebooks"); }, restoreMemos: async () => { calls.push("memos"); }, restorePrompts: async () => { calls.push("prompts"); }, createResourceRestoreSink: async () => ({ write: async () => {}, close: async () => { calls.push("resource"); }, abort: async () => {}, }), }, async () => { calls.push("refresh"); }, ); expect(calls).toEqual(["notebooks", "memos", "refresh"]); }); });