From d02ef5ae022db4136984ee8b80b29b705f454e37 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 16:07:57 -0700 Subject: [PATCH] test(irc): cover connection and probe helpers --- extensions/irc/src/connect-options.test.ts | 47 +++++++++++ extensions/irc/src/control-chars.test.ts | 17 ++++ extensions/irc/src/probe.test.ts | 96 ++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 extensions/irc/src/connect-options.test.ts create mode 100644 extensions/irc/src/control-chars.test.ts create mode 100644 extensions/irc/src/probe.test.ts diff --git a/extensions/irc/src/connect-options.test.ts b/extensions/irc/src/connect-options.test.ts new file mode 100644 index 00000000000..f991ea0ca30 --- /dev/null +++ b/extensions/irc/src/connect-options.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { buildIrcConnectOptions } from "./connect-options.js"; + +describe("buildIrcConnectOptions", () => { + it("copies resolved account connection fields and NickServ config", () => { + const account = { + host: "irc.libera.chat", + port: 6697, + tls: true, + nick: "openclaw", + username: "openclaw", + realname: "OpenClaw Bot", + password: "server-pass", + config: { + nickserv: { + enabled: true, + service: "NickServ", + password: "nickserv-pass", + register: true, + registerEmail: "bot@example.com", + }, + }, + }; + + expect( + buildIrcConnectOptions(account as never, { + connectTimeoutMs: 1234, + }), + ).toEqual({ + host: "irc.libera.chat", + port: 6697, + tls: true, + nick: "openclaw", + username: "openclaw", + realname: "OpenClaw Bot", + password: "server-pass", + nickserv: { + enabled: true, + service: "NickServ", + password: "nickserv-pass", + register: true, + registerEmail: "bot@example.com", + }, + connectTimeoutMs: 1234, + }); + }); +}); diff --git a/extensions/irc/src/control-chars.test.ts b/extensions/irc/src/control-chars.test.ts new file mode 100644 index 00000000000..e8c5a506289 --- /dev/null +++ b/extensions/irc/src/control-chars.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { hasIrcControlChars, isIrcControlChar, stripIrcControlChars } from "./control-chars.js"; + +describe("irc control char helpers", () => { + it("detects IRC control characters by codepoint", () => { + expect(isIrcControlChar(0x00)).toBe(true); + expect(isIrcControlChar(0x1f)).toBe(true); + expect(isIrcControlChar(0x7f)).toBe(true); + expect(isIrcControlChar(0x20)).toBe(false); + }); + + it("detects and strips IRC control characters from strings", () => { + expect(hasIrcControlChars("hello\u0002world")).toBe(true); + expect(hasIrcControlChars("hello world")).toBe(false); + expect(stripIrcControlChars("he\u0002llo\u007f world")).toBe("hello world"); + }); +}); diff --git a/extensions/irc/src/probe.test.ts b/extensions/irc/src/probe.test.ts new file mode 100644 index 00000000000..05b8eda62e1 --- /dev/null +++ b/extensions/irc/src/probe.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; + +const resolveIrcAccountMock = vi.hoisted(() => vi.fn()); +const buildIrcConnectOptionsMock = vi.hoisted(() => vi.fn()); +const connectIrcClientMock = vi.hoisted(() => vi.fn()); + +vi.mock("./accounts.js", () => ({ + resolveIrcAccount: resolveIrcAccountMock, +})); + +vi.mock("./connect-options.js", () => ({ + buildIrcConnectOptions: buildIrcConnectOptionsMock, +})); + +vi.mock("./client.js", () => ({ + connectIrcClient: connectIrcClientMock, +})); + +import { probeIrc } from "./probe.js"; + +describe("probeIrc", () => { + it("returns a configuration error when the IRC account is incomplete", async () => { + resolveIrcAccountMock.mockReturnValue({ + configured: false, + host: "", + port: 6667, + tls: false, + nick: "", + }); + + await expect(probeIrc({} as never)).resolves.toEqual({ + ok: false, + host: "", + port: 6667, + tls: false, + nick: "", + error: "missing host or nick", + }); + expect(connectIrcClientMock).not.toHaveBeenCalled(); + }); + + it("returns latency and quits the probe client on success", async () => { + resolveIrcAccountMock.mockReturnValue({ + configured: true, + host: "irc.libera.chat", + port: 6697, + tls: true, + nick: "openclaw", + }); + buildIrcConnectOptionsMock.mockReturnValue({ host: "irc.libera.chat" }); + const quit = vi.fn(); + connectIrcClientMock.mockResolvedValue({ quit }); + const nowSpy = vi.spyOn(Date, "now").mockReturnValueOnce(100).mockReturnValueOnce(145); + + try { + const result = await probeIrc({} as never, { timeoutMs: 5000 }); + + expect(buildIrcConnectOptionsMock).toHaveBeenCalledWith( + expect.objectContaining({ host: "irc.libera.chat" }), + { connectTimeoutMs: 5000 }, + ); + expect(result).toEqual({ + ok: true, + host: "irc.libera.chat", + port: 6697, + tls: true, + nick: "openclaw", + latencyMs: 45, + }); + expect(quit).toHaveBeenCalledWith("probe"); + } finally { + nowSpy.mockRestore(); + } + }); + + it("formats non-Error probe failures into the returned error field", async () => { + resolveIrcAccountMock.mockReturnValue({ + configured: true, + host: "irc.libera.chat", + port: 6667, + tls: false, + nick: "openclaw", + }); + buildIrcConnectOptionsMock.mockReturnValue({ host: "irc.libera.chat" }); + connectIrcClientMock.mockRejectedValue({ code: "ECONNREFUSED" }); + + await expect(probeIrc({} as never)).resolves.toEqual({ + ok: false, + host: "irc.libera.chat", + port: 6667, + tls: false, + nick: "openclaw", + error: JSON.stringify({ code: "ECONNREFUSED" }), + }); + }); +});