fix(#260): open MCP collab docs by canonical UUID (slugId doc-name split) #265

Merged
vvzvlad merged 2 commits from fix/260-collab-docname-slugid into develop 2026-06-30 11:20:52 +03:00

2 Commits

Author SHA1 Message Date
claude code agent 227
e04afee629 test(#260): cover replaceImage's UUID lock-key invariant; drop dead cache line
Reviewer round 1 on the #260 collab-doc-name fix:

- F1: replaceImage is the one path where the resolved UUID gates BOTH the
  collab-doc open AND the per-page mutex key (withPageLock(pageUuid)). Add a
  deterministic test to resolve-page-id-collab-doc-name.test.mjs: it gates
  /files/upload so replaceImage parks mid-upload holding its lock, asserts the
  doc opened as page.<uuid> (never page.<slug>), and probes the SHARED
  page-lock chain — a withPageLock(UUID) probe must stay blocked while
  replaceImage holds it (with a free-key probe as a non-vacuity guard). The
  test fails if the lock key is reverted to the slugId (verified).
- F2: drop the dead `pageIdCache.set(uuid, uuid)` — resolvePageId returns on
  the isUuid() short-circuit before the cache is ever read with a uuid key, so
  only slugId->uuid entries are stored/read. Comment corrected to match.

MCP suite 430/430, tsc 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:46:07 +03:00
claude code agent 227
3b80285d57 fix(#260): open MCP collab docs by canonical UUID (slugId doc-name split)
Real root cause of the silent MCP edit loss: the web editor always opens the
collaboration document by the page UUID (`page.${page.id}`), but the MCP
opened it by the agent-supplied id — usually a slugId — so `page.${pageId}`
became `page.<slugId>`. For one DB page that is TWO independent Yjs documents;
both persist to the same `pages` row (findById/updatePage resolve id or
slugId), so the human tab's debounced store overwrites the agent edit
(last-store-wins) — gone after reload, never shown live. The slugId doc also
made the server's transclusion sync + embedding reindex throw Postgres 22P02.

Fix:
- MCP (primary): resolvePageId(pageId) returns the canonical UUID — a UUID
  short-circuits with no network call, a slugId resolves once via getPageRaw
  and is cached both ways. Every collab-write path (mutatePageContent /
  updatePageContentRealtime / replacePageContent and the mutate/replace/
  unlocked seams) now opens by the resolved UUID, so the MCP and the editor
  share ONE Yjs doc. replaceImage's whole-operation page lock also keys on the
  UUID so it serializes against the other (now-UUID-keyed) writes.
- Server (defense + kills the 22P02 noise): onStoreDocument passes the resolved
  page.id — not the raw doc-name id — to syncTransclusion, the embedding queue,
  the mention-notification job, addContributors, and the in-tx history read.
  Content store and the empty-guard are untouched.

Tests: a new MCP test stands up a real Hocuspocus server and asserts a slugId
input opens `page.<uuid>` (never `page.<slugId>`), with UUID short-circuit and
single-resolve caching; the server spec asserts the side-effects receive the
UUID for a `page.<slugId>` doc. closes #260

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:04:49 +03:00