cellajs/cella/two-tab-sync-test

Claude Code and agent skill by cellajs/cella

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Drive two signed-in browser tabs or two users with playwright to verify or diagnose realtime sync at runtime, for entity changes (CDC → SSE → fetch prioritizer → cache patch) and for collaborative descriptions (Yjs relay → saved row, outside writes, offline, a relay outage).

Install

npx vskill@latest install cellajs/cella/two-tab-sync-test

SKILL.md

Two-tab realtime sync testing

Use when a live-sync symptom needs runtime evidence ("create doesn't show up in the other tab", "typing doesn't reach the other editor", "offline edits are gone") or to verify sync changes end-to-end. Two drivers, each a fixed matrix with per-tab console, network evidence and screenshots:

  • two-tab-driver.mjs: one user in two tabs. Rename control, create, delete-fresh, delete-preseeded, description typing and save, reload truth-check.
  • description-driver.mjs: two users (and a third who may only read) on one description. Cursors, saves while typing, an outside write, offline from storage, a relay outage, sign-out with unsynced edits, two offline tabs, a delete under unsaved edits.

Preconditions

  • A healthy dev stack in the checkout under test, its stdout captured (verify, Launch). The driver reads that checkout's URL and cookie name.
  • The description experiment needs the relay (appConfig.services.yjs.enabled, on by default).
  • Postgres probes: psql <DATABASE_ADMIN_URL from backend/.env> (e.g. a row's seq, deleted_at or description after an action).
  • Never act in the org a human is testing in (watch the request log for their active org); pick another seeded org.

Auth: a minted session

The driver mints a session for each user and signs it out when it ends (verify, Drive).

  • Default, the bench user (needs pnpm --filter bench db:seed once): xbench-user-0000@xbench.local is admin of xbench/xbench-org. The seed sets that tenant's attachment quota to unlimited, so every experiment runs there, creates included.
  • Any other seeded org: pick an admin and pass EMAIL and ORG_PATH.

SELECT o.tenant_id, o.slug, m.role, e.email FROM organizations o JOIN memberships m ON m.organization_id = o.id JOIN emails e ON e.user_id = m.user_id WHERE o.slug <> 'xbench-org' ORDER BY o.slug, m.role LIMIT 20; An org at its tenant's attachment quota (appConfig.defaultRestrictions.quotas) answers creates with 403 `restrict_by_app`.

  • The description driver's users: EMAIL and EMAIL_B both edit (bench users 0000 and 0001 by default), EMAIL_VIEWER may read the row and not update it. With the template's permissions that is a member who did not create the row; every bench member is an admin, so the read-only experiment needs another org and is skipped without EMAIL_VIEWER.

Run

From the repo root:

[EMAIL=<email>] [ORG_PATH=<tenantId>/<orgSlug>] OUT_DIR=<scratch> \
node cella/skills/two-tab-sync-test/two-tab-driver.mjs

Writes evidence.json (every console line / range fetch / assert, ms timestamps) and shots/*.png. Tab A acts, tab B observes. Asserts print PASS/FAIL live. The first line, target, names the URL and cookie in use: check it against the stack you mean to test.

[EMAIL=<editor>] [EMAIL_B=<editor>] [EMAIL_VIEWER=<member>] [ORG_PATH=<tenantId>/<orgSlug>] \
[ROW=<name>] [DELETE_ROW=<name>] [ONLY=live,typing,outside,viewer,offline,outage,signout,tabs,delete] \
[RELAY=ws://localhost:<port>] OUT_DIR=<scratch> \
node cella/skills/two-tab-sync-test/description-driver.mjs

Writes description-evidence.json and shots/*.png, about two minutes for the whole matrix. User A acts, user B keeps the description of ROW (the first row by default) open and observes; server truth is read through the API. It ends with the count of passed asserts and exits 1 when one failed.

A relay change in a worktree runs beside the main stack, with no stack of its own: start that checkout's relay alone, pointed at the main backend (BACKEND_INTERNAL_URL=<the main backend's internal URL> pnpm --filter yjs-worker dev; it listens on the worktree's own relay port and, with the same backend/.env values, shares the database and the token key), then run the driver from the main checkout with RELAY=ws://localhost:<that port>. Every editor socket of the run goes to that relay, and the main stack serves the rest.

Reading the evidence: entity changes

EvidenceMeaning
[TabCoordinator] Acquired leader lock in one tab, [AppStream] Not leader, listening to broadcasts only in the otherelection ran; only the leader opens SSE (sse-connect GET; the POST on the same path is catchup)
[handleProductChange] attachment:<action> viewing=<bool> in tab B consolethe change crossed SSE + BroadcastChannel
Echo: patched stx, skipped data fetch in tab Aacting tab suppressed its own event (correct; sourceId is per-tab, uuidv7 at page load)
[CacheOps] Range fetch: <type> after a until b patched N entities + network GET …?afterSeq=a&untilSeq=bprioritizer flushed and fetched
range response items (id, name, deletedAt, seq)payload truth; a range read includes soft-deleted rows, as tombstones without a name
[CatchupProcessor] Away longer than the activity window → cursors reset, synced data refetchedthe device was away longer than appConfig.retention.activityDays (caughtUpAt in syncStore). To test it, move caughtUpAt in the persisted sync store back and reload

Decision rule: no console change line and the API's `cdc` component is not healthy (its reason says why) → the worker (read its log), not the client; no console change line otherwise → SSE/leader/broadcast layer; change but no fetch → fetch prioritizer/cursor (check the org and view seqs in syncStore, frontend/src/query/realtime/sync-store.ts); fetch contains the row but UI unchanged → cache-patch layer (fetchRangeAndPatch in frontend/src/query/realtime/cache-ops.ts).

Became leader, reconnecting... follows whenever a tab gains leadership with no open stream: at startup, or when the leader tab closed (Client). viewing=true marks a tab on the org route, which fetches at once. Chromium throttles a hidden tab's timers to about 1s, so use generous assert windows.

Creates not propagating: check the applyServerEntity applicator in cache-ops.ts first.

Reading the evidence: descriptions

Typing does not ride the entity path. Keystrokes go editor → relay socket (/yjs/<entityId>) → the other editors. The relay saves the row a few seconds after the last keystroke (Save), and the saved row then travels the entity path like a rename.

EvidenceMeaning
relay-http yjs/token 200, then relay-connect ws://…/yjs/<id> in both tabseach editor got a token and joined the document
EXP5 live: the typed text in tab B's editorthe relay broadcasts
relay-http yjs/pull or yjs/pushthe socket is out of reach; the editor syncs over HTTP through the API
EXP5 saved: a range-fetch item with hasWatchedText: truethe relay wrote the row and it crossed CDC → SSE → fetch
descriptionSaved after the reloadserver truth

Decision rule: no `relay-connect` → token or socket (the yjs/token status, the relay process, the /yjs proxy); connected but no live text → relay session and broadcast (yjs/src/sync/relay.ts); live text but no saved row → compaction and materialization (yjs/src/sync/compaction.ts, the backend's /internal/yjs/materialize); saved row fetched but the cell is stale → the cache rule for Yjs-owned fields (registerYjsOwnedFields).

The table cell of an editing tab proves no save: its own editor already patched the cache. Only the range fetch and the reload show the server row.

The description driver's experiments, by their ONLY name:

ExperimentPasses whenA failure points at
livetyped text reaches the peer, its cursor follows a burst of arrow keys to where it ends, and leaves when the sheet closes right after a moverelay broadcast and the awareness rate window (yjs/src/sync/relay.ts)
typingthe server row holds the text while nonstop typing goes on (about 10s in)the max-wait compaction (yjsCompactMaxWaitMs) and materialization
outsidea description written through the API shows in both open editors, with no reconnect and no toastrecordYjsOutsideWrite and the relay's log listener
viewera user without update rights gets the static only: no token request, no socketcanEdit in collaborative-blocknote.tsx
offlinea stored document opens editable offline, keeps its edit across a reopen, and syncs once online with no noticeyjs-store.ts, and the load before connect in yjs-connections.ts
outagethe limited-sync status after about 5s, edits crossing over HTTP both ways, the socket back with no reloadyjs-http.ts and the transport switch in yjs-connections.ts
signoutthe sign-out page lists the unsynced document, "keep editing" stays signed in, the edit saves once a server is in reachmodules/auth/unsaved-edits-dialog.tsx, the resume of stored documents
tabstwo offline tabs of one user show each other's edits, and both reach the server rowyjs-tab-channel.ts
deletea row deleted under unsaved edits brings the copy-or-discard notice and the deleted statusunsaveable-notices.tsx, the relay's 4410 close and the routes' 404

relay-http lines with route pull or push during outage are the HTTP transport at work; the same lines in any other experiment mean the socket failed there.

Gotchas

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
5

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/cellajs/cella/two-tab-sync-test/badge)](https://verified-skill.com/skills/cellajs/cella/two-tab-sync-test)

Something wrong with this skill or its page? Tell us.