Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164import assert from 'node:assert/strict'import { createHash } from 'node:crypto'import { chromium } from 'playwright'import { from, save, getAllChanges, decodeChange, change, clone } from '@automerge/automerge'
// Real built application, isolated browser profiles, real OAuth/PDS/HappyView.// Only assertions poll: all transport is the application's ordinary scheduler.export async function verifyBackgroundSidebarSync({ page, origin, account, launchOptions, approvePdsPopup, backupPath }) { const browser = await chromium.launch(launchOptions) const other = await browser.newPage({ ignoreHTTPSErrors: true }) const errors = [] for (const [label, target] of [['first', page], ['second', other]]) { target.on('pageerror', error => errors.push(`${label}: ${error.message}`)) target.on('console', message => { if (message.type() === 'error') errors.push(`${label}: ${message.text()}`) }) } const identity = target => target.evaluate(() => ({ root: window.__reamsApp.vault.rootUrl, space: JSON.parse(localStorage.getItem(`reams:atproto-vault:${JSON.parse(localStorage.getItem('reams:atproto:identity')).did}`)).uri })) const inspect = (target, path) => target.evaluate(async path => { const app = window.__reamsApp const file = app?.vault.getFileByPath(path) return file ? { url: app.vault.getUrlForPath(path), text: await app.vault.read(file) } : null }, path) const assertSidebar = async (target, name) => { await target.getByRole('tab', { name: 'Files', exact: true }).click() await target.getByRole('treeitem', { name: new RegExp(name.replaceAll('.', '\\.')) }).waitFor({ state: 'visible', timeout: 60_000 }) } const waitForText = async (target, path, text) => { const deadline = Date.now() + 60_000 while (Date.now() < deadline) { if ((await inspect(target, path).catch(() => null))?.text === text) return await new Promise(resolve => setTimeout(resolve, 250)) } throw new Error(`Background sync did not deliver ${path} with expected contents`) } const archiveRoot = from({ '@patchwork': { type: 'folder' }, docs: [] }) const importBackup = async (target, name, oversized = false) => { let imageEvidence const docs = { root: change(clone(archiveRoot), draft => { draft.docs.push({ name: `${name}.md`, type: 'file', url: `automerge:${name}` }) }), [name]: from({ '@patchwork': { type: 'file' }, name: `${name}.md`, contentType: 'public.markdown', content: `Imported on ${name}` }), } if (oversized) { // An incompressible binary change reproduces the real archive's images. // IndexedDB loads documents in key order. Put this blocker before the // mounted root, reproducing production's unpublished final root links. const nativeId = id => { let bits = 0, value = 0, result = '' for (const byte of createHash('sha256').update(`reams-legacy-import\0v1\0automerge:${id}`).digest()) { value = (value << 8) | byte; bits += 8 while (bits >= 5) { bits -= 5; result += 'abcdefghijklmnopqrstuvwxyz234567'[(value >>> bits) & 31] } } return result.slice(0, 26) } const mountedRoot = (await identity(target)).root.replace(/^automerge:/, '') let imageId = 'image', candidate = 0 while (nativeId(imageId) >= mountedRoot || nativeId(imageId) >= nativeId(name)) imageId = `image-${++candidate}` const initial = from({ '@patchwork': { type: 'file' }, name: 'large.png', contentType: 'public.png', content: new Uint8Array() }) const content = new Uint8Array(96 * 1024) for (let offset = 0; offset < content.length; offset += 32) { content.set(createHash('sha256').update(`reams-large-change-regression-v1:${offset}`).digest(), offset) } docs[imageId] = change(initial, draft => { draft.content = content }) assert(getAllChanges(docs[imageId]).some(bytes => bytes.length > 65536)) docs.root = change(docs.root, draft => { draft.docs.push({ name: 'large.png', type: 'file', url: `automerge:${imageId}` }) }) imageEvidence = { url: `automerge:${nativeId(imageId)}`, digest: createHash('sha256').update(docs[imageId].content).digest('hex'), changeHashes: getAllChanges(docs[imageId]).map(bytes => decodeChange(bytes).hash), } console.log('Oversized publication fixture:', { imageId: nativeId(imageId), mountedRoot }) } const records = Object.entries(docs).map(([id, doc]) => { const bytes = save(doc) return { key: [id, 'snapshot', decodeChange(getAllChanges(doc)[0]).hash], binary: Buffer.from(bytes).toString('base64'), byteLength: bytes.length } }) const buffer = Buffer.from(JSON.stringify({ format: 'reams-legacy-automerge-indexeddb', version: 1, createdAt: '2026-10-08T00:00:00Z', records, localStorage: { 'reams:vault-root-url': 'automerge:root' } })) await target.goto(`${origin}/import`) await target.getByLabel('Classic backup JSON').setInputFiles({ name: `${name}.json`, mimeType: 'application/json', buffer }) await target.getByRole('button', { name: 'Start import' }).click() await target.getByText(`Import complete · ${oversized ? 3 : 2} documents, 1 folders`).waitFor({ timeout: 60_000 }) await target.getByRole('button', { name: 'Open editor' }).click() await target.locator('.cm-editor').waitFor({ timeout: 60_000 }) await assertSidebar(target, `${name}.md`) return imageEvidence } try { await page.getByRole('button', { name: 'Close account card' }).click() await other.goto(origin) await other.getByRole('button', { name: 'Open ATProto account' }).click() await other.getByRole('combobox', { name: 'Handle' }).fill(account.did) await other.getByRole('button', { name: 'Sign in', exact: true }).waitFor() const popupPromise = other.waitForEvent('popup') await other.getByRole('button', { name: 'Sign in', exact: true }).click() const reloaded = other.waitForEvent('load', { timeout: 60_000 }) await approvePdsPopup(await popupPromise, account, /Lichen Wiki/i) await reloaded await other.locator('.cm-editor').waitFor({ timeout: 60_000 }) assert.deepEqual(await identity(other), await identity(page))
await page.locator('[title="New file"]').first().click() const dialog = page.getByRole('dialog', { name: 'New' }) await dialog.getByPlaceholder('my-note or folder/my-note').fill('Live sync.md') await dialog.getByRole('button', { name: 'Create', exact: true }).click() await assertSidebar(other, 'Live sync.md') await other.getByRole('treeitem', { name: /Live sync\.md/ }).click() await other.locator('.cm-content').waitFor() await page.evaluate(async () => { const app = window.__reamsApp; await app.vault.modify(app.vault.getFileByPath('Live sync.md'), 'Edited on the first running device') }) await waitForText(other, 'Live sync.md', 'Edited on the first running device') await other.waitForFunction(() => [...document.querySelectorAll('.cm-content')].some(editor => editor.textContent.includes('Edited on the first running device')), undefined, { timeout: 30_000 }) await other.evaluate(async () => { const app = window.__reamsApp; await app.vault.modify(app.vault.getFileByPath('Live sync.md'), 'Edited on the second running device') }) await waitForText(page, 'Live sync.md', 'Edited on the second running device') console.log('PASS: same space/root, new sidebar item, open-editor update and reverse edit through background polling.')
await importBackup(page, 'desktop') await assertSidebar(other, 'desktop.md') await waitForText(other, 'desktop.md', 'Imported on desktop') await importBackup(other, 'mobile') await assertSidebar(page, 'mobile.md') await waitForText(page, 'mobile.md', 'Imported on mobile') for (const path of ['desktop.md', 'mobile.md', 'Live sync.md']) assert.deepEqual(await inspect(page, path), await inspect(other, path)) console.log('PASS: separate imports on two devices appear in both live sidebars with identical document IDs and contents, without explicit publish/sync/relogin.')
if (process.env.RUN_SYNC_LARGE_CHANGE === '1') { const image = await importBackup(page, 'oversized', true) await assertSidebar(other, 'oversized.md') await waitForText(other, 'oversized.md', 'Imported on oversized') await assertSidebar(other, 'large.png') const recovered = await other.evaluate(async url => { const app = window.__reamsApp const bytes = await app.vault.readBinary(app.vault.getFileByPath('large.png')) const handle = await app.reamsDocumentManager.repo.find(url) return { bytes: [...new Uint8Array(bytes)], hashes: handle.history().map(entry => entry.change.hash) } }, image.url) assert.equal(createHash('sha256').update(Buffer.from(recovered.bytes)).digest('hex'), image.digest) for (const hash of image.changeHashes) assert(recovered.hashes.includes(hash), `Original change ${hash} must survive publication`) const deadline = Date.now() + 60_000 while (Date.now() < deadline && await page.evaluate(() => window.__reamsApp.reamsDocumentManager.repo.runtime.getStatus().pendingDocuments) !== 0) { await new Promise(resolve => setTimeout(resolve, 250)) } assert.equal(await page.evaluate(() => window.__reamsApp.reamsDocumentManager.repo.runtime.getStatus().pendingDocuments), 0) console.log('PASS: linked oversized image recovered byte-for-byte with original history and no pending publications.') const { verifyIndependentBackgroundReadback } = await import('./background-sync-readback.mjs') await verifyIndependentBackgroundReadback({ page, other, origin, account, launchOptions, approvePdsPopup, backupPath }) } } catch (error) { for (const [label, target] of [['first', page], ['second', other]]) { console.error(label, await target.evaluate(() => ({ pending: window.__reamsApp?.reamsDocumentManager.repo.runtime.getStatus().pendingDocuments, files: window.__reamsApp?.vault.getFiles().map(file => file.path), activeFile: window.__reamsApp?.workspace.getActiveFile()?.path, editorTexts: [...document.querySelectorAll('.cm-content')].map(editor => editor.textContent), error: document.querySelector('.atproto-account-error')?.textContent, alerts: [...document.querySelectorAll('[role=alert]')].map(alert => alert.textContent), })).catch(() => null)) const accountButton = target.getByRole('button', { name: 'Open ATProto account' }) if (await accountButton.isVisible().catch(() => false)) await accountButton.click() console.error(label, await target.locator('.atproto-account-card').innerText().catch(() => '(no account card)')) } console.error(errors) throw error } finally { await browser.close() }}