Automerge backed Markdown editor for the Atmosphere
README.md

Pushwork Loop Harness #

A local dev harness for testing Reams ↔ Pushwork interoperability. Lets you spin up a sync server, create a Pushwork-managed directory of files against it, and iterate on the interop loop without depending on any public sync infrastructure.

This harness contains no tests. It is plumbing — service lifecycle, fixture management, smoke commands. Tests against this harness will land separately under tests/.

What it does #

The harness manages three pieces, kept in three separate processes:

Piece What Owner
Sync server Local automerge-repo-sync-server on ws://localhost:3030 this harness
Reams (Vite dev) The editor itself on http://localhost:7002 you
Pushwork workspace A real directory of files synced to the local sync server via pushwork init this harness

The harness does not manage Vite. You run bun run dev yourself in a second terminal, with the right env var. Why: Vite has loud, useful output, HMR depends on stdio you actually see, and the harness shouldn't fight with your existing dev session.

Prerequisites #

  • automerge-repo-sync-server binary — provided by this repo's own Nix flake (the automerge-repo-nix input). Enter the dev shell (direnv allow or nix develop) and it's on $PATH. Override with SYNC_SERVER_BIN=<path>.

  • pushwork 2.x source build at ~/usr/pushwork/dist/cli.js. Build with:

    cd ~/usr/pushwork && bun install && bun run build
    

    The harness invokes cli.js directly via node (resolved from process.execPath or command -v node). Override with PUSHWORK_BIN=<path> to use a wrapper script.

    Pushwork 2.x defaults to the Subduction sync backend; the harness always passes --legacy so init runs against the classic automerge-repo-sync-server this harness brings up. It also passes --shape patchwork-folder so the root doc keeps the @patchwork:{type:'folder'|'file'} schema Reams expects (the new default vfs shape uses type:'directory', which Reams's isPushworkDirectory type guard rejects).

    The npm-global pushwork from a previous npm install -g pushwork@^1.1.8 is not compatible — it expects the now-removed --sync-server <url> <storage-id> flag and connects to a different default server. The harness no longer consults which('pushwork') precisely to avoid silently picking up that stale install.

  • Reams's own deps installed (bun install or npm install).

The loop, end to end #

direnv assigns the current worktree a separate dev port, dev-sync port, and test-sync port. Inspect them with echo "$REAMS_DEV_PORT $REAMS_DEV_SYNC_PORT $REAMS_TEST_SYNC_PORT"; explicit SYNC_SERVER_URL and SYNC_SERVER_PORT overrides remain available when needed. Set all REAMS_*_PORT values to bypass allocation; if overriding REAMS_DEV_PORT, override DEV_SERVER_URL to the same port for browser tests. Automatically allocated ports are always in the 7000–9000 range.

# Terminal A: bring up the sync server. Idempotent, leaves it running.
bun run test:pushwork-sync:server:up

# Create a workspace from the `basic` fixture and tell pushwork to sync it.
# Prints the root Automerge URL and the path to the workspace on disk.
bun run test:pushwork-sync:fixture init basic

# Terminal B: run Vite against the test harness server. `bun run dev` starts
# a separate worktree-local dev sync server, so use the Vite-only command here.
VITE_SYNC_SERVER_URL="ws://localhost:${REAMS_TEST_SYNC_PORT}" bun run dev:vite-only

# Open $DEV_SERVER_URL in a browser.
# Until the in-app "open by vault root URL" affordance lands, you'll need
# to manually set the vault root in DevTools:
#
#   localStorage.setItem('reams:vault-root-url', '<paste root URL here>')
#   location.reload()
#
# After reload, Reams will sync against the local server and you should
# see the fixture files in its file tree.

# Iterate on the loop:
#   Edit files in the workspace dir, then:
bun run test:pushwork-sync:fixture sync basic
#   ...and see changes show up in Reams.
#
#   Edit in Reams; changes flow through the sync server. Then:
bun run test:pushwork-sync:fixture sync basic
#   ...to pull them onto disk.

# When done:
bun run test:pushwork-sync:server:down

Fixtures #

Each fixture under tests/integration/pushwork-sync/fixtures/<name>/ is a real directory of files (markdown, images, subfolders) that gets copied into a fresh workspace under .dev-state/pushwork-sync/workspaces/<name>/ and synced via pushwork init.

Adding a fixture: drop a directory under fixtures/. Do not include a .pushwork/ subdirectory — pushwork init creates that on the workspace copy. (The repo's .gitignore defensively excludes .pushwork/ everywhere.)

State #

All runtime state lives under .dev-state/pushwork-sync/ (gitignored):

.dev-state/pushwork-sync/
├── server.pid             PID of the running sync server (if any)
├── server.cmdline         /proc/<pid>/cmdline at startup (PID-recycling guard)
├── storage-id             Storage ID the server printed at startup
├── server.log             Sync server stdout/stderr
├── data/                  Sync server's persistent Automerge storage
└── workspaces/<name>/     One per fixture; pushwork-managed

test:pushwork-sync:server:wipe deletes the entire state root. test:pushwork-sync:fixture reset <name> recreates a single workspace without touching the sync server's storage.

Commands #

test:pushwork-sync:server:up #

Idempotent. Starts the sync server, waits for it to print its storage ID, records PID + storage ID. No-op if it's already running.

test:pushwork-sync:server:down #

SIGTERM the server, falling back to SIGKILL after a grace period. Removes the PID file. Persistent storage in data/ is preserved.

test:pushwork-sync:server:status #

Prints whether the server is running, its URL, storage ID, uptime, log location, and the disk usage of the data dir.

test:pushwork-sync:server:logs [-f] #

Prints the server log. -f follows.

test:pushwork-sync:server:wipe #

Stops the server and rm -rfs .dev-state/pushwork-sync/. Use to start absolutely fresh.

test:pushwork-sync:fixture init <name> #

Refuses to overwrite an existing workspace; pass --force to clobber. Creates a workspace at .dev-state/pushwork-sync/workspaces/<name>/ from fixtures/<name>/, runs pushwork init against the local sync server, captures the root URL.

test:pushwork-sync:fixture reset <name> #

Equivalent to init <name> --force. The convenience verb.

test:pushwork-sync:fixture sync <name> #

Runs pushwork sync in the workspace. Use after editing files on disk.

test:pushwork-sync:fixture url <name> #

Prints the root Automerge URL of an initialized workspace.

test:pushwork-sync:fixture status #

Lists known workspaces and whether each is initialized.

Environment #

  • SYNC_SERVER_URL — full ws:// URL for the local sync server. If set, the port is derived from it (SYNC_SERVER_PORT, if also set, must agree).
  • SYNC_SERVER_PORT — port to bind (default 3030). The harness builds the URL as ws://localhost:<port> and exports it to pushwork as PUSHWORK_LEGACY_SERVER.
  • SYNC_SERVER_BIN — override sync-server binary path
  • PUSHWORK_BIN — override pushwork CLI path (defaults to ~/usr/pushwork/dist/cli.js from the local pushwork 2.x source build).
  • PUSHWORK_LEGACY_SERVER — pushwork 2.x reads this for the legacy (--no-sub) WebSocket URL. The harness exports it automatically per call, but you can override it (e.g. to point pushwork at a different server than the one this harness started).
  • PUSHWORK_SYNC_STATE_ROOT — override state root location

Automated round-trip test #

The tests/integration/pushwork-sync/e2e/ Vitest suite drives this harness in Chromium under the existing browser-test gate. It covers three round-trip scenarios plus a runtime hot-swap of the sync-server URL:

  1. pushwork init basic → fixture files render in the Reams file tree.
  2. Editing a Reams document → pushwork sync writes the change to disk.
  3. Writing a new file on disk + pushwork sync → Reams picks it up automatically (no reload, no manual refresh).
  4. Changing the sync server URL at runtime (via the Settings-UI entry point) hot-swaps the WebSocket adapter without reloading the page.

To run it locally:

# Terminal A: dev server (used for HMR + Playwright target)
bun run dev

# Terminal B: run the test
RUN_BROWSER_TESTS=1 \
PLAYWRIGHT_CHROMIUM_BIN=$(command -v chromium) \
bun run test:pushwork-sync:e2e

The suite is gated on RUN_BROWSER_TESTS=1 + PLAYWRIGHT_CHROMIUM_BIN (matching the invariants lane), and skips otherwise. Total runtime is ≈3 minutes; the two pushwork sync calls dominate.

Future hooks (out of scope for this harness) #

These belong to follow-up work in chainlink #402 (Typed Documents) and #406 (file routing):

  • In-app "open vault root by URL" affordance. Until that lands, you set localStorage manually as shown above.
  • Smoke commands that connect a small node client to the sync server and verify what Pushwork wrote (without involving a browser). Useful when diagnosing whether interop bugs are server-side or browser-side. Easy to add to this harness later; keeping the surface minimal for now.