Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499import { describe, expect, it } from "vitest";import { BridgeClient, type ClientDeps } from "../../extension/src/bridgeClient";import { CLOSE, MAX_MESSAGE_BYTES } from "../../shared/protocol";import { FakeSocket, FakeTimers } from "../helpers/fakeSocket";
const token = "ab12".repeat(8);const goodConfig = JSON.stringify({ url: "ws://127.0.0.1:4567/ext", token, profile: "test-qian" });const flush = () => new Promise((r) => setTimeout(r, 0));
function setup(over: Partial<ClientDeps> = {}, configText: string | null | (() => string | null) = goodConfig) { const sockets: FakeSocket[] = []; const timers = new FakeTimers(); const badge: string[] = []; const handled: any[] = []; const welcomes: number[] = []; let loads = 0; const deps: ClientDeps = { loadConfigText: async () => (loads++, typeof configText === "function" ? configText() : configText), createSocket: (url) => { const s = new FakeSocket(url); sockets.push(s); return s; }, setTimer: timers.setTimer, clearTimer: timers.clearTimer, badge: { set: async (s) => void badge.push(s) }, handle: async (req) => (handled.push(req), { ok: true, echo: req.cmd }), extVersion: "0.1.0", chrome: "155.0.8059.39", onWelcome: (n) => void welcomes.push(n), ...over, }; const client = new BridgeClient(deps); const last = () => sockets[sockets.length - 1]!; /** Starts, opens and welcomes: the usual happy path. */ const connect = async (runId = "run-1") => { client.start(); await flush(); last().open(); last().receive({ type: "welcome", runId, protocol: 1 }); await flush(); }; return { client, sockets, timers, badge, handled, welcomes, last, connect, loads: () => loads };}
describe("BridgeClient: connecting", () => { it("reads the config and opens the socket to its url with the token", async () => { const t = setup(); t.client.start(); await flush(); expect(t.sockets).toHaveLength(1); expect(t.last().url).toBe(`ws://127.0.0.1:4567/ext?token=${token}`); }); it("says hello when the socket opens, without resume the first time", async () => { const t = setup(); t.client.start(); await flush(); t.last().open(); expect(t.last().messages).toEqual([{ type: "hello", extVersion: "0.1.0", chrome: "155.0.8059.39", profile: "test-qian" }]); }); it("becomes connected on welcome", async () => { const t = setup(); await t.connect(); expect(t.client.state).toBe("connected"); expect(t.badge.at(-1)).toBe("connected"); expect(t.client.runId).toBe("run-1"); }); it("does not open a second socket if start is called twice", async () => { const t = setup(); t.client.start(); t.client.start(); await flush(); expect(t.sockets).toHaveLength(1); }); it("shows disconnected while it is connecting", async () => { const t = setup(); t.client.start(); await flush(); expect(t.badge.at(-1)).toBe("disconnected"); });});
describe("BridgeClient: configuration problems", () => { it("no bridge.json: disconnected, nothing opened, tries again later", async () => { const t = setup({}, null); t.client.start(); await flush(); expect(t.sockets).toHaveLength(0); expect(t.badge.at(-1)).toBe("disconnected"); expect(t.timers.delays).toEqual([500]); }); it("an invalid bridge.json: error badge, nothing opened, tries again later", async () => { const t = setup({}, JSON.stringify({ url: "ws://example.com:1/ext", token })); t.client.start(); await flush(); expect(t.sockets).toHaveLength(0); expect(t.badge.at(-1)).toBe("error"); expect(t.timers.count).toBe(1); }); it("a bridge.json that cannot be read: error badge, tries again later", async () => { const t = setup({ loadConfigText: async () => { throw new Error("fetch failed"); } }); t.client.start(); await flush(); expect(t.badge.at(-1)).toBe("error"); expect(t.timers.count).toBe(1); }); it("picks up a bridge.json that appears later", async () => { let text: string | null = null; const t = setup({}, () => text); t.client.start(); await flush(); text = goodConfig; t.timers.advance(500); await flush(); expect(t.sockets).toHaveLength(1); }); it("re-reads the config on every attempt, so a restarted bridge's new token is used", async () => { let text = goodConfig; const t = setup({}, () => text); await t.connect(); text = JSON.stringify({ url: "ws://127.0.0.1:9999/ext", token: "cd34".repeat(8) }); t.last().drop(); t.timers.advance(500); await flush(); expect(t.last().url).toBe(`ws://127.0.0.1:9999/ext?token=${"cd34".repeat(8)}`); });});
describe("BridgeClient: reconnecting (P1-2.b)", () => { it("retries after a drop at 0.5 s, then 1, 2, 4, 8 and 8 s while it keeps failing", async () => { const t = setup(); await t.connect(); const seen: number[] = []; t.last().drop(); for (let i = 0; i < 6; i++) { seen.push(t.timers.delays[0]!); t.timers.advance(t.timers.delays[0]!); await flush(); t.last().drop(1006); // the bridge is still down } expect(seen).toEqual([500, 1000, 2000, 4000, 8000, 8000]); }); it("is back within 10 s however long the bridge was down once it is up again", async () => { const t = setup(); await t.connect(); t.last().drop(); for (let i = 0; i < 12; i++) { t.timers.advance(t.timers.delays[0]!); await flush(); t.last().drop(1006); } const before = t.sockets.length; t.timers.advance(8000); // the longest wait there can be await flush(); expect(t.sockets.length).toBe(before + 1); }); it("says hello again with the run id it was given (resume)", async () => { const t = setup(); await t.connect("run-7"); t.last().drop(); t.timers.advance(500); await flush(); t.last().open(); expect(t.last().messages[0]).toEqual({ type: "hello", extVersion: "0.1.0", chrome: "155.0.8059.39", profile: "test-qian", resume: "run-7" }); }); it("starts the backoff over after a successful welcome", async () => { const t = setup(); await t.connect(); t.last().drop(); t.timers.advance(500); await flush(); t.last().drop(1006); expect(t.timers.delays).toEqual([1000]); t.timers.advance(1000); await flush(); t.last().open(); t.last().receive({ type: "welcome", runId: "run-1", protocol: 1 }); await flush(); t.last().drop(); expect(t.timers.delays).toEqual([500]); }); it("shows disconnected when the connection drops", async () => { const t = setup(); await t.connect(); t.last().drop(); await flush(); expect(t.badge.at(-1)).toBe("disconnected"); expect(t.client.state).toBe("disconnected"); }); it("counts reconnects: welcomes after the first", async () => { const t = setup(); await t.connect(); expect(t.client.stats).toEqual({ welcomes: 1, reconnects: 0 }); t.last().drop(); t.timers.advance(500); await flush(); t.last().open(); t.last().receive({ type: "welcome", runId: "run-1", protocol: 1 }); await flush(); expect(t.client.stats).toEqual({ welcomes: 2, reconnects: 1 }); expect(t.welcomes).toEqual([1, 2]); }); it("ignores the close of a socket it has already replaced", async () => { const t = setup(); await t.connect(); const old = t.last(); old.drop(); t.timers.advance(500); await flush(); const fresh = t.last(); old.drop(1006); old.receive({ type: "ping" }); expect(t.timers.count).toBe(0); expect(fresh.messages.filter((m) => m.type === "pong")).toEqual([]); });});
describe("BridgeClient: a failed attempt (an error with no close event, as Node's WebSocket does)", () => { it("counts an error before open as the end of the attempt and retries", async () => { const t = setup(); t.client.start(); await flush(); t.last().onerror?.(); expect(t.timers.delays).toEqual([500]); expect(t.client.state).toBe("disconnected"); expect(t.last().closedWith).toBeDefined(); // the dead socket is closed, not left open }); it("keeps retrying on the backoff schedule when every attempt only errors", async () => { const t = setup(); t.client.start(); await flush(); const seen: number[] = []; for (let i = 0; i < 6; i++) { t.last().onerror?.(); seen.push(t.timers.delays[0]!); t.timers.advance(t.timers.delays[0]!); await flush(); } expect(seen).toEqual([500, 1000, 2000, 4000, 8000, 8000]); }); it("an error followed by the close event schedules one retry, not two", async () => { const t = setup(); t.client.start(); await flush(); const s = t.last(); s.onerror?.(); s.drop(1006); expect(t.timers.count).toBe(1); }); it("an error on an open, welcomed connection drops it and retries", async () => { const t = setup(); await t.connect(); t.last().onerror?.(); expect(t.client.state).toBe("disconnected"); expect(t.timers.delays).toEqual([500]); }); it("an error from a socket that was already replaced is ignored", async () => { const t = setup(); await t.connect(); const old = t.last(); old.drop(); t.timers.advance(500); await flush(); old.onerror?.(); expect(t.timers.count).toBe(0); });});
describe("BridgeClient: refusals (P1-2.a)", () => { it("a bad token (4401) shows the error badge, and it keeps trying so a new token is picked up", async () => { const t = setup(); t.client.start(); await flush(); t.last().open(); t.last().drop(CLOSE.badToken); await flush(); expect(t.badge.at(-1)).toBe("error"); expect(t.client.state).toBe("error"); expect(t.timers.count).toBe(1); }); it("a protocol version it does not know: error badge and the socket is closed", async () => { const t = setup(); t.client.start(); await flush(); t.last().open(); t.last().receive({ type: "welcome", runId: "r", protocol: 2 }); await flush(); expect(t.badge.at(-1)).toBe("error"); expect(t.last().closedWith?.code).toBe(CLOSE.protocolMismatch); expect(t.client.state).toBe("error"); }); it("after an error, a later successful welcome clears it", async () => { const t = setup(); t.client.start(); await flush(); t.last().open(); t.last().drop(CLOSE.badToken); t.timers.advance(500); await flush(); t.last().open(); t.last().receive({ type: "welcome", runId: "r", protocol: 1 }); await flush(); expect(t.badge.at(-1)).toBe("connected"); }); it("a replaced connection (4409) means another extension has taken over: error badge, and it does not fight back", async () => { const t = setup(); await t.connect(); t.last().drop(CLOSE.replaced); await flush(); expect(t.badge.at(-1)).toBe("error"); expect(t.client.state).toBe("error"); expect(t.timers.count).toBe(0); const sockets = t.sockets.length; t.client.onAlarm(); await flush(); expect(t.sockets.length).toBe(sockets); }); it("start() after a replacement connects again (a fresh worker is a fresh decision)", async () => { const t = setup(); await t.connect(); t.last().drop(CLOSE.replaced); await flush(); t.client.start(); await flush(); expect(t.sockets).toHaveLength(2); }); it("its own newer connection replacing its stale one is not a replacement: the stale socket's 4409 is ignored", async () => { const t = setup(); await t.connect(); const stale = t.last(); t.last().readyState = 3; // dead without a close event t.client.onAlarm(); await flush(); stale.drop(CLOSE.replaced); await flush(); expect(t.client.state).not.toBe("error"); });});
describe("BridgeClient: messages", () => { it("answers a ping with a pong", async () => { const t = setup(); await t.connect(); t.last().receive({ type: "ping" }); expect(t.last().messages.at(-1)).toEqual({ type: "pong" }); }); it("runs a request through the handler and answers once with the same id", async () => { const t = setup(); await t.connect(); t.last().receive({ id: 7, cmd: "tabs" }); await flush(); expect(t.handled).toEqual([{ id: 7, cmd: "tabs" }]); expect(t.last().messages.filter((m) => m.id === 7)).toEqual([{ id: 7, ok: true, echo: "tabs" }]); }); it("answers an internal failure when the handler throws, and keeps running", async () => { const t = setup({ handle: async () => { throw new Error("boom"); } }); await t.connect(); t.last().receive({ id: 8, cmd: "act" }); await flush(); expect(t.last().messages.at(-1)).toMatchObject({ id: 8, ok: false, reason: "internal", detail: { message: "boom" } }); t.last().receive({ type: "ping" }); expect(t.last().messages.at(-1)).toEqual({ type: "pong" }); }); it("does not let the handler overwrite the id", async () => { const t = setup({ handle: async () => ({ id: 999, ok: true }) }); await t.connect(); t.last().receive({ id: 3, cmd: "tabs" }); await flush(); expect(t.last().messages.at(-1)).toMatchObject({ id: 3, ok: true }); }); it("ignores requests that arrive before the welcome", async () => { const t = setup(); t.client.start(); await flush(); t.last().open(); t.last().receive({ id: 1, cmd: "act" }); await flush(); expect(t.handled).toEqual([]); }); it.each([ ["malformed JSON", "{ nope"], ["a JSON array", "[1]"], ["an unknown type", { type: "teleport" }], ["a response nobody asked for", { id: 5, ok: true }], ["an event", { type: "event", kind: "x" }], ["a hello (the bridge never sends one)", { type: "hello", extVersion: "1", chrome: "1", profile: "p" }], ])("ignores %s", async (_n, message) => { const t = setup(); await t.connect(); const sentBefore = t.last().sent.length; t.last().receive(message); await flush(); expect(t.last().sent.length).toBe(sentBefore); expect(t.handled).toEqual([]); expect(t.client.state).toBe("connected"); }); it("ignores a message over 8 MB", async () => { const t = setup(); await t.connect(); t.last().receive("x".repeat(MAX_MESSAGE_BYTES + 1)); await flush(); expect(t.client.state).toBe("connected"); expect(t.handled).toEqual([]); }); it("does not crash when the socket closes while a response is being sent", async () => { let release: (v: unknown) => void = () => undefined; const t = setup({ handle: () => new Promise((r) => (release = r as never)) }); await t.connect(); t.last().receive({ id: 1, cmd: "tabs" }); await flush(); t.last().drop(); release({ ok: true }); await flush(); expect(t.client.state).toBe("disconnected"); }); it("does not send a response on a newer connection for a request from an older one", async () => { let release: (v: unknown) => void = () => undefined; const t = setup({ handle: () => new Promise((r) => (release = r as never)) }); await t.connect(); t.last().receive({ id: 1, cmd: "tabs" }); await flush(); t.last().drop(); t.timers.advance(500); await flush(); t.last().open(); t.last().receive({ type: "welcome", runId: "run-1", protocol: 1 }); await flush(); release({ ok: true }); await flush(); expect(t.last().messages.filter((m) => m.id === 1)).toEqual([]); });});
describe("BridgeClient: the alarm backup (it wakes a worker that was stopped)", () => { it("connects if nothing is happening", async () => { const t = setup({}, null); t.client.start(); await flush(); // a worker restarts with no pending timer: the alarm is what brings it back t.timers.advance(0); t.client.onAlarm(); await flush(); expect(t.loads()).toBeGreaterThanOrEqual(2); }); it("does nothing while connected", async () => { const t = setup(); await t.connect(); const sockets = t.sockets.length; t.client.onAlarm(); await flush(); expect(t.sockets.length).toBe(sockets); }); it("replaces a socket that is dead but never reported it", async () => { const t = setup(); await t.connect(); t.last().readyState = 3; // closed without an onclose const sockets = t.sockets.length; t.client.onAlarm(); await flush(); expect(t.sockets.length).toBe(sockets + 1); }); it("does not start a second connection while one is being made", async () => { const t = setup(); t.client.start(); await flush(); const sockets = t.sockets.length; t.client.onAlarm(); await flush(); expect(t.sockets.length).toBe(sockets); }); it("does nothing after stop", async () => { const t = setup(); await t.connect(); t.client.stop(); t.client.onAlarm(); await flush(); expect(t.sockets).toHaveLength(1); });});
describe("BridgeClient: stop", () => { it("closes the socket, cancels retries and stays down", async () => { const t = setup(); await t.connect(); t.client.stop(); expect(t.last().closedWith?.code).toBe(1000); t.last().drop(); expect(t.timers.count).toBe(0); expect(t.client.state).toBe("disconnected"); }); it("cancels a retry that was waiting", async () => { const t = setup({}, null); t.client.start(); await flush(); t.client.stop(); expect(t.timers.count).toBe(0); });});