fix(collab): persist renamed title fragment to page.ydoc (F1, variant C)
A REST/MCP/agent rename (no live editor) wrote the new title to the page.title column, then writePageTitle loaded the ydoc (fragment still OLD) and set it to NEW only in memory. On disconnect onStoreDocument saw titleText(NEW) === column(NEW), took the no-op fast-path, and never persisted the in-memory fragment — so page.ydoc kept the OLD title and a later body edit silently reverted the column back to OLD. writePageTitle now persists the 'title' fragment to page.ydoc DIRECTLY (PersistenceExtension.persistTitleFragmentYdoc) after the transact, bypassing the no-op onStoreDocument. The write carries no treeUpdate, so the tree WS/redis listeners do not re-broadcast (no double broadcast), and it is idempotent/lock-free so it is safe whether or not a live editor is connected. Adds a persist-then-reload-then-edit-body regression test that fails on the old no-op behaviour and passes after the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,15 +85,26 @@ describe('CollaborationGateway.writePageTitle — Redis-independent path', () =>
|
||||
// redisSync is intentionally null — this is the no-Redis scenario.
|
||||
gateway.redisSync = null;
|
||||
gateway.hocuspocus = { openDirectConnection } as any;
|
||||
// F1 (variant C): writePageTitle persists the 'title' fragment directly so a
|
||||
// later body edit can't revert the rename (see title-rename-durability.spec).
|
||||
const persistTitleFragmentYdoc = jest.fn().mockResolvedValue(undefined);
|
||||
gateway.persistenceExtension = { persistTitleFragmentYdoc } as any;
|
||||
|
||||
return { gateway, openDirectConnection, transact, disconnect };
|
||||
return {
|
||||
gateway,
|
||||
openDirectConnection,
|
||||
transact,
|
||||
disconnect,
|
||||
persistTitleFragmentYdoc,
|
||||
};
|
||||
};
|
||||
|
||||
it('writes the new title via openDirectConnection and disconnects', async () => {
|
||||
const doc = new Y.Doc();
|
||||
Y.applyUpdate(doc, Y.encodeStateAsUpdate(buildTitleSeedYdoc('Old Title')));
|
||||
|
||||
const { gateway, openDirectConnection, disconnect } = makeGateway(doc);
|
||||
const { gateway, openDirectConnection, disconnect, persistTitleFragmentYdoc } =
|
||||
makeGateway(doc);
|
||||
|
||||
await gateway.writePageTitle('page-1', 'New Title', { user: { id: 'u1' } });
|
||||
|
||||
@@ -102,6 +113,11 @@ describe('CollaborationGateway.writePageTitle — Redis-independent path', () =>
|
||||
expect.objectContaining({ user: { id: 'u1' } }),
|
||||
);
|
||||
expect(readTitleText(doc)).toBe('New Title');
|
||||
// The renamed fragment is persisted directly to page.ydoc (F1 variant C).
|
||||
expect(persistTitleFragmentYdoc).toHaveBeenCalledWith(
|
||||
'page-1',
|
||||
expect.any(Buffer),
|
||||
);
|
||||
expect(disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user