Skip to content

Commit 712e69d

Browse files
committed
fix(e2e): honor gateway network client deadline
1 parent 13aaece commit 712e69d

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

scripts/e2e/lib/gateway-network/client.mjs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ function delay(ms) {
1111
});
1212
}
1313

14+
function remainingDeadlineMs(deadline) {
15+
return Math.max(1, deadline - Date.now());
16+
}
17+
1418
async function openSocket(url, timeoutMs = 10_000) {
1519
const ws = new WebSocket(url);
1620
await waitForWebSocketOpen(ws, timeoutMs, "ws open timeout");
@@ -68,15 +72,15 @@ export async function runGatewayNetworkClient(
6872
const deadline = Date.now() + timeoutMs;
6973
const delayImpl = deps.delay ?? delay;
7074
const onceFrameImpl = deps.onceFrame ?? onceFrame;
71-
const openSocketImpl = deps.openSocket ?? ((targetUrl) => openSocket(targetUrl));
75+
const openSocketImpl = deps.openSocket ?? openSocket;
7276
const protocolVersion = deps.protocolVersion ?? (await readProtocolVersion());
7377
const stdout = deps.stdout ?? console.log;
7478

7579
let lastError;
7680
while (Date.now() < deadline) {
7781
let ws;
7882
try {
79-
ws = await openSocketImpl(url);
83+
ws = await openSocketImpl(url, remainingDeadlineMs(deadline));
8084
ws.send(
8185
JSON.stringify({
8286
type: "req",
@@ -101,6 +105,7 @@ export async function runGatewayNetworkClient(
101105
const connectRes = await onceFrameImpl(
102106
ws,
103107
(frame) => frame?.type === "res" && frame?.id === "c1",
108+
remainingDeadlineMs(deadline),
104109
);
105110
if (!connectRes.ok) {
106111
lastError = responseError("connect", connectRes);
@@ -112,6 +117,7 @@ export async function runGatewayNetworkClient(
112117
const healthRes = await onceFrameImpl(
113118
ws,
114119
(frame) => frame?.type === "res" && frame?.id === "h1",
120+
remainingDeadlineMs(deadline),
115121
);
116122
if (healthRes.ok) {
117123
if (!hasGatewayHealthSummaryPayload(healthRes)) {
@@ -132,7 +138,10 @@ export async function runGatewayNetworkClient(
132138
ws?.close();
133139
}
134140

135-
await delayImpl(500);
141+
const retryDelayMs = Math.min(500, deadline - Date.now());
142+
if (retryDelayMs > 0) {
143+
await delayImpl(retryDelayMs);
144+
}
136145
}
137146

138147
throw lastError ?? new Error("connect failed: timeout");

test/scripts/gateway-network-client.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Gateway Network Client tests cover gateway network client script behavior.
22
import { EventEmitter } from "node:events";
3-
import { describe, expect, it } from "vitest";
3+
import { describe, expect, it, vi } from "vitest";
44
import { runGatewayNetworkClient } from "../../scripts/e2e/lib/gateway-network/client.mjs";
55
import { readGatewayNetworkClientConnectTimeoutMs } from "../../scripts/e2e/lib/gateway-network/limits.mjs";
66
import { onceFrame } from "../../scripts/e2e/lib/gateway-network/ws-frames.mjs";
@@ -126,7 +126,11 @@ describe("gateway network WebSocket open guard", () => {
126126
stdout,
127127
deps: {
128128
delay: async () => {},
129-
onceFrame: async (_ws: unknown, predicate: (frame: unknown) => boolean) => {
129+
onceFrame: async (
130+
_ws: unknown,
131+
predicate: (frame: unknown) => boolean,
132+
_timeoutMs?: number,
133+
) => {
130134
const frame = {
131135
type: "res",
132136
id: sentMethods.at(-1) === "connect" ? "c1" : "h1",
@@ -157,6 +161,59 @@ describe("gateway network WebSocket open guard", () => {
157161
expect(harness.closeCount).toBe(1);
158162
});
159163

164+
it("bounds socket and frame waits by the client deadline", async () => {
165+
const harness = createNetworkClientHarness([{ ok: true }, healthResponse()]);
166+
const openSocket = vi.fn(harness.deps.openSocket);
167+
const onceFrame = vi.fn(harness.deps.onceFrame);
168+
169+
await runGatewayNetworkClient(
170+
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 },
171+
{
172+
...harness.deps,
173+
onceFrame,
174+
openSocket,
175+
},
176+
);
177+
178+
expect(openSocket.mock.calls[0]?.[1]).toBeGreaterThan(0);
179+
expect(openSocket.mock.calls[0]?.[1]).toBeLessThanOrEqual(250);
180+
expect(onceFrame.mock.calls.map((call) => call[2])).toHaveLength(2);
181+
for (const frameTimeoutMs of onceFrame.mock.calls.map((call) => call[2])) {
182+
expect(frameTimeoutMs).toBeGreaterThan(0);
183+
expect(frameTimeoutMs).toBeLessThanOrEqual(250);
184+
}
185+
});
186+
187+
it("does not sleep past the remaining client deadline between retries", async () => {
188+
const delays: number[] = [];
189+
let now = 1_000;
190+
191+
const dateSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
192+
try {
193+
await expect(
194+
runGatewayNetworkClient(
195+
{ token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 },
196+
{
197+
delay: async (ms: number) => {
198+
delays.push(ms);
199+
now += ms;
200+
},
201+
openSocket: async () => {
202+
now += 200;
203+
throw new Error("ECONNREFUSED");
204+
},
205+
protocolVersion: 1,
206+
stdout: () => {},
207+
},
208+
),
209+
).rejects.toThrow("ECONNREFUSED");
210+
} finally {
211+
dateSpy.mockRestore();
212+
}
213+
214+
expect(delays).toEqual([50]);
215+
});
216+
160217
it("fails a connected socket whose health success lacks summary evidence", async () => {
161218
const harness = createNetworkClientHarness([{ ok: true }, { ok: true }]);
162219

0 commit comments

Comments
 (0)