



























@@ -75,7 +75,7 @@ let lastClientOptions: {
7575scopes?: string[];
7676deviceIdentity?: unknown;
7777onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise<void>;
78-onClose?: (code: number, reason: string) => void;
78+onClose?: (code: number, reason: string, info?: StubGatewayClientCloseInfo) => void;
7979onConnectError?: (err: Error) => void;
8080} | null = null;
8181let lastRequestOptions: {
@@ -88,7 +88,18 @@ let lastRequestOptions: {
8888onAccepted?: (payload: unknown) => void;
8989};
9090} | null = null;
91-type StartMode = "hello" | "close" | "connect-error" | "silent" | "startup-retry-then-hello";
91+type StartMode =
92+| "hello"
93+| "close"
94+| "connect-error"
95+| "silent"
96+| "startup-retry-then-hello"
97+| "clean-prehello-close-then-hello"
98+| "repeated-clean-prehello-close";
99+type StubGatewayClientCloseInfo = {
100+phase: "pre-hello" | "post-hello";
101+transientPreHelloCleanClose: boolean;
102+};
92103let startMode: StartMode = "hello";
93104let startCalls = 0;
94105let closeCode = 1006;
@@ -119,7 +130,7 @@ vi.mock("./client.js", () => ({
119130approvalRuntimeToken?: string;
120131scopes?: string[];
121132onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise<void>;
122-onClose?: (code: number, reason: string) => void;
133+onClose?: (code: number, reason: string, info?: StubGatewayClientCloseInfo) => void;
123134onConnectError?: (err: Error) => void;
124135}) {
125136lastClientOptions = opts;
@@ -146,6 +157,25 @@ vi.mock("./client.js", () => ({
146157methods: helloMethods,
147158},
148159});
160+} else if (startMode === "clean-prehello-close-then-hello") {
161+lastClientOptions?.onClose?.(1000, "", {
162+phase: "pre-hello",
163+transientPreHelloCleanClose: true,
164+});
165+void lastClientOptions?.onHelloOk?.({
166+features: {
167+methods: helloMethods,
168+},
169+});
170+} else if (startMode === "repeated-clean-prehello-close") {
171+lastClientOptions?.onClose?.(1000, "", {
172+phase: "pre-hello",
173+transientPreHelloCleanClose: true,
174+});
175+lastClientOptions?.onClose?.(1000, "", {
176+phase: "pre-hello",
177+transientPreHelloCleanClose: true,
178+});
149179} else if (startMode === "connect-error") {
150180lastClientOptions?.onConnectError?.(
151181connectError ?? connectAssemblyErrorState.create("device private key invalid"),
@@ -191,7 +221,7 @@ class StubGatewayClient {
191221mode?: string;
192222scopes?: string[];
193223onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise<void>;
194-onClose?: (code: number, reason: string) => void;
224+onClose?: (code: number, reason: string, info?: StubGatewayClientCloseInfo) => void;
195225onConnectError?: (err: Error) => void;
196226}) {
197227lastClientOptions = opts;
@@ -223,6 +253,25 @@ class StubGatewayClient {
223253methods: helloMethods,
224254},
225255});
256+} else if (startMode === "clean-prehello-close-then-hello") {
257+lastClientOptions?.onClose?.(1000, "", {
258+phase: "pre-hello",
259+transientPreHelloCleanClose: true,
260+});
261+void lastClientOptions?.onHelloOk?.({
262+features: {
263+methods: helloMethods,
264+},
265+});
266+} else if (startMode === "repeated-clean-prehello-close") {
267+lastClientOptions?.onClose?.(1000, "", {
268+phase: "pre-hello",
269+transientPreHelloCleanClose: true,
270+});
271+lastClientOptions?.onClose?.(1000, "", {
272+phase: "pre-hello",
273+transientPreHelloCleanClose: true,
274+});
226275} else if (startMode === "connect-error") {
227276lastClientOptions?.onConnectError?.(
228277connectError ?? connectAssemblyErrorState.create("device private key invalid"),
@@ -1362,6 +1411,30 @@ describe("callGateway error details", () => {
13621411expect(lastRequestOptions?.method).toBe("health");
13631412});
136414131414+it("keeps the request alive through one transient pre-hello clean close", async () => {
1415+startMode = "clean-prehello-close-then-hello";
1416+setLocalLoopbackGatewayConfig();
1417+1418+await expect(callGateway({ method: "health" })).resolves.toEqual({ ok: true });
1419+1420+expect(lastRequestOptions?.method).toBe("health");
1421+});
1422+1423+it("surfaces repeated transient pre-hello clean closes", async () => {
1424+startMode = "repeated-clean-prehello-close";
1425+setLocalLoopbackGatewayConfig();
1426+1427+let err: Error | null = null;
1428+try {
1429+await callGateway({ method: "health" });
1430+} catch (caught) {
1431+err = caught as Error;
1432+}
1433+1434+expect(err?.message).toContain("gateway closed (1000 normal closure): no close reason");
1435+expect(lastRequestOptions).toBeNull();
1436+});
1437+13651438it("rejects immediately when the client reports a connect error", async () => {
13661439startMode = "connect-error";
13671440setLocalLoopbackGatewayConfig();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。