import { describe, expect, it } from 'vitest' import { seedGraph } from './fixtures' import { HttpError } from './graph' const kids = (g: ReturnType, id: string) => g.children(id).map((e) => e.to) describe('mock graph /api/edit semantics', () => { it('insert at an index, and its inverse removes it again', () => { const g = seedGraph() const r = g.edit([{ op: 'insert', parent: 's1', type: 'text', index: 1, data: { body: 'x' } }]) expect(kids(g, 's1')).toEqual(['h1', r.results[0].node, 't1', 'b1']) g.edit(r.inverse) expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1']) expect(g.nodes.has(r.results[0].node!)).toBe(false) }) it('set merges, null deletes, inverse restores (including absent keys)', () => { const g = seedGraph() const r = g.edit([{ op: 'set', id: 'h1', data: { text: 'New', extra: 1, level: null } }]) expect(g.node('h1').data).toEqual({ text: 'New', extra: 1 }) g.edit(r.inverse) expect(g.node('h1').data).toEqual({ text: 'Edit this headline', level: 1 }) }) it('rejects a stale version with 409 and lifecycle keys with 400', () => { const g = seedGraph() g.edit([{ op: 'set', id: 'h1', data: { text: 'a' } }]) expect(() => g.edit([{ op: 'set', id: 'h1', data: { text: 'b' }, version: 1 }])).toThrow(expect.objectContaining({ status: 409 })) expect(() => g.edit([{ op: 'set', id: 'h1', data: { status: 'x' } }])).toThrow(HttpError) }) it('move reorders and re-parents; inverse puts it back', () => { const g = seedGraph() const r = g.edit([{ op: 'move', edge: 'e4', parent: 's2', index: 0 }]) expect(kids(g, 's2')).toEqual(['b1', 't2']) expect(kids(g, 's1')).toEqual(['h1', 't1']) g.edit(r.inverse) expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1']) expect(kids(g, 's2')).toEqual(['t2']) }) it('refuses cycles', () => { const g = seedGraph() g.edit([{ op: 'insert', parent: 's1', type: 'section', id: 'inner' }]) expect(() => g.edit([{ op: 'move', edge: 'e1', parent: 'inner' }])).toThrow(/cycle/) }) it('delete takes the subtree; the inverse (restore) brings it back verbatim', () => { const g = seedGraph() const r = g.edit([{ op: 'delete', edge: 'e1' }]) expect(g.nodes.has('s1')).toBe(false) expect(g.nodes.has('h1')).toBe(false) g.edit(r.inverse) expect(kids(g, 'p1')).toEqual(['s1', 's2', 'l1', 'f1']) expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1']) }) it('duplicate deep-copies right after the original', () => { const g = seedGraph() const r = g.edit([{ op: 'duplicate', edge: 'e1' }]) const copy = r.results[0].node! expect(kids(g, 'p1')).toEqual(['s1', copy, 's2', 'l1', 'f1']) expect(kids(g, copy)).toHaveLength(3) }) it('a failing op rolls the whole batch back and names the index', () => { const g = seedGraph() const before = JSON.stringify([...g.nodes]) expect(() => g.edit([{ op: 'set', id: 'h1', data: { text: 'changed' } }, { op: 'delete', edge: 'nope' }])).toThrow(/op 1/) expect(JSON.stringify([...g.nodes])).toBe(before) }) })