

























@@ -1,4 +1,5 @@
11import { Buffer } from "node:buffer";
2+import { generateKeyPairSync } from "node:crypto";
23import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
34import type { DeviceIdentity } from "../infra/device-identity.js";
45import { captureEnv } from "../test-utils/env.js";
@@ -188,10 +189,11 @@ type GatewayClientModule = typeof import("./client.js");
188189type GatewayClientInstance = InstanceType<GatewayClientModule["GatewayClient"]>;
189190190191let GatewayClient: GatewayClientModule["GatewayClient"];
192+let isGatewayConnectAssemblyError: GatewayClientModule["isGatewayConnectAssemblyError"];
191193192194async function loadGatewayClientModule() {
193195vi.resetModules();
194-({ GatewayClient } = await import("./client.js"));
196+({ GatewayClient, isGatewayConnectAssemblyError } = await import("./client.js"));
195197}
196198197199function getLatestWs(): MockWebSocket {
@@ -248,10 +250,11 @@ function createClientWithIdentity(
248250onClose: (code: number, reason: string) => void,
249251overrides: Partial<ConstructorParameters<typeof GatewayClient>[0]> = {},
250252) {
253+const { privateKey, publicKey } = generateKeyPairSync("ed25519");
251254const identity: DeviceIdentity = {
252255 deviceId,
253-privateKeyPem: "private-key", // pragma: allowlist secret
254-publicKeyPem: "public-key",
256+privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }),
257+publicKeyPem: publicKey.export({ type: "spki", format: "pem" }),
255258};
256259return new GatewayClient({
257260url: "ws://127.0.0.1:18789",
@@ -836,6 +839,45 @@ describe("GatewayClient close handling", () => {
836839});
837840});
838841842+describe("GatewayClient message dispatch", () => {
843+beforeEach(() => {
844+wsInstances.length = 0;
845+logDebugMock.mockClear();
846+});
847+848+it("keeps event callback errors inside message dispatch", () => {
849+const onEvent = vi.fn(() => {
850+throw new Error("event callback failed");
851+});
852+const client = new GatewayClient({
853+url: "ws://127.0.0.1:18789",
854+deviceIdentity: null,
855+ onEvent,
856+});
857+858+try {
859+client.start();
860+const ws = getLatestWs();
861+862+expect(() =>
863+ws.emitMessage(
864+JSON.stringify({
865+type: "event",
866+event: "tick",
867+payload: {},
868+}),
869+),
870+).not.toThrow();
871+expect(onEvent).toHaveBeenCalledOnce();
872+expect(logDebugMock).toHaveBeenCalledWith(
873+"gateway client event handler error: Error: event callback failed",
874+);
875+} finally {
876+client.stop();
877+}
878+});
879+});
880+839881describe("GatewayClient connect auth payload", () => {
840882beforeEach(() => {
841883vi.useRealTimers();
@@ -927,6 +969,76 @@ describe("GatewayClient connect auth payload", () => {
927969return { ws, connect: connectRequestFrom(ws) };
928970}
929971972+it("surfaces connect assembly errors instead of waiting for the wrapper timeout", async () => {
973+vi.useFakeTimers();
974+let client: GatewayClientInstance | null = null;
975+try {
976+const onClose = vi.fn();
977+const onConnectError = vi.fn();
978+client = new GatewayClient({
979+url: "ws://127.0.0.1:18789",
980+token: "shared-token",
981+deviceIdentity: {
982+deviceId: "bad-device",
983+privateKeyPem: "not a pem",
984+publicKeyPem: "not a pem",
985+},
986+ onClose,
987+ onConnectError,
988+});
989+990+client.start();
991+const ws = getLatestWs();
992+ws.emitOpen();
993+emitConnectChallenge(ws);
994+995+expect(ws.sent.some((frame) => frame.includes('"method":"connect"'))).toBe(false);
996+const error = firstMockArg(onConnectError, "connect error") as Error;
997+expect(error).toBeInstanceOf(Error);
998+expect(error.message).not.toContain("gateway request timeout");
999+expect(isGatewayConnectAssemblyError(error)).toBe(true);
1000+expect(ws.lastClose).toEqual({ code: 1008, reason: "connect failed" });
1001+await vi.advanceTimersByTimeAsync(1_000);
1002+expect(wsInstances).toHaveLength(1);
1003+expect(logErrorMock).toHaveBeenCalledWith(expect.stringContaining("gateway connect failed:"));
1004+expect(logDebugMock).not.toHaveBeenCalledWith(
1005+expect.stringContaining("gateway client parse error:"),
1006+);
1007+} finally {
1008+client?.stop();
1009+vi.useRealTimers();
1010+}
1011+});
1012+1013+it("keeps connect error callback throws inside challenge dispatch", () => {
1014+const onConnectError = vi.fn(() => {
1015+throw new Error("connect callback failed");
1016+});
1017+const client = new GatewayClient({
1018+url: "ws://127.0.0.1:18789",
1019+deviceIdentity: null,
1020+ onConnectError,
1021+});
1022+1023+try {
1024+client.start();
1025+const ws = getLatestWs();
1026+ws.emitOpen();
1027+1028+expect(() => emitConnectChallenge(ws, " ")).not.toThrow();
1029+expect(onConnectError).toHaveBeenCalledOnce();
1030+expect(ws.lastClose).toEqual({
1031+code: 1008,
1032+reason: "connect challenge missing nonce",
1033+});
1034+expect(logDebugMock).toHaveBeenCalledWith(
1035+"gateway client connect error handler error: Error: connect callback failed",
1036+);
1037+} finally {
1038+client.stop();
1039+}
1040+});
1041+9301042function emitConnectFailure(
9311043ws: MockWebSocket,
9321044connectId: string | undefined,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。