# 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=`. - `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=` 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 ` 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. ```bash # 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', '') # 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//` is a real directory of files (markdown, images, subfolders) that gets copied into a fresh workspace under `.dev-state/pushwork-sync/workspaces//` 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//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// One per fixture; pushwork-managed ``` `test:pushwork-sync:server:wipe` deletes the entire state root. `test:pushwork-sync:fixture reset ` 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 -rf`s `.dev-state/pushwork-sync/`. Use to start absolutely fresh. ### `test:pushwork-sync:fixture init ` Refuses to overwrite an existing workspace; pass `--force` to clobber. Creates a workspace at `.dev-state/pushwork-sync/workspaces//` from `fixtures//`, runs `pushwork init` against the local sync server, captures the root URL. ### `test:pushwork-sync:fixture reset ` Equivalent to `init --force`. The convenience verb. ### `test:pushwork-sync:fixture sync ` Runs `pushwork sync` in the workspace. Use after editing files on disk. ### `test:pushwork-sync:fixture url ` 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:` 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: ```bash # 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.