test(files): collaborative agent-streaming coverage (two-writer, multi-editor, undo, persist round-trip) (#6199)

* test(files): two-writer concurrent-editing regression coverage for agent streaming

* test(files): multi-editor + undo + persist round-trip + late-joiner streaming integration coverage

* test(files): make full-rewrite two-writer test actually exercise the concurrent peer edit

* test(files): assert every peer edit lands (no false-green from a no-op peerInsertNear)
This commit is contained in:
Waleed
2026-08-02 19:21:18 -07:00
committed by GitHub
parent 5460133e07
commit 2ccda18f3e
2 changed files with 414 additions and 0 deletions
@@ -0,0 +1,214 @@
/**
* @vitest-environment jsdom
*
* Two-writer evaluation: does a PEER editing the shared doc WHILE the agent streams cause corruption,
* clobbering, duplication, or stray empty paragraphs? The agent applies via the real
* `beginAgentStream`/`applyAgentStreamFrame` path (a shadow doc diffed with `updateYFragment`, seeded
* once and never shown the peer's edits). A second editor is wired as a genuine Yjs peer (bidirectional
* update forwarding), so this reproduces the production two-client scenario, not a mock.
*
* Convergence is a hard invariant everywhere (CRDT MUST converge). Peer-edit survival is hard-asserted
* only for the NON-overlapping case (an agent that appends must not clobber an unrelated peer edit); for
* the overlapping case it is diagnostic (CRDT last-writer semantics are acceptable there), so those are
* logged for judgement. Run: bunx vitest run <thisfile> --disable-console-intercept
*/
import { Editor } from '@tiptap/core'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { Awareness } from 'y-protocols/awareness'
import * as Y from 'yjs'
import { createMarkdownEditorExtensions } from '../editor-extensions'
import { parseMarkdownToDoc } from '../markdown-parse'
import { applyAgentStreamFrame, beginAgentStream, endAgentStream } from './apply-streamed-markdown'
beforeAll(() => {
if (!document.elementFromPoint) document.elementFromPoint = () => null
})
function makeCollabEditor() {
const doc = new Y.Doc()
const awareness = new Awareness(doc)
const editor = new Editor({
extensions: createMarkdownEditorExtensions({
placeholder: '',
collaboration: { doc, awareness, user: { name: 'U', color: '#fff', clientId: doc.clientID } },
}),
content: '',
})
return { editor, doc, awareness }
}
const teardown: Array<() => void> = []
afterEach(() => {
for (const fn of teardown.splice(0)) fn()
})
function track(t: { editor: Editor; doc: Y.Doc; awareness: Awareness }) {
teardown.push(() => {
t.editor.destroy()
t.awareness.destroy()
t.doc.destroy()
})
return t
}
/** Wire two Y.Docs as real peers: forward each update to the other, origin-guarded to avoid echo. */
function wirePeers(a: Y.Doc, b: Y.Doc) {
const A2B = Symbol('a->b')
const B2A = Symbol('b->a')
a.on('update', (u: Uint8Array, origin: unknown) => {
if (origin !== B2A) Y.applyUpdate(b, u, A2B)
})
b.on('update', (u: Uint8Array, origin: unknown) => {
if (origin !== A2B) Y.applyUpdate(a, u, B2A)
})
}
/** Seed editor A with markdown (through the real parse), then bring up B as a synced peer. */
function seededPair(markdown: string) {
const A = track(makeCollabEditor())
A.editor.commands.setContent(parseMarkdownToDoc(markdown), { contentType: 'json' })
const B = track(makeCollabEditor())
Y.applyUpdate(B.doc, Y.encodeStateAsUpdate(A.doc))
wirePeers(A.doc, B.doc)
return { A, B }
}
/** A peer edit: insert `text` at the start of the first text node containing `needle`. */
function peerInsertNear(editor: Editor, needle: string, text: string): boolean {
let pos: number | null = null
editor.state.doc.descendants((node, p) => {
if (pos !== null) return false
if (node.isText && node.text?.includes(needle)) pos = p + node.text.indexOf(needle)
})
if (pos === null) return false
return editor.commands.insertContentAt(pos, text)
}
function fragStr(doc: Y.Doc): string {
return doc.getXmlFragment('default').toString()
}
function count(hay: string, needle: string): number {
return hay.split(needle).length - 1
}
function emptyParas(editor: Editor): number {
let n = 0
editor.state.doc.descendants((node) => {
if (node.type.name === 'paragraph' && node.childCount === 0) n++
})
return n
}
describe('two-writer: peer edits while the agent streams', () => {
it('SANITY: peers converge on seed and a plain peer edit with no agent activity', () => {
const { A, B } = seededPair('# Title\n\nAlpha\n\nBeta')
expect(peerInsertNear(B.editor, 'Alpha', 'PEER ')).toBe(true)
expect(fragStr(A.doc)).toBe(fragStr(B.doc))
expect(A.editor.state.doc.textContent).toContain('PEER Alpha')
})
it('NON-OVERLAPPING: agent appends at the bottom while the peer edits the top — peer edit MUST survive', () => {
const { A, B } = seededPair('# Title\n\nAlpha\n\nBeta')
const session = beginAgentStream(A.editor)!
// Frame 1: agent appends Gamma (region far from the peer's target).
applyAgentStreamFrame(A.editor, session, '# Title\n\nAlpha\n\nBeta\n\nGamma')
// Peer edits the TOP paragraph mid-stream (the agent never touches or knows about this).
expect(peerInsertNear(B.editor, 'Alpha', 'PEER ')).toBe(true)
// Frames 2-3: agent keeps appending. Its bodies say "Alpha" (no PEER) — the test is whether the
// (aggressive) updateYFragment re-emits/clobbers the unchanged Alpha paragraph.
applyAgentStreamFrame(A.editor, session, '# Title\n\nAlpha\n\nBeta\n\nGamma\n\nDelta')
applyAgentStreamFrame(
A.editor,
session,
'# Title\n\nAlpha\n\nBeta\n\nGamma\n\nDelta\n\nEpsilon'
)
endAgentStream(session)
const textA = A.editor.state.doc.textContent
console.log(`\n[NON-OVERLAP] A: ${JSON.stringify(textA)}`)
console.log(
`[NON-OVERLAP] converged=${fragStr(A.doc) === fragStr(B.doc)} peerCount=${count(textA, 'PEER ')} emptyParas=${emptyParas(A.editor)}`
)
expect(fragStr(A.doc)).toBe(fragStr(B.doc)) // CRDT convergence
expect(count(textA, 'PEER ')).toBe(1) // peer edit survives, exactly once (no clobber, no dup)
expect(textA).toContain('Epsilon') // agent's stream landed
expect(textA).toContain('Beta') // untouched content intact
expect(emptyParas(A.editor)).toBe(0) // no stray empties from the merge
})
it('POSITION DRIFT: agent inserts a paragraph ABOVE while the peer edits the paragraph BELOW', () => {
// The exact scenario relative-position anchoring is meant to protect: the agent shifts positions by
// inserting content above the region the peer is editing. Without anchoring, an offset-based writer
// would misplace the edit; a whole-doc CRDT diff should not.
const { A, B } = seededPair('# Title\n\nAlpha\n\nBeta')
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(A.editor, session, '# Title\n\nAlpha\n\nMIDDLE\n\nBeta')
// Peer edits Beta, which just shifted down by the agent's inserted MIDDLE paragraph.
expect(peerInsertNear(B.editor, 'Beta', 'PEER ')).toBe(true)
applyAgentStreamFrame(A.editor, session, '# Title\n\nAlpha\n\nMIDDLE\n\nMIDDLE2\n\nBeta')
endAgentStream(session)
const textA = A.editor.state.doc.textContent
console.log(`\n[POS-DRIFT] A: ${JSON.stringify(textA)}`)
console.log(
`[POS-DRIFT] converged=${fragStr(A.doc) === fragStr(B.doc)} peerCount=${count(textA, 'PEER ')} peerOnBeta=${textA.includes('PEER Beta')} emptyParas=${emptyParas(A.editor)}`
)
expect(fragStr(A.doc)).toBe(fragStr(B.doc)) // convergence
expect(count(textA, 'PEER ')).toBe(1) // no duplication
expect(textA).toContain('PEER Beta') // peer edit stayed attached to Beta despite the insert above
expect(textA).toContain('MIDDLE2') // agent's inserts landed
expect(emptyParas(A.editor)).toBe(0)
})
it('OVERLAPPING: agent rewrites the exact paragraph the peer is editing (diagnostic + must converge)', () => {
const { A, B } = seededPair('# Title\n\noriginal body text')
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(A.editor, session, '# Title\n\noriginal body text extended')
// Peer edits the SAME paragraph the agent is rewriting.
expect(peerInsertNear(B.editor, 'original', 'PEER ')).toBe(true)
applyAgentStreamFrame(A.editor, session, '# Title\n\nagent fully rewrote this paragraph')
endAgentStream(session)
const textA = A.editor.state.doc.textContent
console.log(`\n[OVERLAP] A: ${JSON.stringify(textA)}`)
console.log(
`[OVERLAP] converged=${fragStr(A.doc) === fragStr(B.doc)} peerSurvived=${textA.includes('PEER')} emptyParas=${emptyParas(A.editor)}`
)
expect(fragStr(A.doc)).toBe(fragStr(B.doc)) // convergence is non-negotiable even in conflict
expect(emptyParas(A.editor)).toBe(0) // conflict must not leave stray empty paragraphs
// peer survival here is CRDT-dependent — reported above, not hard-asserted.
})
it('FULL REWRITE: peer edits original content that the agent then deletes in a full rewrite', () => {
const { A, B } = seededPair('# Title\n\nAlpha\n\nBeta\n\nGamma')
const session = beginAgentStream(A.editor)!
// Peer edits Beta WHILE it still exists — genuinely concurrent with the impending rewrite.
// (Asserting the insert landed guards against a false-green where the target was already gone.)
expect(peerInsertNear(B.editor, 'Beta', 'PEER ')).toBe(true)
// Agent replaces the WHOLE doc across two frames, deleting Alpha/Beta/Gamma.
applyAgentStreamFrame(A.editor, session, '# Report\n\nOne\n\nTwo')
applyAgentStreamFrame(A.editor, session, '# Report\n\nOne\n\nTwo\n\nThree')
endAgentStream(session)
const textA = A.editor.state.doc.textContent
console.log(`\n[FULL-REWRITE] A: ${JSON.stringify(textA)}`)
console.log(
`[FULL-REWRITE] converged=${fragStr(A.doc) === fragStr(B.doc)} peerCount=${count(textA, 'PEER ')} oneCount=${count(textA, 'One')} threeCount=${count(textA, 'Three')} emptyParas=${emptyParas(A.editor)}`
)
expect(fragStr(A.doc)).toBe(fragStr(B.doc)) // convergence
expect(count(textA, 'One')).toBe(1) // agent content not duplicated by the concurrent merge
expect(count(textA, 'Three')).toBe(1)
expect(emptyParas(A.editor)).toBe(0) // no stray empties from a delete/insert conflict
// The peer's insert is NOT lost when the rewrite deletes its surrounding paragraph: Yjs preserves
// the inserted text and reattaches it to the nearest surviving anchor (it relocates into the
// rewritten content rather than vanishing). What matters is that it survives exactly once — never
// duplicated, never silently dropped.
expect(count(textA, 'PEER ')).toBe(1)
})
})
@@ -0,0 +1,200 @@
/**
* @vitest-environment jsdom
*
* Integration coverage for the collaborative agent-streaming surface with all the moving pieces:
* multiple peers, undo isolation, the durable persist→reopen round-trip, empty-collapse on the live
* streaming path, and a late joiner. Editors are wired as genuine Yjs peers (mesh update forwarding).
* This exercises the CRDT/merge/convert LOGIC deterministically; it does NOT cover the realtime socket
* transport, RAF-paced stream loop, or real browser timing (those need a live 2-browser E2E harness).
* Run: bunx vitest run <thisfile> --disable-console-intercept
*/
import { Editor } from '@tiptap/core'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { Awareness } from 'y-protocols/awareness'
import * as Y from 'yjs'
import { markdownToYDoc, yDocToMarkdown } from '@/lib/collab-doc/converter'
import { createMarkdownEditorExtensions } from '../editor-extensions'
import { parseMarkdownToDoc } from '../markdown-parse'
import { applyAgentStreamFrame, beginAgentStream, endAgentStream } from './apply-streamed-markdown'
beforeAll(() => {
if (!document.elementFromPoint) document.elementFromPoint = () => null
})
const teardown: Array<() => void> = []
afterEach(() => {
for (const fn of teardown.splice(0)) fn()
})
function makeCollabEditor() {
const doc = new Y.Doc()
const awareness = new Awareness(doc)
const editor = new Editor({
extensions: createMarkdownEditorExtensions({
placeholder: '',
collaboration: { doc, awareness, user: { name: 'U', color: '#fff', clientId: doc.clientID } },
}),
content: '',
})
const t = { editor, doc, awareness }
teardown.push(() => {
editor.destroy()
awareness.destroy()
doc.destroy()
})
return t
}
/** Forward every local/agent update from each doc to all others (origin-guarded), a full mesh. */
function wireMesh(docs: Y.Doc[]) {
const MESH = Symbol('mesh')
for (const d of docs) {
d.on('update', (u: Uint8Array, origin: unknown) => {
if (origin === MESH) return
for (const other of docs) if (other !== d) Y.applyUpdate(other, u, MESH)
})
}
}
function peerInsertNear(editor: Editor, needle: string, text: string): boolean {
let pos: number | null = null
editor.state.doc.descendants((node, p) => {
if (pos !== null) return false
if (node.isText && node.text?.includes(needle)) pos = p + node.text.indexOf(needle)
})
if (pos === null) return false
return editor.commands.insertContentAt(pos, text)
}
const fragStr = (doc: Y.Doc) => doc.getXmlFragment('default').toString()
const countText = (hay: string, needle: string) => hay.split(needle).length - 1
function emptyParas(editor: Editor): number {
let n = 0
editor.state.doc.descendants((node) => {
if (node.type.name === 'paragraph' && node.childCount === 0) n++
})
return n
}
describe('collab streaming integration — moving pieces', () => {
it('THREE-WAY: agent + two peers editing different regions all converge, both peer edits survive', () => {
const A = makeCollabEditor()
A.editor.commands.setContent(parseMarkdownToDoc('# Title\n\nAlpha\n\nBeta\n\nGamma'), {
contentType: 'json',
})
const B = makeCollabEditor()
const C = makeCollabEditor()
Y.applyUpdate(B.doc, Y.encodeStateAsUpdate(A.doc))
Y.applyUpdate(C.doc, Y.encodeStateAsUpdate(A.doc))
wireMesh([A.doc, B.doc, C.doc])
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(A.editor, session, '# Title\n\nAlpha\n\nBeta\n\nGamma\n\nDelta')
expect(peerInsertNear(B.editor, 'Alpha', 'B_EDIT ')).toBe(true) // peer B edits the top
expect(peerInsertNear(C.editor, 'Gamma', 'C_EDIT ')).toBe(true) // peer C edits the bottom
applyAgentStreamFrame(
A.editor,
session,
'# Title\n\nAlpha\n\nBeta\n\nGamma\n\nDelta\n\nEpsilon'
)
endAgentStream(session)
const textA = A.editor.state.doc.textContent
console.log(`\n[3-WAY] A: ${JSON.stringify(textA)}`)
console.log(
`[3-WAY] converged=${fragStr(A.doc) === fragStr(B.doc) && fragStr(B.doc) === fragStr(C.doc)} B_EDIT=${countText(textA, 'B_EDIT ')} C_EDIT=${countText(textA, 'C_EDIT ')} empty=${emptyParas(A.editor)}`
)
expect(fragStr(A.doc)).toBe(fragStr(B.doc))
expect(fragStr(B.doc)).toBe(fragStr(C.doc))
expect(countText(textA, 'B_EDIT ')).toBe(1)
expect(countText(textA, 'C_EDIT ')).toBe(1)
expect(textA).toContain('Epsilon')
expect(emptyParas(A.editor)).toBe(0)
})
it('UNDO ISOLATION: a peer undo reverts only the peer’s own edit, never the agent’s stream', () => {
const A = makeCollabEditor()
A.editor.commands.setContent(parseMarkdownToDoc('# Title\n\nbase'), { contentType: 'json' })
const B = makeCollabEditor()
Y.applyUpdate(B.doc, Y.encodeStateAsUpdate(A.doc))
wireMesh([A.doc, B.doc])
expect(peerInsertNear(B.editor, 'base', 'PEER_UNDOABLE ')).toBe(true) // peer's own edit (undo stack)
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(
A.editor,
session,
'# Title\n\nPEER_UNDOABLE base\n\nagent added this line'
)
endAgentStream(session)
const undid = B.editor.commands.undo()
const textB = B.editor.state.doc.textContent
console.log(`\n[UNDO] undoRan=${undid} afterUndo=${JSON.stringify(textB)}`)
expect(fragStr(A.doc)).toBe(fragStr(B.doc)) // still converged after undo
expect(textB).toContain('agent added this line') // agent content NOT undone by the peer
expect(textB).not.toContain('PEER_UNDOABLE') // peer's own edit was undone
})
it('PERSIST ROUND-TRIP: stream → serialize to durable markdown → reopen yields the same content, no empties', () => {
const A = makeCollabEditor()
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(A.editor, session, '# Report\n\n## Section 1\n\nbody one')
applyAgentStreamFrame(
A.editor,
session,
'# Report\n\n## Section 1\n\nbody one\n\n## Section 2\n\nbody two'
)
endAgentStream(session)
const durable = yDocToMarkdown(A.doc) // server-side projection to durable markdown
const reopened = markdownToYDoc(durable) // cold reopen from durable
const reopenedMd = yDocToMarkdown(reopened)
const blankRuns = (durable.match(/\n{3,}/g) ?? []).length
console.log(`\n[ROUND-TRIP] durable=${JSON.stringify(durable)}`)
console.log(`[ROUND-TRIP] reopenStable=${reopenedMd === durable} blankRuns=${blankRuns}`)
expect(durable).toContain('Section 1')
expect(durable).toContain('Section 2')
expect(durable).toContain('body two')
expect(blankRuns).toBe(0) // no pathological blank runs in the persisted markdown
expect(reopenedMd).toBe(durable) // reopen is a fixed point (stable)
reopened.destroy()
})
it('EMPTY-COLLAPSE ON THE STREAM PATH: an agent body with a huge blank run does not strand empties', () => {
const A = makeCollabEditor()
A.editor.commands.setContent(parseMarkdownToDoc('# Title\n\nintro'), { contentType: 'json' })
const session = beginAgentStream(A.editor)!
// The agent emits a pathological blank run between two blocks (the original incident's shape).
applyAgentStreamFrame(A.editor, session, `# Title\n\nintro${'\n'.repeat(400)}tail paragraph`)
endAgentStream(session)
console.log(
`\n[STREAM-COLLAPSE] text=${JSON.stringify(A.editor.state.doc.textContent)} empty=${emptyParas(A.editor)}`
)
expect(emptyParas(A.editor)).toBe(0) // collapse protects the live streaming path, not just static open
expect(A.editor.state.doc.textContent).toContain('tail paragraph')
})
it('LATE JOINER: a peer that syncs AFTER the stream sees the full, clean document', () => {
const A = makeCollabEditor()
A.editor.commands.setContent(parseMarkdownToDoc('# Doc\n\nstart'), { contentType: 'json' })
const session = beginAgentStream(A.editor)!
applyAgentStreamFrame(A.editor, session, '# Doc\n\nstart\n\nstreamed body')
endAgentStream(session)
// A brand-new client joins now and syncs from the current state.
const D = makeCollabEditor()
Y.applyUpdate(D.doc, Y.encodeStateAsUpdate(A.doc))
console.log(
`\n[LATE-JOIN] D: ${JSON.stringify(D.editor.state.doc.textContent)} converged=${fragStr(A.doc) === fragStr(D.doc)}`
)
expect(fragStr(A.doc)).toBe(fragStr(D.doc))
expect(D.editor.state.doc.textContent).toContain('streamed body')
expect(emptyParas(D.editor)).toBe(0)
})
})