diff --git a/src/gateway/client.test.ts b/src/gateway/client.test.ts new file mode 100644 index 000000000..3b7a2637b --- /dev/null +++ b/src/gateway/client.test.ts @@ -0,0 +1,67 @@ +import { createServer } from "node:net"; +import { afterEach, describe, expect, test } from "vitest"; +import { WebSocketServer } from "ws"; +import { GatewayClient } from "./client.js"; + +// Find a free localhost port for ad-hoc WS servers. +async function getFreePort(): Promise { + return await new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, "127.0.0.1", () => { + const port = (server.address() as { port: number }).port; + server.close((err) => (err ? reject(err) : resolve(port))); + }); + }); +} + +describe("GatewayClient", () => { + let wss: WebSocketServer | null = null; + + afterEach(async () => { + if (wss) { + await new Promise((resolve) => wss?.close(() => resolve())); + wss = null; + } + }); + + test("closes on missing ticks", async () => { + const port = await getFreePort(); + wss = new WebSocketServer({ port, host: "127.0.0.1" }); + + wss.on("connection", (socket) => { + socket.once("message", () => { + // Respond with tiny tick interval to trigger watchdog quickly. + const helloOk = { + type: "hello-ok", + protocol: 1, + server: { version: "dev", connId: "c1" }, + features: { methods: [], events: [] }, + snapshot: { + presence: [], + health: {}, + stateVersion: { presence: 1, health: 1 }, + uptimeMs: 1, + }, + policy: { + maxPayload: 512 * 1024, + maxBufferedBytes: 1024 * 1024, + tickIntervalMs: 5, + }, + }; + socket.send(JSON.stringify(helloOk)); + }); + }); + + const closed = new Promise<{ code: number; reason: string }>((resolve) => { + const client = new GatewayClient({ + url: `ws://127.0.0.1:${port}`, + onClose: (code, reason) => resolve({ code, reason }), + }); + client.start(); + }); + + const res = await closed; + expect(res.code).toBe(4000); + expect(res.reason).toContain("tick timeout"); + }, 4000); +}); diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 5005cc0ea..680e276d1 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -538,4 +538,47 @@ describe("gateway server", () => { ws2.close(); await server.close(); }); + + test("presence includes client fingerprint", async () => { + const { server, ws } = await startServerWithClient(); + ws.send( + JSON.stringify({ + type: "hello", + minProtocol: 1, + maxProtocol: 1, + client: { + name: "fingerprint", + version: "9.9.9", + platform: "test", + mode: "ui", + instanceId: "abc", + }, + caps: [], + }), + ); + await onceMessage(ws, (o) => o.type === "hello-ok"); + + const presenceP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "fingerprint", + 4000, + ); + ws.send( + JSON.stringify({ + type: "req", + id: "fingerprint", + method: "system-presence", + }), + ); + + const presenceRes = await presenceP; + const entries = presenceRes.payload as Array>; + const clientEntry = entries.find((e) => e.instanceId === "abc"); + expect(clientEntry?.host).toBe("fingerprint"); + expect(clientEntry?.version).toBe("9.9.9"); + expect(clientEntry?.mode).toBe("ui"); + + ws.close(); + await server.close(); + }); }); diff --git a/src/infra/agent-events.test.ts b/src/infra/agent-events.test.ts new file mode 100644 index 000000000..50c432c98 --- /dev/null +++ b/src/infra/agent-events.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; +import { emitAgentEvent, onAgentEvent } from "./agent-events.js"; + +describe("agent-events sequencing", () => { + test("maintains monotonic seq per runId", async () => { + const seen: Record = {}; + const stop = onAgentEvent((evt) => { + const list = seen[evt.runId] ?? []; + seen[evt.runId] = list; + list.push(evt.seq); + }); + + emitAgentEvent({ runId: "run-1", stream: "job", data: {} }); + emitAgentEvent({ runId: "run-1", stream: "job", data: {} }); + emitAgentEvent({ runId: "run-2", stream: "job", data: {} }); + emitAgentEvent({ runId: "run-1", stream: "job", data: {} }); + + stop(); + + expect(seen["run-1"]).toEqual([1, 2, 3]); + expect(seen["run-2"]).toEqual([1]); + }); +});