Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380import { describe, expect, it } from "vitest";import { BridgeCore, type BridgeOptions } from "../../bridge/core";import { CLOSE, PROTOCOL_VERSION } from "../../shared/protocol";import { FakeTimers } from "../helpers/fakeSocket";import { FakeBridgeSocket, FakeTransport } from "../helpers/fakeTransport";
const flush = () => new Promise((r) => setTimeout(r, 0));const hello = (extra: object = {}) => ({ type: "hello", extVersion: "0.1.0", chrome: "155", profile: "p", ...extra });
async function setup(options: BridgeOptions = {}) { const transport = new FakeTransport(4567); const timers = new FakeTimers(); const core = new BridgeCore(transport, { pingIntervalMs: 1000, helloTimeoutMs: 500, token: "t".repeat(32), runId: "run-1", ...options }, { setTimer: timers.setTimer, clearTimer: timers.clearTimer, setRepeating: timers.setRepeating, clearRepeating: timers.clearRepeating, now: () => timers.now, }); await core.listen(options.port ?? 0); const connect = (token: string | null = core.token) => transport.connect(token); /** A connected, welcomed extension. */ const extension = (extra: object = {}) => { const s = connect(); s.receive(hello(extra)); return s; }; return { core, transport, timers, connect, extension };}
describe("BridgeCore: start", () => { it("listens on the port it is given and reports the one the transport chose", async () => { const t = await setup({ port: 1234 }); expect(t.transport.listenedOn).toBe(1234); expect(t.core.port).toBe(4567); expect(t.core.endpoint).toBe("ws://127.0.0.1:4567/ext"); }); it("makes a token and a run id when none are given", async () => { const transport = new FakeTransport(); const core = new BridgeCore(transport); expect(core.token).toMatch(/^[0-9a-f]{32}$/); expect(core.runId).toMatch(/^[0-9T-]+Z-[0-9a-f]{4}$/); expect(core.connected).toBe(false); expect(core.peer).toBeUndefined(); });});
describe("BridgeCore: token (P1-2.a)", () => { it.each([["a wrong token", "x".repeat(32)], ["a short token", "abc"], ["no token", null]])("closes a connection with %s using 4401 and says nothing", async (_n, token) => { const t = await setup(); const s = t.connect(token); expect(s.closedWith?.code).toBe(CLOSE.badToken); expect(s.sent).toEqual([]); s.receive(hello()); expect(t.core.connected).toBe(false); });});
describe("BridgeCore: handshake", () => { it("welcomes a hello with the run id and protocol version", async () => { const t = await setup(); const s = t.extension(); expect(s.messages).toEqual([{ type: "welcome", runId: "run-1", protocol: PROTOCOL_VERSION }]); expect(t.core.connected).toBe(true); expect(t.core.peer).toEqual({ extVersion: "0.1.0", chrome: "155", profile: "p", resumed: false }); }); it("notes a resume", async () => { const t = await setup(); t.extension({ resume: "run-1" }); expect(t.core.peer?.resumed).toBe(true); }); it("closes a connection that does not say hello in time (4408)", async () => { const t = await setup(); const s = t.connect(); t.timers.advance(499); expect(s.closedWith).toBeUndefined(); t.timers.advance(1); expect(s.closedWith?.code).toBe(CLOSE.helloTimeout); }); it("does not time out a connection that did say hello", async () => { const t = await setup({ pingIntervalMs: 100_000 }); const s = t.extension(); t.timers.advance(5000); expect(s.closedWith).toBeUndefined(); }); it("ignores everything before the hello", async () => { const t = await setup(); const s = t.connect(); s.receive({ type: "pong" }); s.receive({ type: "ping" }); s.receive({ id: 1, ok: true }); s.receive({ type: "event", kind: "x" }); s.receive("{ nope"); expect(s.sent).toEqual([]); expect(t.core.connected).toBe(false); }); it("ignores a second hello on a connection that already said one", async () => { const t = await setup(); const s = t.extension(); s.receive(hello({ profile: "other" })); expect(s.messages).toHaveLength(1); expect(t.core.peer?.profile).toBe("p"); }); it("lets a newer connection replace the old one, which is closed with 4409", async () => { const t = await setup(); const first = t.extension(); const second = t.extension({ profile: "second" }); expect(first.closedWith?.code).toBe(CLOSE.replaced); expect(t.core.peer?.profile).toBe("second"); first.finishClose(); // the old socket finishing closing must not disconnect the new one expect(t.core.connected).toBe(true); expect(second.closedWith).toBeUndefined(); }); it("is disconnected as soon as the connection closes", async () => { const t = await setup(); const s = t.extension(); s.finishClose(); expect(t.core.connected).toBe(false); expect(t.core.peer).toBeUndefined(); }); it("can be waited for, with a time limit", async () => { const t = await setup(); const early = t.core.waitForConnection(1000); t.extension(); await expect(early).resolves.toBeUndefined(); await expect(t.core.waitForConnection(10)).resolves.toBeUndefined(); // already connected const t2 = await setup(); const late = t2.core.waitForConnection(300); const rejected = expect(late).rejects.toThrowError(/no extension connected within 300 ms/); t2.timers.advance(300); await rejected; }); it("a wait that was given up on does not wake later", async () => { const t = await setup(); const late = t.core.waitForConnection(300); const rejected = expect(late).rejects.toThrowError(); t.timers.advance(300); await rejected; expect(() => t.extension()).not.toThrow(); });});
describe("BridgeCore: connection counters (for the soak report)", () => { it("starts at zero", async () => { const t = await setup(); expect(t.core.stats).toEqual({ welcomes: 0, closes: 0 }); }); it("counts each welcome and each connection that ends", async () => { const t = await setup(); const a = t.extension(); expect(t.core.stats).toEqual({ welcomes: 1, closes: 0 }); a.finishClose(); expect(t.core.stats).toEqual({ welcomes: 1, closes: 1 }); t.extension({ resume: "run-1" }); expect(t.core.stats).toEqual({ welcomes: 2, closes: 1 }); }); it("a connection that never said hello is not a welcome and not a close of the extension", async () => { const t = await setup(); const s = t.connect(); s.finishClose(); const bad = t.connect("x".repeat(32)); bad.finishClose(); expect(t.core.stats).toEqual({ welcomes: 0, closes: 0 }); }); it("a replaced connection counts one welcome for the new one and one close for the old", async () => { const t = await setup(); const old = t.extension(); t.extension(); expect(t.core.stats).toEqual({ welcomes: 2, closes: 0 }); // the old socket is closing, not yet closed old.finishClose(); expect(t.core.stats).toEqual({ welcomes: 2, closes: 1 }); }); it("a connection closed because it went silent counts once, not twice when its close event arrives", async () => { const t = await setup({ pingIntervalMs: 1000 }); const s = t.extension(); t.timers.advance(3000); s.finishClose(); expect(t.core.stats).toEqual({ welcomes: 1, closes: 1 }); });});
describe("BridgeCore: keep-alive", () => { it("pings every interval", async () => { const t = await setup({ pingIntervalMs: 1000 }); const s = t.extension(); for (let i = 1; i <= 4; i++) { s.receive({ type: "pong" }); t.timers.advance(1000); expect(s.messages.filter((m) => m.type === "ping")).toHaveLength(i); } expect(t.core.connected).toBe(true); }); it("closes with 4410 after two intervals with no sign of life, and is disconnected at once", async () => { const t = await setup({ pingIntervalMs: 1000 }); const s = t.extension(); t.timers.advance(1000); // ping 1 t.timers.advance(1000); // missed 1 expect(s.closedWith).toBeUndefined(); t.timers.advance(1000); // missed 2 expect(s.closedWith?.code).toBe(CLOSE.silent); expect(t.core.connected).toBe(false); t.timers.advance(200); expect(s.terminated).toBe(true); }); it("counts any message, not just pongs, as a sign of life", async () => { const t = await setup({ pingIntervalMs: 1000 }); const s = t.extension(); for (let i = 0; i < 6; i++) { t.timers.advance(500); s.receive({ type: "event", kind: "console", tab: 1, level: "log", text: "x" }); t.timers.advance(500); } expect(s.closedWith).toBeUndefined(); }); it("answers a ping from the extension", async () => { const t = await setup(); const s = t.extension(); s.receive({ type: "ping" }); expect(s.messages.at(-1)).toEqual({ type: "pong" }); }); it("does nothing when nobody is connected, or the socket is already closing", async () => { const t = await setup({ pingIntervalMs: 1000 }); t.timers.advance(5000); const s = t.extension(); s.readyState = 2; t.timers.advance(5000); expect(s.closedWith).toBeUndefined(); });});
describe("BridgeCore: requests", () => { it("sends a request and returns the response", async () => { const t = await setup(); const s = t.extension(); const r = t.core.request({ cmd: "tabs" }); expect(s.messages.at(-1)).toEqual({ id: 1, cmd: "tabs" }); s.receive({ id: 1, ok: true, tabs: [] }); expect(await r).toEqual({ id: 1, ok: true, tabs: [] }); }); it("holds a request until an extension connects", async () => { const t = await setup(); const r = t.core.request({ cmd: "tabs" }); const s = t.extension(); expect(s.messages.at(-1)).toEqual({ id: 1, cmd: "tabs" }); s.receive({ id: 1, ok: true }); await r; }); it("sends the next request only after the previous is answered", async () => { const t = await setup(); const s = t.extension(); const a = t.core.request({ cmd: "tabs" }); const b = t.core.request({ cmd: "verify" }); expect(s.messages.filter((m) => m.cmd)).toHaveLength(1); s.receive({ id: 1, ok: true }); await a; expect(s.messages.filter((m) => m.cmd)).toHaveLength(2); s.receive({ id: 2, ok: true }); await b; }); it("fails a request the extension never answers, with the time it waited", async () => { const t = await setup({ pingIntervalMs: 100_000 }); t.extension(); const r = t.core.request({ cmd: "verify" }, { timeoutMs: 200 }); t.timers.advance(200); expect(await r).toMatchObject({ ok: false, reason: "timeout", detail: { waitedMs: 200 } }); }); it("passes events to every handler", async () => { const t = await setup(); const a: unknown[] = []; const b: unknown[] = []; t.core.onEvent((e) => a.push(e)); t.core.onEvent((e) => b.push(e)); const s = t.extension(); s.receive({ type: "event", kind: "tab-opened", tab: 5 }); expect(a).toHaveLength(1); expect(b).toHaveLength(1); }); it("ignores a request sent by the extension, a response nobody asked for, and junk", async () => { const t = await setup(); const s = t.extension(); const before = s.sent.length; for (const junk of [{ id: 5, cmd: "act" }, { id: 77, ok: true }, "{ nope", "[1]", { type: "teleport" }, { type: "welcome", runId: "x", protocol: 1 }]) s.receive(junk); expect(s.sent.length).toBe(before); expect(t.core.connected).toBe(true); });});
describe("BridgeCore: delivery across a reconnect (P1-2.c)", () => { it("an act with no answer fails with connection-lost and is not sent again", async () => { const t = await setup(); const first = t.extension(); const act = t.core.request({ cmd: "act", action: "click" }); first.finishClose(); expect(await act).toMatchObject({ ok: false, reason: "connection-lost" }); const second = t.extension({ resume: "run-1" }); expect(second.messages.filter((m) => m.cmd)).toEqual([]); }); it("an idempotent request with no answer is sent again, with the same id", async () => { const t = await setup(); const first = t.extension(); const tabs = t.core.request({ cmd: "tabs" }); first.finishClose(); const second = t.extension({ resume: "run-1" }); expect(second.messages.filter((m) => m.cmd)).toEqual([{ id: 1, cmd: "tabs" }]); second.receive({ id: 1, ok: true, tabs: [] }); expect(await tabs).toMatchObject({ ok: true }); }); it("a request in flight is not lost when a newer connection replaces the old one", async () => { const t = await setup(); t.extension(); const tabs = t.core.request({ cmd: "tabs" }); const second = t.extension(); expect(second.messages.filter((m) => m.cmd)).toEqual([{ id: 1, cmd: "tabs" }]); second.receive({ id: 1, ok: true }); expect(await tabs).toMatchObject({ ok: true }); });});
describe("BridgeCore: close", () => { it("fails waiting requests, closes sockets with 1001 and stops the transport", async () => { const t = await setup(); const s = t.extension(); const pending = t.core.request({ cmd: "verify" }); await t.core.close(); expect(await pending).toMatchObject({ ok: false, reason: "connection-lost", detail: { why: "the bridge was stopped" } }); expect(s.closedWith?.code).toBe(1001); expect(t.transport.closed).toBe(true); }); it("terminates a socket that does not finish closing", async () => { const t = await setup(); const s = t.extension(); const done = t.core.close(); t.timers.advance(300); await done; expect(s.terminated).toBe(true); }); it("is safe to call twice", async () => { const t = await setup(); await t.core.close(); await t.core.close(); }); it("rejects a wait for the extension, and cancels its timer, instead of leaving it hanging", async () => { const t = await setup(); const waiting = t.core.waitForConnection(60_000); const rejected = expect(waiting).rejects.toThrowError("the bridge was stopped"); const before = t.timers.count; await t.core.close(); await rejected; expect(t.timers.count).toBeLessThan(before + 1); // the wait's 60 s timer is gone expect(t.timers.delays).not.toContain(60_000); }); it("refuses new requests afterwards", async () => { const t = await setup(); await t.core.close(); expect(await t.core.request({ cmd: "tabs" })).toMatchObject({ ok: false, reason: "connection-lost" }); }); it("closes a connection that arrives after close", async () => { const t = await setup(); await t.core.close(); const late = t.connect(); expect(late.closedWith?.code).toBe(1001); }); it("stops pinging", async () => { const t = await setup({ pingIntervalMs: 1000 }); const s = t.extension(); await t.core.close(); const sent = s.sent.length; t.timers.advance(5000); expect(s.sent.length).toBe(sent); });});
describe("FakeBridgeSocket sanity (so the tests above test the core, not the fake)", () => { it("send fails on a closed socket", () => { const s = new FakeBridgeSocket(); s.finishClose(); expect(() => s.send("x")).toThrowError(); });});