diff --git a/tests/tools/brainstorm.test.ts b/tests/tools/brainstorm.test.ts new file mode 100644 index 0000000..1ca1c0a --- /dev/null +++ b/tests/tools/brainstorm.test.ts @@ -0,0 +1,80 @@ +import { jest, describe, it, expect, beforeEach } from "@jest/globals"; +import type { ConfigChatType, ThreadStateType } from "../../src/types"; +import type { Message } from "telegraf/types"; + +const mockBuildMessages = jest.fn(); +const mockLlmCall = jest.fn(); +const mockReadConfig = jest.fn(); + +jest.unstable_mockModule("../../src/helpers/gpt.ts", () => ({ + buildMessages: (...args: unknown[]) => mockBuildMessages(...args), + llmCall: (...args: unknown[]) => mockLlmCall(...args), +})); + +jest.unstable_mockModule("../../src/config.ts", () => ({ + readConfig: () => mockReadConfig(), +})); + +let mod: typeof import("../../src/tools/brainstorm.ts"); + +beforeEach(async () => { + jest.resetModules(); + mockBuildMessages.mockReset(); + mockLlmCall.mockReset(); + mockReadConfig.mockReset(); + mod = await import("../../src/tools/brainstorm.ts"); +}); + +describe("BrainstormClient", () => { + it("calls buildMessages and llmCall with prompts", async () => { + const cfg: ConfigChatType = { + name: "chat", + agent_name: "agent", + completionParams: {}, + chatParams: {}, + toolParams: { + brainstorm: { promptBefore: "BEFORE", promptAfter: "AFTER" }, + }, + } as ConfigChatType; + + const thread: ThreadStateType = { + id: 1, + msgs: [{ text: "hi" } as Message.TextMessage], + messages: [], + } as ThreadStateType; + + mockBuildMessages.mockResolvedValue([{ role: "system" }]); + mockLlmCall.mockResolvedValue({ + res: { choices: [{ message: { content: "RES" } }] }, + }); + + const client = new mod.BrainstormClient(cfg, thread); + const res = await client.brainstorm({ systemMessage: "SYS" }); + + expect(mockBuildMessages).toHaveBeenCalledWith( + "SYS\n\nBEFORE", + thread.messages, + ); + expect(mockLlmCall).toHaveBeenCalled(); + expect(res.content).toBe("RES\n\nAFTER"); + }); + + it("options_string formats text", () => { + const client = new mod.BrainstormClient( + {} as ConfigChatType, + { id: 1, msgs: [], messages: [] } as ThreadStateType, + ); + expect(client.options_string('{"systemMessage":"p"}')).toBe( + "**Brainstorm:** `p`", + ); + expect(client.options_string("{}" as string)).toBe("{}"); + }); + + it("call returns instance", () => { + const client = mod.call( + {} as ConfigChatType, + { id: 1, msgs: [], messages: [] } as ThreadStateType, + ); + expect(client).toBeInstanceOf(mod.BrainstormClient); + }); +}); diff --git a/tests/tools/forget.test.ts b/tests/tools/forget.test.ts new file mode 100644 index 0000000..6ea0395 --- /dev/null +++ b/tests/tools/forget.test.ts @@ -0,0 +1,63 @@ +import { jest, describe, it, expect, beforeEach } from "@jest/globals"; +import type { ConfigChatType, ThreadStateType } from "../../src/types"; + +const mockForgetHistory = jest.fn(); +const mockLog = jest.fn(); + +jest.unstable_mockModule("../../src/helpers/history.ts", () => ({ + forgetHistory: (...args: unknown[]) => mockForgetHistory(...args), +})); + +jest.unstable_mockModule("../../src/helpers.ts", () => ({ + log: (...args: unknown[]) => mockLog(...args), +})); + +let mod: typeof import("../../src/tools/forget.ts"); + +beforeEach(async () => { + jest.resetModules(); + mockForgetHistory.mockReset(); + mockLog.mockReset(); + mod = await import("../../src/tools/forget.ts"); +}); + +describe("ForgetClient", () => { + const cfg = {} as ConfigChatType; + const thread = { id: 2 } as ThreadStateType; + + it("forgets history and logs", async () => { + const client = new mod.ForgetClient(cfg, thread); + const res = await client.forget({}); + expect(mockForgetHistory).toHaveBeenCalledWith(2); + expect(mockLog).toHaveBeenCalled(); + expect(res.content).toBe("Forgot history"); + }); + + it("uses custom message", async () => { + const client = new mod.ForgetClient(cfg, thread); + const res = await client.forget({ message: "Bye" }); + expect(res.content).toBe("Bye"); + }); + + it("handles errors", async () => { + mockForgetHistory.mockImplementation(() => { + throw new Error("boom"); + }); + const client = new mod.ForgetClient(cfg, thread); + const res = await client.forget({}); + expect(res.content).toContain("boom"); + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ logLevel: "error" }), + ); + }); + + it("options_string constant", () => { + const client = new mod.ForgetClient(cfg, thread); + expect(client.options_string()).toBe("`Clear conversation history:`"); + }); + + it("call returns instance", () => { + const client = mod.call(cfg, thread); + expect(client).toBeInstanceOf(mod.ForgetClient); + }); +}); diff --git a/tests/tools/javascript_interpreter.test.ts b/tests/tools/javascript_interpreter.test.ts new file mode 100644 index 0000000..d588495 --- /dev/null +++ b/tests/tools/javascript_interpreter.test.ts @@ -0,0 +1,36 @@ +import { jest, describe, it, expect, beforeEach } from "@jest/globals"; +import type { ToolResponse } from "../../src/types"; + +let mod: typeof import("../../src/tools/javascript_interpreter.ts"); + +beforeEach(async () => { + jest.resetModules(); + mod = await import("../../src/tools/javascript_interpreter.ts"); +}); + +describe("JavascriptInterpreterClient", () => { + it("executes code and returns result", async () => { + const client = new mod.JavascriptInterpreterClient(); + const res = await client.javascript_interpreter({ code: "1+2" }); + expect(res).toEqual({ content: "3" } as ToolResponse); + }); + + it("returns error string on exception", async () => { + const client = new mod.JavascriptInterpreterClient(); + const res = await client.javascript_interpreter({ + code: "throw new Error('x')", + }); + expect(res.content).toContain("Error: Unknown error"); + }); + + it("options_string formats code", () => { + const client = new mod.JavascriptInterpreterClient(); + const formatted = client.options_string('{"code":"2+2"}'); + expect(formatted).toBe("`Javascript:`\n```js\n2+2\n```"); + }); + + it("call returns instance", () => { + const client = mod.call(); + expect(client).toBeInstanceOf(mod.JavascriptInterpreterClient); + }); +}); diff --git a/tests/tools/ssh_command.test.ts b/tests/tools/ssh_command.test.ts new file mode 100644 index 0000000..9e6d24c --- /dev/null +++ b/tests/tools/ssh_command.test.ts @@ -0,0 +1,116 @@ +import { jest, describe, it, expect, beforeEach } from "@jest/globals"; +import type { ConfigChatType } from "../../src/types"; + +const mockExec = jest.fn(); +const mockFileSync = jest.fn(); +const mockWriteFileSync = jest.fn(); + +jest.unstable_mockModule("child_process", () => ({ + exec: (...args: unknown[]) => mockExec(...args), +})); + +jest.unstable_mockModule("tmp", () => ({ + fileSync: (...args: unknown[]) => mockFileSync(...args), +})); +jest.unstable_mockModule("fs", () => { + const real = jest.requireActual("fs"); + return { + __esModule: true, + ...real, + default: { + ...real, + writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args), + }, + writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args), + }; +}); +let mod: typeof import("../../src/tools/ssh_command.ts"); + +beforeEach(async () => { + jest.resetModules(); + mockExec.mockReset(); + mockFileSync.mockReset(); + mockWriteFileSync.mockReset(); + mod = await import("../../src/tools/ssh_command.ts"); +}); + +describe("SshCommandClient", () => { + const cfg: ConfigChatType = { + name: "chat", + agent_name: "agent", + completionParams: {}, + chatParams: {}, + toolParams: { + ssh_command: { user: "u", host: "h", strictHostKeyChecking: true }, + }, + } as ConfigChatType; + + it("runs command via ssh", async () => { + mockFileSync.mockReturnValue({ + name: "/tmp/tmp.sh", + removeCallback: jest.fn(), + }); + mockExec + .mockImplementationOnce((_cmd: string, cb: (e: any) => void) => cb(null)) + .mockImplementationOnce( + (_cmd: string, cb: (e: any, out: string, err: string) => void) => + cb(null, "ok", ""), + ); + + const client = new mod.SshCommandClient(cfg); + const res = await client.ssh_command({ command: "ls" }); + + expect(mockWriteFileSync).toHaveBeenCalledWith("/tmp/tmp.sh", "ls"); + expect(mockExec).toHaveBeenCalledTimes(2); + expect(res.content).toBe("```\nok\n```"); + }); + + it("returns exit code when ssh fails", async () => { + const remove = jest.fn(); + mockFileSync.mockReturnValue({ + name: "/tmp/tmp.sh", + removeCallback: remove, + }); + mockExec + .mockImplementationOnce((_c: string, cb: (e: any) => void) => cb(null)) + .mockImplementationOnce( + (_c: string, cb: (e: any, out: string, err: string) => void) => { + const err = new Error("Command failed: ssh boom"); + (err as any).code = 1; + cb(err, "sout", "serr"); + }, + ); + + const client = new mod.SshCommandClient(cfg); + const res = await client.ssh_command({ command: "do" }); + expect(res.content).toContain("Exit code: 1"); + expect(remove).toHaveBeenCalled(); + }); + + it("getUserHost defaults", () => { + const client = new mod.SshCommandClient({ + name: "c", + agent_name: "a", + completionParams: {}, + chatParams: {}, + toolParams: {}, + } as ConfigChatType); + expect(client.getUserHost()).toEqual({ + user: "root", + host: "localhost", + strictHostKeyChecking: false, + }); + }); + + it("options_string and systemMessage", () => { + const client = new mod.SshCommandClient(cfg); + const str = client.options_string('{"command":"echo hi"}'); + expect(str).toContain("`ssh u@h`"); + expect(str).toContain("echo hi"); + expect(client.systemMessage()).toContain("u@h"); + }); + + it("call returns instance", () => { + expect(mod.call(cfg)).toBeInstanceOf(mod.SshCommandClient); + }); +});