Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677// src/plugin/__tests__/vault-adapter.test.tsimport { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'import { Vault } from '../vault'import { FileIndex } from '../../services/file-index'import type { AutomergeUrl } from '@automerge/automerge-repo'import type { TextReadResult } from '../../services/document-manager'import { withMockDirectoryEvidence } from './mock-directory-evidence'
function createMockDocumentManager() { return withMockDirectoryEvidence({ listFolder: vi.fn().mockResolvedValue([]), readText: vi.fn(async (url: AutomergeUrl): Promise<TextReadResult> => ({ status: 'available', documentId: url, heads: ['h0'], content: 'file content' })), updateContent: vi.fn().mockResolvedValue(undefined), updateBinaryContent: vi.fn().mockResolvedValue(undefined), createFileInFolder: vi.fn().mockResolvedValue('automerge:test' as AutomergeUrl), createSubfolder: vi.fn().mockResolvedValue('automerge:subfolder' as AutomergeUrl), createBinaryFileInFolder: vi.fn().mockResolvedValue('automerge:binary' as AutomergeUrl), readBinaryContent: vi.fn().mockResolvedValue(undefined), deleteFile: vi.fn().mockResolvedValue(undefined), deleteFolder: vi.fn().mockResolvedValue(undefined), renameFile: vi.fn().mockResolvedValue(undefined), renameEntry: vi.fn().mockResolvedValue(undefined), moveFile: vi.fn().mockResolvedValue(undefined), })}
async function initVaultWithNotesFile(dm: ReturnType<typeof createMockDocumentManager>): Promise<Vault> { dm.listFolder.mockReset() dm.listFolder.mockImplementation(async (url: AutomergeUrl) => { if (url === 'automerge:root') { return [ { name: 'notes', type: 'folder', url: 'automerge:notes' as AutomergeUrl }, ] } if (url === 'automerge:notes') { return [ { name: 'file.md', type: 'file', url: 'automerge:notes-file' as AutomergeUrl }, ] } return [] })
const vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init() return vault}
describe('Vault.adapter', () => { let vault: Vault let dm: ReturnType<typeof createMockDocumentManager>
beforeEach(async () => { dm = createMockDocumentManager() dm.listFolder.mockResolvedValue([ { name: 'note.md', type: 'file', url: 'automerge:note' as AutomergeUrl }, ]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init() })
afterEach(() => { vi.restoreAllMocks() })
it('exists on Vault as a property', () => { expect(vault.adapter).toBeDefined() expect(typeof vault.adapter.stat).toBe('function') expect(typeof vault.adapter.exists).toBe('function') expect(typeof vault.adapter.read).toBe('function') expect(typeof vault.adapter.write).toBe('function') })
describe('stat()', () => { it('returns stat for existing file', async () => { const result = await vault.adapter.stat('note.md') expect(result).not.toBeNull() expect(result).toHaveProperty('type', 'file') expect(result).toHaveProperty('ctime') expect(result).toHaveProperty('mtime') expect(result).toHaveProperty('size') })
it('returns null for non-existent path', async () => { const result = await vault.adapter.stat('nonexistent.md') expect(result).toBeNull() })
it('returns stat for folders', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'subfolder', type: 'folder', url: 'automerge:sub' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init() const result = await vault.adapter.stat('subfolder') expect(result).not.toBeNull() expect(result).toHaveProperty('type', 'folder') }) })
describe('exists()', () => { it('returns true for existing file', async () => { expect(await vault.adapter.exists('note.md')).toBe(true) })
it('returns false for non-existent file', async () => { expect(await vault.adapter.exists('nope.md')).toBe(false) }) })
describe('read()', () => { it('reads content of existing file', async () => { const content = await vault.adapter.read('note.md') expect(content).toBe('file content') })
it('throws for non-existent file', async () => { await expect(vault.adapter.read('nope.md')).rejects.toThrow() })
// m7: throws when text is unavailable it('throws when file content is null', async () => { dm.readText.mockImplementationOnce(async (url: AutomergeUrl) => ({ status: 'unavailable' as const, documentId: url })) await expect(vault.adapter.read('note.md')).rejects.toThrow() })
it('normalizes slashy non-root paths', async () => { vault = await initVaultWithNotesFile(dm)
await expect(vault.adapter.read('/notes//file.md')).resolves.toBe('file content') expect(dm.readText).toHaveBeenCalledWith('automerge:notes-file') }) })
describe('write()', () => { it('writes to existing file via updateContent', async () => { await vault.adapter.write('note.md', 'new content') expect(dm.updateContent).toHaveBeenCalled() })
it('creates new file if it does not exist', async () => { await vault.adapter.write('new-file.md', 'hello') expect(dm.createFileInFolder).toHaveBeenCalled() })
// M4: write fires modify event on existing file it('fires modify event when writing to existing file', async () => { const modifySpy = vi.fn() vault.on('modify', modifySpy) await vault.adapter.write('note.md', 'updated content') expect(modifySpy).toHaveBeenCalledTimes(1) expect(modifySpy.mock.calls[0][0].name).toBe('note.md') })
it('updates an existing file through a normalized adapter path', async () => { vault = await initVaultWithNotesFile(dm)
await vault.adapter.write('/notes//file.md', 'updated content')
expect(dm.updateContent).toHaveBeenCalledWith('automerge:notes-file', 'updated content') }) })
describe('list()', () => { it('returns files and folders for root', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'file1.md', type: 'file', url: 'automerge:f1' as AutomergeUrl }, { name: 'file2.txt', type: 'file', url: 'automerge:f2' as AutomergeUrl }, { name: 'folder1', type: 'folder', url: 'automerge:d1' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const result = await vault.adapter.list('/') expect(result.files).toContain('file1.md') expect(result.files).toContain('file2.txt') expect(result.folders).toContain('folder1') })
it('returns files and folders for empty string path (root)', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'note.md', type: 'file', url: 'automerge:n' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const result = await vault.adapter.list('') expect(result.files).toContain('note.md') })
it('returns contents of subfolder', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'subfolder', type: 'folder', url: 'automerge:sub' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValueOnce([ { name: 'nested.md', type: 'file', url: 'automerge:nested' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const result = await vault.adapter.list('subfolder') expect(result.files).toContain('nested.md') expect(result.folders).toHaveLength(0) })
it('includes storage-backed descendants for adopted folders', async () => { dm.listFolder.mockReset() dm.listFolder.mockImplementation(async (url: AutomergeUrl) => { if (url === 'automerge:root') { return [ { name: 'adopted', type: 'folder', url: 'automerge:adopted' as AutomergeUrl }, ] } if (url === 'automerge:adopted') { return [ { name: 'child.md', type: 'file', url: 'automerge:child' as AutomergeUrl }, { name: 'nested', type: 'folder', url: 'automerge:nested' as AutomergeUrl }, ] } if (url === 'automerge:nested') { return [ { name: 'deep.md', type: 'file', url: 'automerge:deep' as AutomergeUrl }, ] } return [] })
vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await (vault as any).ensureFolderPath('adopted')
await expect(vault.adapter.list('adopted')).resolves.toEqual({ files: ['child.md'], folders: ['nested'], }) await expect(vault.adapter.list('adopted/nested')).resolves.toEqual({ files: ['deep.md'], folders: [], }) })
it('returns empty arrays for non-existent path', async () => { const result = await vault.adapter.list('does-not-exist') expect(result.files).toHaveLength(0) expect(result.folders).toHaveLength(0) }) })
describe('exists()/stat() root normalization', () => { it('treats slash root the same as empty-string root for exists()', async () => { await expect(vault.adapter.exists('/')).resolves.toBe(true) await expect(vault.adapter.exists('')).resolves.toBe(true) })
it('treats slash root the same as empty-string root for stat()', async () => { await expect(vault.adapter.stat('/')).resolves.toMatchObject({ type: 'folder' }) await expect(vault.adapter.stat('')).resolves.toMatchObject({ type: 'folder' }) })
it('normalizes slashy non-root folder paths for exists()', async () => { vault = await initVaultWithNotesFile(dm)
await expect(vault.adapter.exists('notes/')).resolves.toBe(true) await expect(vault.adapter.exists('/notes')).resolves.toBe(true) await expect(vault.adapter.exists('notes//file.md')).resolves.toBe(true) }) })
describe('mkdir()', () => { it('creates a folder', async () => { await vault.adapter.mkdir('new-folder') expect(dm.createSubfolder).toHaveBeenCalled() })
it('creates nested folders', async () => { // Mock the folder creation to return proper URLs let callCount = 0 dm.createSubfolder.mockImplementation(async () => { callCount++ return `automerge:folder${callCount}` as AutomergeUrl })
await vault.adapter.mkdir('parent/child') // Should create both parent and child expect(dm.createSubfolder).toHaveBeenCalledTimes(2) })
it('handles slash-normalized and root-like paths truthfully', async () => { dm.createSubfolder .mockResolvedValueOnce('automerge:notes' as AutomergeUrl) .mockResolvedValueOnce('automerge:parent' as AutomergeUrl) .mockResolvedValueOnce('automerge:child' as AutomergeUrl)
await vault.adapter.mkdir('/notes') await vault.adapter.mkdir('notes/') await vault.adapter.mkdir('parent//child/') const successfulCreateCalls = dm.createSubfolder.mock.calls.length
await expect(vault.adapter.mkdir('/')).rejects.toThrow('Cannot create root folder') await expect(vault.adapter.mkdir('//')).rejects.toThrow('Cannot create root folder')
expect(dm.createSubfolder).toHaveBeenCalledTimes(successfulCreateCalls) expect(vault.getFolderByPath('notes')?.path).toBe('notes') expect(vault.getFolderByPath('parent/child')?.path).toBe('parent/child') }) })
describe('rmdir()', () => { beforeEach(async () => { dm.listFolder.mockReset() dm.listFolder.mockImplementation(async (url: AutomergeUrl) => { if (url === 'automerge:root') { return [ { name: 'empty-folder', type: 'folder', url: 'automerge:empty' as AutomergeUrl }, { name: 'non-empty', type: 'folder', url: 'automerge:nonempty' as AutomergeUrl }, ] } if (url === 'automerge:empty') { return [] } if (url === 'automerge:nonempty') { return [ { name: 'file.md', type: 'file', url: 'automerge:file' as AutomergeUrl }, ] } return [] }) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init() })
it('removes empty folder', async () => { await vault.adapter.rmdir('empty-folder', false) expect(dm.deleteFolder).toHaveBeenCalled() })
it('throws for non-empty folder without recursive flag', async () => { await expect(vault.adapter.rmdir('non-empty', false)).rejects.toThrow('not empty') })
it('treats storage-adopted folders with storage children as non-empty even if children are not hydrated', async () => { dm.listFolder.mockReset() dm.listFolder.mockImplementation(async (url: AutomergeUrl) => { if (url === 'automerge:root') { return [ { name: 'adopted', type: 'folder', url: 'automerge:adopted' as AutomergeUrl }, ] } if (url === 'automerge:adopted') { return [ { name: 'child.md', type: 'file', url: 'automerge:child' as AutomergeUrl }, ] } return [] }) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
await expect(vault.adapter.rmdir('adopted', false)).rejects.toThrow('not empty') expect(dm.deleteFolder).not.toHaveBeenCalled() })
it('removes non-empty folder with recursive flag', async () => { await vault.adapter.rmdir('non-empty', true) expect(dm.deleteFolder).toHaveBeenCalled() })
it('throws for non-existent folder', async () => { await expect(vault.adapter.rmdir('does-not-exist', false)).rejects.toThrow('not found') })
it('rejects recursive removal of the vault root', async () => { await expect(vault.adapter.rmdir('', true)).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFolder).not.toHaveBeenCalled() })
it('rejects non-recursive removal of the empty-string root before emptiness checks', async () => { await expect(vault.adapter.rmdir('', false)).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFolder).not.toHaveBeenCalled() })
it('treats slash root removal the same as empty-string root removal', async () => { await expect(vault.adapter.rmdir('/', true)).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFolder).not.toHaveBeenCalled() })
it('rejects non-recursive removal of the slash root before emptiness checks', async () => { await expect(vault.adapter.rmdir('/', false)).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFolder).not.toHaveBeenCalled() }) })
describe('remove()', () => { it('removes a file', async () => { await vault.adapter.remove('note.md') expect(dm.deleteFile).toHaveBeenCalled() })
it('removes a folder', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'folder', type: 'folder', url: 'automerge:folder' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
await vault.adapter.remove('folder') expect(dm.deleteFolder).toHaveBeenCalled() })
it('throws for non-existent path', async () => { await expect(vault.adapter.remove('nope.md')).rejects.toThrow('not found') })
it('treats slash root removal the same as empty-string root removal', async () => { await expect(vault.adapter.remove('/')).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFile).not.toHaveBeenCalled() expect(dm.deleteFolder).not.toHaveBeenCalled() })
it('normalizes slashy non-root paths before removal', async () => { vault = await initVaultWithNotesFile(dm)
await vault.adapter.remove('/notes//file.md')
expect(dm.deleteFile).toHaveBeenCalledWith('automerge:notes-file', 'automerge:notes', expect.any(Function), undefined) expect(vault.getFileByPath('notes/file.md')).toBeNull() }) })
describe('trashLocal()', () => { it('treats slash root trashing the same as empty-string root trashing', async () => { await expect(vault.adapter.trashLocal('/')).rejects.toThrow('Cannot delete vault root') expect(dm.deleteFile).not.toHaveBeenCalled() expect(dm.deleteFolder).not.toHaveBeenCalled() }) })
describe('rename()', () => { it('normalizes slash root and rejects renaming the synthetic root folder', async () => { await expect(vault.adapter.rename('/', 'renamed-root')).rejects.toThrow('Cannot rename root folder') expect(dm.renameEntry).not.toHaveBeenCalled() expect(dm.moveFile).not.toHaveBeenCalled() })
it('renames a file', async () => { await vault.adapter.rename('note.md', 'renamed.md') expect(dm.renameFile).toHaveBeenCalled() })
it('throws for non-existent file', async () => { await expect(vault.adapter.rename('nope.md', 'other.md')).rejects.toThrow('not found') })
it('normalizes slashy non-root source and destination paths', async () => { vault = await initVaultWithNotesFile(dm)
await vault.adapter.rename('/notes//file.md', 'notes//renamed.md')
expect(dm.renameFile).toHaveBeenCalledWith('automerge:notes-file', 'automerge:notes', 'renamed.md', 'file.md', expect.any(Function)) expect(vault.getFileByPath('notes/file.md')).toBeNull() expect(vault.getFileByPath('notes/renamed.md')?.path).toBe('notes/renamed.md') }) })
describe('copy()', () => { it('throws with clear error message', async () => { await expect(vault.adapter.copy('note.md', 'copy.md')).rejects.toThrow( 'Vault.adapter.copy is not implemented in Reams' ) }) })
describe('readBinary()', () => { beforeEach(async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'image.png', type: 'file', url: 'automerge:img' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init() })
it('returns ArrayBuffer for binary file', async () => { const binaryData = new Uint8Array([1, 2, 3, 4]) dm.readBinaryContent.mockResolvedValue(binaryData)
const result = await vault.adapter.readBinary('image.png') expect(result).toBeInstanceOf(ArrayBuffer) expect(new Uint8Array(result)).toEqual(binaryData) })
it('throws for non-existent file', async () => { await expect(vault.adapter.readBinary('nope.png')).rejects.toThrow('not found') })
it('throws when file has no binary content', async () => { dm.readBinaryContent.mockResolvedValue(undefined) await expect(vault.adapter.readBinary('image.png')).rejects.toThrow('no binary content') }) })
describe('writeBinary()', () => { it('creates new binary file', async () => { const data = new ArrayBuffer(4) new Uint8Array(data).set([1, 2, 3, 4])
await vault.adapter.writeBinary('new-image.png', data) expect(dm.createBinaryFileInFolder).toHaveBeenCalled() })
it('updates existing binary file', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'existing.bin', type: 'file', url: 'automerge:existing' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const data = new ArrayBuffer(4) await vault.adapter.writeBinary('existing.bin', data) expect(dm.updateBinaryContent).toHaveBeenCalled() })
it('fires modify event when updating existing file', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'existing.bin', type: 'file', url: 'automerge:existing' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const modifySpy = vi.fn() vault.on('modify', modifySpy)
const data = new ArrayBuffer(4) await vault.adapter.writeBinary('existing.bin', data) expect(modifySpy).toHaveBeenCalledTimes(1) }) })
describe('readBinary/writeBinary round-trip', () => { it('round-trips binary data correctly', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const originalData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) dm.createBinaryFileInFolder.mockImplementation(async (_folder, fileName) => { // Simulate creating the file const url = `automerge:${fileName}` as AutomergeUrl // Re-init vault with the new file dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: fileName, type: 'file', url }, ]) dm.listFolder.mockResolvedValue([]) dm.readBinaryContent.mockResolvedValue(originalData) return url })
await vault.adapter.writeBinary('test.png', originalData.buffer)
// Re-init to pick up the "created" file vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
const readBack = await vault.adapter.readBinary('test.png') expect(new Uint8Array(readBack)).toEqual(originalData) }) })
describe('append()', () => { it('appends to existing file', async () => { dm.readText.mockImplementationOnce(async (url: AutomergeUrl) => ({ status: 'available' as const, documentId: url, heads: ['h0'], content: 'existing ' })) await vault.adapter.append('note.md', 'appended') expect(dm.updateContent).toHaveBeenCalledWith( expect.anything(), 'existing appended' ) })
it('creates file if it does not exist', async () => { await vault.adapter.append('new-file.md', 'content') expect(dm.createFileInFolder).toHaveBeenCalled() }) })
describe('process()', () => { it('normalizes slashy non-root paths before processing', async () => { vault = await initVaultWithNotesFile(dm) dm.readText.mockImplementationOnce(async (url: AutomergeUrl) => ({ status: 'available' as const, documentId: url, heads: ['h0'], content: 'start' }))
await expect(vault.adapter.process('/notes//file.md', data => `${data}-done`)).resolves.toBe('start-done') expect(dm.updateContent).toHaveBeenCalledWith('automerge:notes-file', 'start-done') }) })
describe('getBasePath()', () => { it('throws with clear error message about browser PWA', () => { expect(() => vault.adapter.getBasePath()).toThrow('browser-based PWA') expect(() => vault.adapter.getBasePath()).toThrow('getBasePath') }) })
describe('getResourcePath()', () => { it('throws for non-existent file', () => { expect(() => vault.adapter.getResourcePath('nope.md')).toThrow('not found') })
it('returns automerge URL for non-image files', () => { const result = vault.adapter.getResourcePath('note.md') expect(result).toContain('automerge:') })
it('normalizes slashy non-root paths before resolving a resource path', async () => { vault = await initVaultWithNotesFile(dm)
expect(vault.adapter.getResourcePath('/notes//file.md')).toContain('automerge:notes-file') })
it('throws for image without cached URL', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'image.png', type: 'file', url: 'automerge:img' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
expect(() => vault.adapter.getResourcePath('image.png')).toThrow('Image not yet resolved') })
it('returns cached blob URL for image', async () => { dm.listFolder.mockReset() dm.listFolder.mockResolvedValueOnce([ { name: 'image.png', type: 'file', url: 'automerge:img' as AutomergeUrl }, ]) dm.listFolder.mockResolvedValue([]) vault = new Vault('test', new FileIndex(dm as any, 'automerge:root' as AutomergeUrl), dm as any) await vault.init()
// Mock window.app with reamsImageResolver that has cached URL const originalApp = (globalThis as any).window?.app ;(globalThis as any).window = { app: { reamsImageResolver: { getCachedUrl: (path: string) => path === 'image.png' ? 'blob:http://test/abc123' : null, }, }, }
try { const result = vault.adapter.getResourcePath('image.png') expect(result).toBe('blob:http://test/abc123') } finally { if (originalApp) { (globalThis as any).window.app = originalApp } else { delete (globalThis as any).window } } }) })})