diff --git a/scripts/dev/gateway-smoke.ts b/scripts/dev/gateway-smoke.ts index d90b15fce8fc..8727af430983 100644 --- a/scripts/dev/gateway-smoke.ts +++ b/scripts/dev/gateway-smoke.ts @@ -29,6 +29,28 @@ type GatewaySmokeDeps = { stdout?: (message: string) => void; }; +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function hasHealthSummaryPayload(response: unknown): boolean { + 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) + ); +} + function hasChatHistoryMessages( response: unknown, ): response is { payload: { messages: unknown[] } } { @@ -93,6 +115,10 @@ export async function runGatewaySmoke( stderr(`health failed: ${String(healthRes.error)}`); return 3; } + if (!hasHealthSummaryPayload(healthRes)) { + stderr("health failed: missing health summary payload"); + return 3; + } const historyRes = await request("chat.history", { sessionKey: "main" }, 15000); if (!historyRes.ok) { diff --git a/test/scripts/gateway-smoke.test.ts b/test/scripts/gateway-smoke.test.ts index fdb2119d5f1a..cb3b18db962f 100644 --- a/test/scripts/gateway-smoke.test.ts +++ b/test/scripts/gateway-smoke.test.ts @@ -3,6 +3,22 @@ import { describe, expect, it } from "vitest"; import { runGatewaySmoke } from "../../scripts/dev/gateway-smoke.js"; describe("gateway-smoke", () => { + 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(), + }, + }; + } + function createSmokeDeps( responses: Record>, calls: Array<{ method: string; timeout?: number }> = [], @@ -82,7 +98,7 @@ describe("gateway-smoke", () => { it("requires connect, health, and chat history in order", async () => { const fake = createSmokeDeps({ connect: { ok: true }, - health: { ok: true }, + health: healthResponse(), "chat.history": { ok: true, payload: { messages: [] } }, }); @@ -105,7 +121,7 @@ describe("gateway-smoke", () => { it("fails when chat history success is missing message evidence", async () => { const fake = createSmokeDeps({ connect: { ok: true }, - health: { ok: true }, + health: healthResponse(), "chat.history": { ok: true }, }); @@ -128,7 +144,7 @@ describe("gateway-smoke", () => { it("fails when chat history messages are not an array", async () => { const fake = createSmokeDeps({ connect: { ok: true }, - health: { ok: true }, + health: healthResponse(), "chat.history": { ok: true, payload: { messages: {} } }, }); @@ -159,10 +175,27 @@ describe("gateway-smoke", () => { expect(fake.stderr).toEqual(["health failed: not healthy"]); }); - it("fails after health when chat history is unavailable", async () => { + it("fails when health success is missing summary evidence", async () => { const fake = createSmokeDeps({ connect: { ok: true }, health: { ok: true }, + }); + + const code = await runGatewaySmoke( + { token: "secret-token", urlRaw: "ws://127.0.0.1:12345" }, + fake.deps, + ); + + expect(code).toBe(3); + expect(fake.closed).toBe(1); + expect(fake.calls.map((call) => call.method)).toEqual(["connect", "health"]); + expect(fake.stderr).toEqual(["health failed: missing health summary payload"]); + }); + + it("fails after health when chat history is unavailable", async () => { + const fake = createSmokeDeps({ + connect: { ok: true }, + health: healthResponse(), "chat.history": { ok: false, error: "session store unavailable" }, });