Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
8.2 kB · 157 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158// tests/format-preservation/plugin-api.property.test.tsimport { describe, it, expect, vi } from 'vitest'import * as A from '@automerge/automerge'import { reconcile } from './harness/legacy'import { ConstructRegistry, RegistrationError } from '../../src/format-preservation'import { emitMark } from './harness/legacy'import type { ConstructSpec, FrameworkDoc } from '../../src/format-preservation/types'
function makePluginSpec(name: string, overrides: Partial<ConstructSpec> = {}): ConstructSpec { return { name, schemaVersion: 1, defaultExpand: 'both', categories: ['inline'], synthesisPrecedence: 100, serializeAttrs: () => '{}', parseAttrs: () => ({}), classify: () => ({ kind: 'clean' }), rewriteForLocalState: () => [], synthesizeFromText: () => [], ...overrides, } as ConstructSpec}
describe('plugin API property tests', () => { it('1. Register a plugin spec, emit a mark, verify owner field', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('my-mark')) let doc = A.from<{ content: string }>({ content: 'hello world' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'my-mark', { start: 0, end: 5 }, {}) }) const marks = A.marks(doc as A.Doc<unknown>, ['content']) expect(marks).toHaveLength(1) const blob = JSON.parse(marks[0].value as string) expect(blob.o).toBe('plugin:test-plugin') })
it('2. Unregister spec, verify marks are preserved in doc', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('my-mark')) let doc = A.from<{ content: string }>({ content: 'hello world' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'my-mark', { start: 0, end: 5 }, {}) }) // Unregister the spec registry.unregisterPluginSpecs('test-plugin') expect(registry.getSpec('my-mark')).toBeNull() // Reconcile with unregistered spec — mark should be preserved (opaque) const { doc: newDoc } = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' } }) const marks = A.marks(newDoc as A.Doc<unknown>, ['content']) expect(marks).toHaveLength(1) expect(marks[0].name).toBe('my-mark') })
it('3. Re-register spec, verify marks become live (classify clean)', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('my-mark')) let doc = A.from<{ content: string }>({ content: 'hello world' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'my-mark', { start: 0, end: 5 }, {}) }) // Unregister then re-register registry.unregisterPluginSpecs('test-plugin') registry.register('plugin:test-plugin', makePluginSpec('my-mark')) const { doc: newDoc, report } = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' } }) expect(report.converged).toBe(true) expect(report.errors).toHaveLength(0) const marks = A.marks(newDoc as A.Doc<unknown>, ['content']) expect(marks).toHaveLength(1) })
it('4a. Register a spec with spliceScopeOutsideMark: 1000 → registration rejected', () => { const registry = new ConstructRegistry() expect(() => registry.register('plugin:test-plugin', makePluginSpec('bad', { spliceScopeOutsideMark: 1000 })) ).toThrow(RegistrationError) })
it('4b. Register a spec with maxClaimRange: 257 → registration rejected (plugin cap)', () => { const registry = new ConstructRegistry() expect(() => registry.register('plugin:test-plugin', makePluginSpec('bad-claim', { maxClaimRange: 257 })) ).toThrow(RegistrationError) })
it('4c. Core spec with maxClaimRange 4096 is allowed (category cap preserved for core)', () => { const registry = new ConstructRegistry() expect(() => registry.register('core', makePluginSpec('core-big', { maxClaimRange: 4096, categories: ['inline'] })) ).not.toThrow() })
it('4d. Plugin spec missing classify → registration rejected', () => { const registry = new ConstructRegistry() const bad = { ...makePluginSpec('incomplete'), classify: undefined } as unknown as ConstructSpec expect(() => registry.register('plugin:test-plugin', bad)).toThrow(RegistrationError) })
it('5. Plugin spec throws during classify → spec disabled for current pass, mark preserved', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('bomb', { classify: () => { throw new Error('classify boom') }, })) let doc = A.from<{ content: string }>({ content: 'hello world' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'bomb', { start: 0, end: 5 }, {}) }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const { report, doc: newDoc } = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' } }) expect(report.disabledThisPass).toContain('bomb') expect(report.errors.some(e => e.specName === 'bomb' && e.phase === 'classify')).toBe(true) const marks = A.marks(newDoc as A.Doc<unknown>, ['content']) expect(marks).toHaveLength(1) expect(marks[0].name).toBe('bomb') warn.mockRestore() })
it('5b. Plugin spec throws during rewriteForLocalState → spec disabled for current pass, mark preserved', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('rewrite-bomb', { rewriteForLocalState: () => { throw new Error('rewrite boom') }, })) let doc = A.from<{ content: string }>({ content: 'hello world' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'rewrite-bomb', { start: 0, end: 5 }, {}) }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const { report, doc: newDoc } = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' } }) expect(report.disabledThisPass).toContain('rewrite-bomb') expect(report.errors.some(e => e.specName === 'rewrite-bomb' && e.phase === 'rewrite')).toBe(true) const marks = A.marks(newDoc as A.Doc<unknown>, ['content']) expect(marks).toHaveLength(1) expect(marks[0].name).toBe('rewrite-bomb') warn.mockRestore() })
it('5c. Plugin spec throws during synthesizeFromText → spec disabled for current pass', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('synth-bomb', { synthesizeFromText: () => { throw new Error('synth boom') }, })) let doc = A.from<{ content: string }>({ content: 'hello world' }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const { report } = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' } }) expect(report.disabledThisPass).toContain('synth-bomb') expect(report.errors.some(e => e.specName === 'synth-bomb' && e.phase === 'synthesize')).toBe(true) warn.mockRestore() })
it('6. Plugin spec throws on 5 consecutive passes → user notification surfaces', () => { const registry = new ConstructRegistry() registry.register('plugin:test-plugin', makePluginSpec('repeat-bomb', { classify: () => { throw new Error('boom') }, })) let doc = A.from<{ content: string }>({ content: 'x' }) doc = A.change(doc, d => { emitMark(d as unknown as A.Doc<FrameworkDoc>, registry, 'repeat-bomb', { start: 0, end: 1 }, {}) }) const notify = vi.fn() const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) for (let i = 0; i < 5; i++) { doc = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' }, onPluginNotification: notify }).doc } expect(notify).not.toHaveBeenCalled() doc = reconcile(doc, registry, { docConfig: { canonicalize: 'standard' }, onPluginNotification: notify }).doc expect(notify).toHaveBeenCalledTimes(1) expect(notify).toHaveBeenCalledWith(expect.objectContaining({ specName: 'repeat-bomb', consecutiveFailures: 5, })) warn.mockRestore() })})