From e5cb351a1a2e48bcf8a5f4888e1357e1197547fd Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Tue, 27 Jan 2026 16:26:29 -0500 Subject: [PATCH] Add bridge lifecycle test Tests verify that: - Bridge starts and signals ready message immediately - Bridge connects to daemon via websocket and disconnects cleanly - Bridge handles content synchronization without errors - Process exits with code 0 on clean disconnect --- adapters/vim/bridge.test.ts | 190 ++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 adapters/vim/bridge.test.ts diff --git a/adapters/vim/bridge.test.ts b/adapters/vim/bridge.test.ts new file mode 100644 index 0000000..5b27447 --- /dev/null +++ b/adapters/vim/bridge.test.ts @@ -0,0 +1,190 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { spawn } from "bun"; +import type { Subprocess } from "bun"; + +describe("Bridge lifecycle", () => { + let daemon: ReturnType; + const DAEMON_PORT = 4042; + + beforeEach(async () => { + const { decode } = await import("../../src/protocol"); + const { getOrCreateSession, getSession, removeSession } = await import( + "../../src/session" + ); + + // start daemon for bridge to connect to + daemon = Bun.serve({ + port: DAEMON_PORT, + fetch(req, server) { + const url = new URL(req.url); + if (url.pathname === "/ws") { + const upgraded = server.upgrade(req, { + data: { room: null, client: null }, + }); + if (!upgraded) { + return new Response("websocket upgrade failed", { status: 400 }); + } + return; + } + return new Response("test daemon"); + }, + websocket: { + open(ws) { + const client = { ws }; + ws.data.client = client; + }, + message(ws, raw) { + const msg = decode(raw.toString()); + if (!msg) return; + + const client = ws.data.client; + if (!client) return; + + switch (msg.type) { + case "join": { + const session = getOrCreateSession(msg.room); + ws.data.room = msg.room; + session.join(client); + break; + } + case "leave": { + if (ws.data.room) { + const session = getSession(ws.data.room); + session?.leave(client); + removeSession(ws.data.room); + ws.data.room = null; + } + break; + } + case "update": { + if (ws.data.room) { + const session = getSession(ws.data.room); + session?.applyUpdate(new Uint8Array(msg.data), client); + } + break; + } + } + }, + close(ws) { + if (ws.data.room && ws.data.client) { + const session = getSession(ws.data.room); + session?.leave(ws.data.client); + removeSession(ws.data.room); + } + }, + }, + }); + }); + + afterEach(() => { + daemon.stop(); + }); + + test("bridge starts and signals ready", async () => { + const bridge = spawn({ + cmd: ["bun", "adapters/vim/bridge.ts"], + env: { + ...process.env, + COLLABD_URL: `ws://localhost:${DAEMON_PORT}/ws`, + }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + // read first line from stdout + const reader = bridge.stdout.getReader(); + const decoder = new TextDecoder(); + + const { value } = await reader.read(); + expect(value).toBeDefined(); + + const output = decoder.decode(value); + const firstLine = output.trim().split("\n")[0]; + const msg = JSON.parse(firstLine); + + expect(msg.type).toBe("ready"); + + bridge.kill(); + await bridge.exited; + }); + + test("bridge connects to daemon and disconnects cleanly", async () => { + const bridge = spawn({ + cmd: ["bun", "adapters/vim/bridge.ts"], + env: { + ...process.env, + COLLABD_URL: `ws://localhost:${DAEMON_PORT}/ws`, + }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + const reader = bridge.stdout.getReader(); + const decoder = new TextDecoder(); + + // wait for ready + let { value } = await reader.read(); + expect(value).toBeDefined(); + let output = decoder.decode(value); + expect(output).toContain('"type":"ready"'); + + // send connect message + bridge.stdin.write(JSON.stringify({ type: "connect", room: "test" }) + "\n"); + + // wait for connected message + ({ value } = await reader.read()); + expect(value).toBeDefined(); + output = decoder.decode(value); + expect(output).toContain('"type":"connected"'); + expect(output).toContain('"room":"test"'); + + // send disconnect message + bridge.stdin.write(JSON.stringify({ type: "disconnect" }) + "\n"); + bridge.stdin.end(); + + // bridge should exit cleanly + const exitCode = await bridge.exited; + expect(exitCode).toBe(0); + }); + + test("bridge handles content synchronization", async () => { + const bridge = spawn({ + cmd: ["bun", "adapters/vim/bridge.ts"], + env: { + ...process.env, + COLLABD_URL: `ws://localhost:${DAEMON_PORT}/ws`, + }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + const reader = bridge.stdout.getReader(); + const decoder = new TextDecoder(); + + // wait for ready + await reader.read(); + + // connect to room + bridge.stdin.write(JSON.stringify({ type: "connect", room: "content-test" }) + "\n"); + + // wait for connected and peers messages + await reader.read(); + await reader.read(); + + // send content + bridge.stdin.write(JSON.stringify({ type: "content", text: "hello world" }) + "\n"); + + // give it time to process + await new Promise((r) => setTimeout(r, 100)); + + // disconnect + bridge.stdin.write(JSON.stringify({ type: "disconnect" }) + "\n"); + bridge.stdin.end(); + + const exitCode = await bridge.exited; + expect(exitCode).toBe(0); + }); +});