import assert from "node:assert/strict"; import { test } from "node:test"; import { DISPLACED_CLOSE_CODE, GatewayRegistry } from "../src/modules/trapos-cloud-gateway/registry.js"; import { GatewayError, type ComputerResponse } from "../src/modules/trapos-cloud-gateway/protocol.js"; import { FakeSocket, silentLog } from "./helpers.js"; function response(messageId: string, ok: boolean, payload: Record = {}, error?: { code: string; message?: string }): ComputerResponse { return { event: "computer_response", traposSenderId: "7:", messageId, type: "ping", ok, payload, error }; } function sentMessageId(socket: FakeSocket): string { return socket.lastFrame().messageId as string; } test("register / has / count / unregister", () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); assert.equal(reg.has("local", "7:"), true); assert.equal(reg.count(), 1); reg.unregister("local", "7:", a); assert.equal(reg.has("local", "7:"), false); assert.equal(reg.count(), 0); }); test("newest-wins evicts previous socket for same key", () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); const b = new FakeSocket(); reg.register("local", "7:", a); reg.register("local", "7:", b); assert.equal(a.closed, true); // Distinct close code lets the displaced Lua client yield instead of reconnect-looping. assert.equal(a.closeCode, DISPLACED_CLOSE_CODE); assert.equal(b.closed, false); assert.equal(reg.count(), 1); }); test("same traposId under different accounts coexist", () => { const reg = new GatewayRegistry(silentLog); reg.register("local", "7:", new FakeSocket()); reg.register("other", "7:", new FakeSocket()); assert.equal(reg.count(), 2); }); test("request sends a server_request frame and resolves on matching ok response", async () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); const promise = reg.request("local", "7:", "ping", {}); const frame = a.lastFrame(); assert.equal(frame.event, "server_request"); assert.equal(frame.type, "ping"); assert.equal(frame.traposReceiverId, "7:"); reg.resolveResponse(a, "local", "7:", response(frame.messageId as string, true, { traposId: "7:" })); assert.deepEqual(await promise, { traposId: "7:" }); }); test("request rejects with the response error code on ok:false", async () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); const promise = reg.request("local", "7:", "ping", {}); reg.resolveResponse(a, "local", "7:", response(sentMessageId(a), false, {}, { code: "unknown_type", message: "nope" })); await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "unknown_type"); }); test("request rejects not_connected when no socket registered", async () => { const reg = new GatewayRegistry(silentLog); await assert.rejects( reg.request("local", "nobody", "ping", {}), (err: unknown) => err instanceof GatewayError && err.code === "not_connected", ); }); test("request rejects timeout after the deadline", async () => { const reg = new GatewayRegistry(silentLog); reg.register("local", "7:", new FakeSocket()); await assert.rejects( reg.request("local", "7:", "ping", {}, 10), (err: unknown) => err instanceof GatewayError && err.code === "timeout", ); }); test("messageId correlation: concurrent requests resolve independently", async () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); const p1 = reg.request("local", "7:", "ping", { n: 1 }); const id1 = a.lastFrame().messageId as string; const p2 = reg.request("local", "7:", "ping", { n: 2 }); const id2 = a.lastFrame().messageId as string; assert.notEqual(id1, id2); // resolve out of order reg.resolveResponse(a, "local", "7:", response(id2, true, { n: 2 })); reg.resolveResponse(a, "local", "7:", response(id1, true, { n: 1 })); assert.deepEqual(await p1, { n: 1 }); assert.deepEqual(await p2, { n: 2 }); }); test("pending rejected on unregister (close)", async () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); const promise = reg.request("local", "7:", "ping", {}); reg.unregister("local", "7:", a); await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected"); }); test("eviction: old socket's pending rejected and late unregister(A) does not evict B", async () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); const b = new FakeSocket(); reg.register("local", "7:", a); const promise = reg.request("local", "7:", "ping", {}); reg.register("local", "7:", b); // evict A await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected"); reg.unregister("local", "7:", a); // late close from evicted A — must be a no-op assert.equal(reg.has("local", "7:"), true); assert.equal(reg.count(), 1); }); test("resolveResponse with no matching pending is a silent no-op", () => { const reg = new GatewayRegistry(silentLog); const a = new FakeSocket(); reg.register("local", "7:", a); // should not throw reg.resolveResponse(a, "local", "7:", response("unknown-id", true, {})); });