Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'import { Repo } from '@automerge/automerge-repo'import type { AutomergeUrl } from '@automerge/automerge-repo'import type { PushworkFileDocument } from '../../types/pushwork'import { DocumentManager, DocumentManagerIdentityError, DocumentManagerValidationError } from '../document-manager'import { encodeBlockFactKey } from '../../types-records/block-record-facts'import * as A from '@automerge/automerge'
const indexFailure = vi.hoisted(() => ({ next: false, fired: 0 }))vi.mock('../../automerge-markdown', async importOriginal => { const actual = await importOriginal<typeof import('../../automerge-markdown')>() return { ...actual, buildNodeIndex: (...args: Parameters<typeof actual.buildNodeIndex>) => { if (indexFailure.next) { indexFailure.next = false indexFailure.fired++ throw new Error('node index failed') } return actual.buildNodeIndex(...args) }, }})
describe('DocumentManager public contract coverage', () => { let repo: Repo let manager: DocumentManager
beforeEach(() => { repo = new Repo() manager = new DocumentManager(repo) })
afterEach(async () => { manager.dispose() await new Promise(resolve => setTimeout(resolve, 0)) await repo.shutdown() })
it.each([undefined, null, 0, {}, ''])('rejects structurally invalid document identity %j', async (value: unknown) => { await expect(manager.readText(value as never)).rejects.toBeInstanceOf(DocumentManagerIdentityError) await expect(manager.readMarkdown(value as never)).rejects.toBeInstanceOf(DocumentManagerIdentityError) await expect(manager.isCurrent(value as never, ['head'])).rejects.toBeInstanceOf(DocumentManagerIdentityError) })
it('distinguishes missing heads, unavailable documents, and current documents', async () => { const url = manager.createFile('states.md') expect(await manager.readText(url, [])).toMatchObject({ status: 'missing-heads' }) const initial = await manager.readText(url) expect(initial.status).toBe('available') if (initial.status !== 'available') throw new Error('expected available text') await manager.updateContent(url, 'changed') expect(await manager.isCurrent(url, initial.heads)).toBe(false) })
it('classifies authoritative not-found separately from other unavailable lookups', async () => { vi.spyOn(repo, 'find').mockRejectedValueOnce(new DOMException('missing', 'NotFoundError')) await expect(manager.readText('automerge:missing' as AutomergeUrl)).resolves.toMatchObject({ status: 'not-found' }) vi.spyOn(repo, 'find').mockRejectedValueOnce(new Error('storage failed')) await expect(manager.readText('automerge:unavailable' as AutomergeUrl)).resolves.toMatchObject({ status: 'unavailable' }) })
it('reads text and Markdown at an exact ancestor frontier', async () => { const url = manager.createFile('history.md') await manager.updateContent(url, 'first') const first = await manager.readText(url) if (first.status !== 'available') throw new Error('expected first frontier') await manager.updateContent(url, 'second') const text = await manager.readText(url, first.heads) expect(text).toMatchObject({ status: 'available', content: 'first', heads: first.heads }) const markdownRead = await manager.readMarkdown(url, first.heads) expect(markdownRead).toMatchObject({ status: 'available', content: 'first', heads: first.heads }) })
it('validates a projection after selecting its historical frontier', async () => { const url = manager.createFile('projection-history.md') await manager.updateContent(url, '# historical') const historical = await manager.readText(url) if (historical.status !== 'available') throw new Error('expected historical frontier') await manager.updateBinaryContent(url, new Uint8Array([1, 2, 3])) await expect(manager.readMarkdownProjectionSnapshot(url, [])).resolves.toMatchObject({ status: 'missing-heads' }) const snapshot = await manager.readMarkdownProjectionSnapshot(url, historical.heads) expect(snapshot).toMatchObject({ status: 'available', content: '# historical', heads: historical.heads }) if (snapshot.status === 'available') expect(snapshot.blocks.every(block => !('markerValue' in block))).toBe(true) })
it('projects only identity attributes without copying large opaque marker data', async () => { const url = manager.createFile('opaque-projection.md') const handle = await repo.find<PushworkFileDocument>(url) const payload = 'x'.repeat(70_000) handle.change(draft => { draft.content = 'Body' A.splitBlock(draft, ['content'], 0, { type: 'paragraph', parents: ['opaque-native-parent'], attrs: { 'reams.nodeId': 'body-id', opaque: new A.RawString(payload) }, }) }) const heads = A.getHeads(handle.doc())
const snapshot = await manager.readMarkdownProjectionSnapshot(url)
expect(snapshot).toMatchObject({ status: 'available', content: 'Body', heads }) if (snapshot.status !== 'available') throw new Error('expected snapshot') expect(snapshot.blocks).toEqual([ { plainIndex: 0, type: 'paragraph', parents: [], attrs: { 'reams.nodeId': 'body-id' } }, ]) expect(A.getHeads(handle.doc())).toEqual(heads) const attrs = A.block(handle.doc(), ['content'], 0)?.attrs as Record<string, unknown> expect(String(attrs.opaque)).toBe(payload) expect(A.block(handle.doc(), ['content'], 0)?.parents).toEqual(['opaque-native-parent']) })
it('classifies folders, binary files, and non-markdown text correctly', async () => { expect(await manager.readMarkdown(manager.createFolder())).toMatchObject({ status: 'not-markdown' }) expect(await manager.readMarkdown(manager.createBinaryFile('image.png', new Uint8Array([1]), 'image/png'))).toMatchObject({ status: 'not-markdown' }) expect(await manager.readMarkdown(manager.createFile('plain.txt'))).toMatchObject({ status: 'not-markdown' }) })
it('returns unavailable for an injected node-index failure rather than not-markdown', async () => { // A newly authored file has no prepared text-first index cache; the read // must build its index from the canonical Markdown snapshot. const url = manager.createFile('broken.md', undefined, '# Broken') // Fail the actual index-building seam without replacing the live handle's // doc() method, which repository persistence may still call asynchronously. indexFailure.fired = 0 indexFailure.next = true try { await expect(manager.readMarkdown(url)).resolves.toMatchObject({ status: 'unavailable' }) expect(indexFailure.fired).toBe(1) } finally { indexFailure.next = false } })
it('keeps binary writes atomic and rejects text writes to binary content', async () => { const url = manager.createBinaryFile('image.png', new Uint8Array([1, 2]), 'image/png') await manager.updateBinaryContent(url, new Uint8Array([3, 4])) expect(await manager.readBinaryContent(url)).toEqual(new Uint8Array([3, 4])) await expect(manager.updateContent(url, 'text')).rejects.toBeInstanceOf(DocumentManagerValidationError) })
it('rejects invalid semantic ranges before changing the document', async () => { const url = manager.createFile('range.md') await manager.updateContent(url, 'hello') const before = await manager.readText(url) if (before.status !== 'available') throw new Error('expected available text') await expect(manager.updateContentWithInlineSemanticOps(url, before.heads, 'hello', { ops: [{ kind: 'add-mark', specName: 'bold', range: { start: 0, end: 9 }, attrs: null }], })).rejects.toThrow('Invalid inline semantic range') const after = await manager.readText(url) expect(after.status).toBe('available') if (after.status === 'available') expect(after.content).toBe('hello') })
it('reports a no-op fact write without changing metadata heads', async () => { const url = manager.createFile('facts.md', undefined, 'task #task') await manager.prepareTextFirstDocument(url) await manager.initializeTextFirstLineOwners(url, ['task']) const key = encodeBlockFactKey(manager.getTextFirstDocumentState(url).lineOwners[0]!.nodeId, 'task', 'done') const first = await manager.writeBlockRecordFact(url, key, true) expect(first.changed).toBe(true) const second = await manager.writeBlockRecordFact(url, key, true) expect(second.changed).toBe(false) expect(second.heads).toEqual(first.heads) expect(second.beforeContent).toBe(second.afterContent) })
it('throws malformed-pushwork when content is a number', async () => { const url = manager.createFile('malformed.md') const handle = await repo.find<PushworkFileDocument>(url) handle.change(doc => { (doc as unknown as { content: unknown }).content = 42 }) await expect(manager.updateContent(url, 'new')).rejects.toMatchObject({ reason: 'malformed-pushwork' }) })
it('returns not-text when readText sees binary content', async () => { const url = manager.createBinaryFile('image.png', new Uint8Array([1, 2, 3]), 'image/png') await expect(manager.readText(url)).resolves.toMatchObject({ status: 'not-text', documentId: url }) })
it('publishes initialized child content before its folder entry', async () => { const folder = manager.createFolder() const file = await manager.createFileInFolder(folder, 'ready.json', undefined, '{"ready":true}') const read = await manager.readText(file) expect(read.status).toBe('available') if (read.status === 'available') expect(read.content).toBe('{"ready":true}') expect((await manager.getFolder(folder))!.docs[0]!.url).toBe(file) })
it('renames and deletes entries through the folder API', async () => { const folder = manager.createFolder() const file = await manager.createFileInFolder(folder, 'old.md') await manager.renameEntry(file, folder, 'new.md') expect((await manager.getFolder(folder))!.docs[0]!.name).toBe('new.md') await manager.deleteFile(file, folder) expect((await manager.getFolder(folder))!.docs).toEqual([]) })
it('does not call a read-side materializer for isCurrent', async () => { const url = manager.createFile('current.md') const state = await manager.readText(url) if (state.status !== 'available') throw new Error('expected available text') const read = vi.spyOn(manager, 'readText') expect(await manager.isCurrent(url, state.heads)).toBe(true) expect(read).not.toHaveBeenCalled() })})