| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { describe, it, expect } from "vitest"; |
| import * as Y from "yjs"; |
| import { createAgentBatch } from "../src/editor/agent-undo-batch"; |
|
|
| const AGENT_ORIGIN = Symbol("agent"); |
| const USER_ORIGIN = Symbol("user"); |
|
|
| interface Fixture { |
| ydoc: Y.Doc; |
| body: Y.XmlFragment; |
| frontmatter: Y.Map<unknown>; |
| undoManager: Y.UndoManager; |
| batch: ReturnType<typeof createAgentBatch>; |
| } |
|
|
| function makeFixture(): Fixture { |
| const ydoc = new Y.Doc(); |
| const body = ydoc.getXmlFragment("default"); |
| const frontmatter = ydoc.getMap("frontmatter"); |
|
|
| |
| |
| |
| const undoManager = new Y.UndoManager([body, frontmatter], { |
| captureTimeout: 500, |
| trackedOrigins: new Set([AGENT_ORIGIN, USER_ORIGIN]), |
| }); |
|
|
| return { ydoc, body, frontmatter, undoManager, batch: createAgentBatch(undoManager) }; |
| } |
|
|
| |
| |
| |
| |
| function appendParagraph(ydoc: Y.Doc, body: Y.XmlFragment, text: string, origin: symbol) { |
| ydoc.transact(() => { |
| const p = new Y.XmlElement("paragraph"); |
| p.insert(0, [new Y.XmlText(text)]); |
| body.push([p]); |
| }, origin); |
| } |
|
|
| |
| |
| |
| |
| function rewriteParagraph( |
| ydoc: Y.Doc, |
| body: Y.XmlFragment, |
| index: number, |
| newText: string, |
| origin: symbol, |
| ) { |
| ydoc.transact(() => { |
| const p = body.get(index) as Y.XmlElement; |
| const t = p.get(0) as Y.XmlText; |
| t.delete(0, t.length); |
| t.insert(0, newText); |
| }, origin); |
| } |
|
|
| function bodyAsText(body: Y.XmlFragment): string[] { |
| const out: string[] = []; |
| for (let i = 0; i < body.length; i++) { |
| const node = body.get(i) as Y.XmlElement; |
| const t = node.get(0) as Y.XmlText; |
| out.push(t.toString()); |
| } |
| return out; |
| } |
|
|
| describe("agent-undo-batch — primitive contract", () => { |
| it("startAgentBatch + endAgentBatch are idempotent", () => { |
| const { batch } = makeFixture(); |
|
|
| expect(batch.isActive()).toBe(false); |
| batch.startAgentBatch(); |
| expect(batch.isActive()).toBe(true); |
|
|
| |
| batch.startAgentBatch(); |
| expect(batch.isActive()).toBe(true); |
|
|
| batch.endAgentBatch(); |
| expect(batch.isActive()).toBe(false); |
|
|
| |
| batch.endAgentBatch(); |
| expect(batch.isActive()).toBe(false); |
| }); |
|
|
| it("is a noop when the UndoManager is null", () => { |
| const batch = createAgentBatch(null); |
| expect(() => batch.startAgentBatch()).not.toThrow(); |
| expect(() => batch.endAgentBatch()).not.toThrow(); |
| expect(batch.isActive()).toBe(false); |
| }); |
| }); |
|
|
| describe("agent-undo-batch — 5.3.1 batched undo (P0)", () => { |
| it("reverts 3 chained agent edits with a single undo()", () => { |
| const { ydoc, body, frontmatter, undoManager, batch } = makeFixture(); |
|
|
| |
| |
| |
| appendParagraph(ydoc, body, "User wrote this first.", USER_ORIGIN); |
| expect(bodyAsText(body)).toEqual(["User wrote this first."]); |
|
|
| |
| batch.startAgentBatch(); |
|
|
| |
| rewriteParagraph(ydoc, body, 0, "User wrote this first. (refined by agent)", AGENT_ORIGIN); |
|
|
| |
| appendParagraph(ydoc, body, "Agent added a follow-up.", AGENT_ORIGIN); |
|
|
| |
| ydoc.transact(() => { |
| frontmatter.set("title", "New title from agent"); |
| }, AGENT_ORIGIN); |
|
|
| batch.endAgentBatch(); |
|
|
| |
| expect(bodyAsText(body)).toEqual([ |
| "User wrote this first. (refined by agent)", |
| "Agent added a follow-up.", |
| ]); |
| expect(frontmatter.get("title")).toBe("New title from agent"); |
|
|
| |
| undoManager.undo(); |
|
|
| |
| expect(bodyAsText(body)).toEqual(["User wrote this first."]); |
| expect(frontmatter.get("title")).toBeUndefined(); |
| }); |
|
|
| it("does not lump the previous user edit into the agent step", () => { |
| const { ydoc, body, undoManager, batch } = makeFixture(); |
|
|
| |
| appendParagraph(ydoc, body, "First user paragraph.", USER_ORIGIN); |
| appendParagraph(ydoc, body, "Second user paragraph.", USER_ORIGIN); |
|
|
| batch.startAgentBatch(); |
| appendParagraph(ydoc, body, "Agent paragraph.", AGENT_ORIGIN); |
| batch.endAgentBatch(); |
|
|
| expect(bodyAsText(body)).toEqual([ |
| "First user paragraph.", |
| "Second user paragraph.", |
| "Agent paragraph.", |
| ]); |
|
|
| |
| undoManager.undo(); |
| expect(bodyAsText(body)).toEqual([ |
| "First user paragraph.", |
| "Second user paragraph.", |
| ]); |
| }); |
| }); |
|
|