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:
parent
7e92cf251a
commit
e5cb351a1a
1 changed files with 190 additions and 0 deletions
190
adapters/vim/bridge.test.ts
Normal file
190
adapters/vim/bridge.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue