import { randomUUID } from "node:crypto"; import { afterAll, describe, expect, it } from "vitest"; import { GatewayClient } from "../src/gateway/client.js"; import { connectGatewayClient } from "../src/gateway/test-helpers.e2e.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js"; import { type ChatEventPayload, type GatewayInstance, connectNode, extractFirstTextBlock, postJson, spawnGatewayInstance, stopGatewayInstance, waitForChatFinalEvent, waitForNodeStatus, } from "./helpers/gateway-e2e-harness.js"; const E2E_TIMEOUT_MS = 120_000; describe("gateway multi-instance e2e", () => { const instances: GatewayInstance[] = []; const nodeClients: GatewayClient[] = []; const chatClients: GatewayClient[] = []; afterAll(async () => { for (const client of nodeClients) { client.stop(); } for (const client of chatClients) { client.stop(); } for (const inst of instances) { await stopGatewayInstance(inst); } }); it( "spins up two gateways and exercises WS + HTTP + node pairing", { timeout: E2E_TIMEOUT_MS }, async () => { const [gwA, gwB] = await Promise.all([spawnGatewayInstance("a"), spawnGatewayInstance("b")]); instances.push(gwA, gwB); const [hookResA, hookResB] = await Promise.all([ postJson( `http://127.0.0.1:${gwA.port}/hooks/wake`, { text: "wake a", mode: "now", }, { "x-openclaw-token": gwA.hookToken }, ), postJson( `http://127.0.0.1:${gwB.port}/hooks/wake`, { text: "wake b", mode: "now", }, { "x-openclaw-token": gwB.hookToken }, ), ]); expect(hookResA.status).toBe(200); expect((hookResA.json as { ok?: boolean } | undefined)?.ok).toBe(true); expect(hookResB.status).toBe(200); expect((hookResB.json as { ok?: boolean } | undefined)?.ok).toBe(true); const [nodeA, nodeB] = await Promise.all([ connectNode(gwA, "node-a"), connectNode(gwB, "node-b"), ]); nodeClients.push(nodeA.client, nodeB.client); await Promise.all([ waitForNodeStatus(gwA, nodeA.nodeId), waitForNodeStatus(gwB, nodeB.nodeId), ]); }, ); it( "delivers final chat event for telegram-shaped session keys", { timeout: E2E_TIMEOUT_MS }, async () => { const gw = await spawnGatewayInstance("chat-telegram-fixture"); instances.push(gw); const chatEvents: ChatEventPayload[] = []; const chatClient = await connectGatewayClient({ url: `ws://127.0.0.1:${gw.port}`, token: gw.gatewayToken, clientName: GATEWAY_CLIENT_NAMES.CLI, clientDisplayName: "chat-e2e-cli", clientVersion: "1.0.0", platform: "test", mode: GATEWAY_CLIENT_MODES.CLI, onEvent: (evt) => { if (evt.event === "chat" && evt.payload && typeof evt.payload === "object") { chatEvents.push(evt.payload as ChatEventPayload); } }, }); chatClients.push(chatClient); const sessionKey = "agent:main:telegram:direct:123456"; const idempotencyKey = `idem-${randomUUID()}`; const sendRes = await chatClient.request<{ runId?: string; status?: string }>("chat.send", { sessionKey, message: "/context list", idempotencyKey, }); expect(sendRes.status).toBe("started"); const runId = sendRes.runId; expect(typeof runId).toBe("string"); const finalEvent = await waitForChatFinalEvent({ events: chatEvents, runId: String(runId), sessionKey, }); const finalText = extractFirstTextBlock(finalEvent.message); expect(typeof finalText).toBe("string"); expect(finalText?.length).toBeGreaterThan(0); }, ); });