Files
BrowserSkill/apps/extension/src/transport/__tests__/handshake.test.ts
T
2026-06-22 17:36:26 +08:00

225 lines
6.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
detectBrowserMeta,
EXTENSION_VERSION,
MIN_COMPATIBLE_PEER,
MIN_COMPATIBLE_PROTOCOL,
PROTOCOL_VERSION,
performHandshake,
} from "../handshake";
import type { ConnectionStateHandler, FrameHandler, Transport } from "../transport";
import type { ConnectionState, HandshakeResult, ProtocolFrame } from "../types";
function fakeTransport(reply: (req: ProtocolFrame) => ProtocolFrame | null): Transport {
const handlers = new Set<FrameHandler>();
const t: Transport = {
state: "connected" as ConnectionState,
connect: () => Promise.resolve(),
disconnect: () => Promise.resolve(),
send: (msg) => {
const response = reply(msg);
if (response) {
for (const h of handlers) h(response);
}
},
onMessage: (h: FrameHandler) => {
handlers.add(h);
return {
dispose: () => {
handlers.delete(h);
},
};
},
onConnectionStateChange: (_h: ConnectionStateHandler) => ({
dispose: () => {},
}),
};
return t;
}
function deferredFakeTransport(): { transport: Transport; emit: (frame: ProtocolFrame) => void } {
const handlers = new Set<FrameHandler>();
const transport: Transport = {
state: "connected" as ConnectionState,
connect: () => Promise.resolve(),
disconnect: () => Promise.resolve(),
send: () => {},
onMessage: (h: FrameHandler) => {
handlers.add(h);
return {
dispose: () => {
handlers.delete(h);
},
};
},
onConnectionStateChange: (_h: ConnectionStateHandler) => ({
dispose: () => {},
}),
};
return {
transport,
emit: (frame) => {
for (const h of handlers) h(frame);
},
};
}
describe("performHandshake", () => {
it("sends system.handshake with identity and both compat fields", async () => {
let sentFrame: ProtocolFrame | null = null;
const transport = fakeTransport((req) => {
sentFrame = req;
return {
id: (req as { id: string }).id,
result: {
server: "browser-skill-daemon",
version: "0.1.0",
protocol_version: "1.0",
min_compatible_peer: "0.0.0",
min_compatible_protocol: "1.0",
},
};
});
const outcome = await performHandshake(transport, {
instanceId: "abcdef01-2345-4678-89ab-cdef01234567",
browser: { name: "chrome", version: "131.0" },
label: "Personal Chrome",
rpcId: "hs-test",
});
expect(sentFrame).toEqual({
id: "hs-test",
method: "system.handshake",
params: {
client: "browser-skill-extension",
version: EXTENSION_VERSION,
protocol_version: PROTOCOL_VERSION,
instance_id: "abcdef01-2345-4678-89ab-cdef01234567",
browser: { name: "chrome", version: "131.0" },
label: "Personal Chrome",
min_compatible_peer: MIN_COMPATIBLE_PEER,
min_compatible_protocol: MIN_COMPATIBLE_PROTOCOL,
},
});
expect(outcome.result.min_compatible_peer).toBe("0.0.0");
expect(outcome.result.min_compatible_protocol).toBe("1.0");
});
it("accepts legacy daemon reply with only min_compatible_peer", async () => {
const transport = fakeTransport((req) => ({
id: (req as { id: string }).id,
result: {
server: "browser-skill-daemon",
version: "0.1.0",
protocol_version: "1.0",
min_compatible_peer: "0.1.0",
},
}));
const outcome = await performHandshake(transport, {
instanceId: "x",
browser: { name: "chrome", version: "131" },
label: "",
rpcId: "hs-legacy",
});
expect(outcome.result.protocol_version).toBe("1.0");
expect(outcome.result.min_compatible_protocol).toBeUndefined();
});
it("accepts daemon reply without legacy min_compatible_peer", async () => {
const response = {
server: "browser-skill-daemon",
version: "0.1.0",
protocol_version: "1.0",
min_compatible_protocol: "1.0",
} satisfies HandshakeResult;
const transport = fakeTransport((req) => ({
id: (req as { id: string }).id,
result: response,
}));
const outcome = await performHandshake(transport, {
instanceId: "x",
browser: { name: "chrome", version: "131" },
label: "",
rpcId: "hs-no-legacy-peer",
});
expect(outcome.result.min_compatible_peer).toBeUndefined();
expect(outcome.result.min_compatible_protocol).toBe("1.0");
});
it("rejects when the daemon responds with an error", async () => {
const transport = fakeTransport((req) => ({
id: (req as { id: string }).id,
error: { code: "version_too_old", message: "extension is too old" },
}));
await expect(
performHandshake(transport, {
instanceId: "x",
browser: { name: "chrome", version: "131" },
label: "",
rpcId: "hs-err",
}),
).rejects.toThrow(/version_too_old/);
});
it("ignores unrelated frames and resolves on the matching response id", async () => {
const { transport, emit } = deferredFakeTransport();
const pending = performHandshake(transport, {
instanceId: "x",
browser: { name: "chrome", version: "131" },
label: "",
rpcId: "hs-target",
});
emit({ event: "browser.event", payload: { ignored: true } });
emit({
id: "hs-other",
result: {
server: "browser-skill-daemon",
version: "0.1.0",
protocol_version: "1.0",
min_compatible_peer: "0.0.0",
min_compatible_protocol: "1.0",
},
});
emit({
id: "hs-target",
result: {
server: "browser-skill-daemon",
version: "0.1.0",
protocol_version: "1.0",
min_compatible_peer: "0.0.0",
min_compatible_protocol: "1.0",
},
});
await expect(pending).resolves.toMatchObject({
result: { server: "browser-skill-daemon", protocol_version: "1.0" },
});
});
});
describe("detectBrowserMeta", () => {
it("recognises Chrome", () => {
const meta = detectBrowserMeta(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36",
);
expect(meta).toEqual({ name: "chrome", version: "131.0.6778.86" });
});
it("recognises Edge before the Chrome fallback", () => {
const meta = detectBrowserMeta(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70",
);
expect(meta).toEqual({ name: "edge", version: "131.0.2903.70" });
});
it("falls back to chromium with unknown version for unknown UAs", () => {
expect(detectBrowserMeta("Unknown Browser/1.0")).toEqual({
name: "chromium",
version: "unknown",
});
});
});