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-serverbinary — provided by this repo's own Nix flake (theautomerge-repo-nixinput). Enter the dev shell (direnv allowornix develop) and it's on$PATH. Override withSYNC_SERVER_BIN=<path>. -
pushwork2.x source build at~/usr/pushwork/dist/cli.js. Build with:cd ~/usr/pushwork && bun install && bun run buildThe harness invokes
cli.jsdirectly via node (resolved fromprocess.execPathorcommand -v node). Override withPUSHWORK_BIN=<path>to use a wrapper script.Pushwork 2.x defaults to the Subduction sync backend; the harness always passes
--legacyso init runs against the classicautomerge-repo-sync-serverthis harness brings up. It also passes--shape patchwork-folderso the root doc keeps the@patchwork:{type:'folder'|'file'}schema Reams expects (the new defaultvfsshape usestype:'directory', which Reams'sisPushworkDirectorytype guard rejects).The npm-global
pushworkfrom a previousnpm install -g pushwork@^1.1.8is not compatible — it expects the now-removed--sync-server <url> <storage-id>flag and connects to a different default server. The harness no longer consultswhich('pushwork')precisely to avoid silently picking up that stale install. -
Reams's own deps installed (
bun installornpm 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 asws://localhost:<port>and exports it to pushwork asPUSHWORK_LEGACY_SERVER.SYNC_SERVER_BIN— override sync-server binary pathPUSHWORK_BIN— override pushwork CLI path (defaults to~/usr/pushwork/dist/cli.jsfrom 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:
pushwork init basic→ fixture files render in the Reams file tree.- Editing a Reams document →
pushwork syncwrites the change to disk. - Writing a new file on disk +
pushwork sync→ Reams picks it up automatically (no reload, no manual refresh). - 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.