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
This commit is contained in:
Jared Miller 2026-01-27 16:26:29 -05:00
parent 7e92cf251a
commit e5cb351a1a
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

190
adapters/vim/bridge.test.ts Normal file
View file

@ -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<typeof Bun.serve>;
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);
});
});