From 690a04f81e6b5d2bf356da4286b52ea982bbdc22 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 7 Jun 2026 07:16:19 +0200 Subject: [PATCH] fix(e2e): require gateway network health payload --- scripts/e2e/lib/gateway-network/client.mjs | 25 ++++++++++++++++ test/scripts/gateway-network-client.test.ts | 33 ++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/lib/gateway-network/client.mjs b/scripts/e2e/lib/gateway-network/client.mjs index 1573d1fc43f..63a1c22cf57 100644 --- a/scripts/e2e/lib/gateway-network/client.mjs +++ b/scripts/e2e/lib/gateway-network/client.mjs @@ -17,6 +17,28 @@ async function openSocket(url, timeoutMs = 10_000) { return ws; } +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function hasGatewayHealthSummaryPayload(response) { + if (!isRecord(response) || !isRecord(response.payload)) { + return false; + } + const { payload } = response; + return ( + payload.ok === true && + typeof payload.ts === "number" && + typeof payload.durationMs === "number" && + typeof payload.defaultAgentId === "string" && + payload.defaultAgentId.trim() !== "" && + Array.isArray(payload.agents) && + isRecord(payload.channels) && + Array.isArray(payload.channelOrder) && + isRecord(payload.sessions) + ); +} + export function responseError(method, response) { const message = response.error?.message ?? "unknown"; return new Error(`${method} failed: ${message}`); @@ -92,6 +114,9 @@ export async function runGatewayNetworkClient( (frame) => frame?.type === "res" && frame?.id === "h1", ); if (healthRes.ok) { + if (!hasGatewayHealthSummaryPayload(healthRes)) { + throw new Error("health failed: missing health summary payload"); + } stdout("ok"); return; } diff --git a/test/scripts/gateway-network-client.test.ts b/test/scripts/gateway-network-client.test.ts index 685f1718ec6..34f5621594b 100644 --- a/test/scripts/gateway-network-client.test.ts +++ b/test/scripts/gateway-network-client.test.ts @@ -6,6 +6,22 @@ import { readGatewayNetworkClientConnectTimeoutMs } from "../../scripts/e2e/lib/ import { onceFrame } from "../../scripts/e2e/lib/gateway-network/ws-frames.mjs"; describe("gateway network WebSocket open guard", () => { + function healthResponse() { + return { + ok: true, + payload: { + agents: [], + channelOrder: [], + channels: {}, + defaultAgentId: "codex", + durationMs: 3, + ok: true, + sessions: { count: 0, path: "/state/sessions", recent: [] }, + ts: Date.now(), + }, + }; + } + it("rejects loose client timeout env values instead of parsing prefixes", () => { expect(() => readGatewayNetworkClientConnectTimeoutMs({ @@ -129,7 +145,7 @@ describe("gateway network WebSocket open guard", () => { } it("proves health after the authenticated connect handshake", async () => { - const harness = createNetworkClientHarness([{ ok: true }, { ok: true }]); + const harness = createNetworkClientHarness([{ ok: true }, healthResponse()]); await runGatewayNetworkClient( { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, @@ -141,6 +157,21 @@ describe("gateway network WebSocket open guard", () => { expect(harness.closeCount).toBe(1); }); + it("fails a connected socket whose health success lacks summary evidence", async () => { + const harness = createNetworkClientHarness([{ ok: true }, { ok: true }]); + + await expect( + runGatewayNetworkClient( + { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, + harness.deps, + ), + ).rejects.toThrow("health failed: missing health summary payload"); + + expect(harness.sentMethods).toEqual(["connect", "health"]); + expect(harness.stdout).toEqual([]); + expect(harness.closeCount).toBe(1); + }); + it("fails a connected socket whose health probe fails", async () => { const harness = createNetworkClientHarness([ { ok: true },