simulator.test.ts•3.53 kB
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import type { EventEmitter } from "node:events";
const connectMock = vi.fn();
const listToolsMock = vi.fn(async () => ({ tools: [{ name: "sshExecute" }] }));
const callToolMock = vi.fn(async () => ({ content: [{ type: "text", text: "ok" }] }));
const closeMock = vi.fn();
const transportCloseMock = vi.fn();
const transportInstances: Array<{ options: unknown; stderr: EventEmitter }> = [];
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
  class MockClient {
    listTools = listToolsMock;
    callTool = callToolMock;
    connect = connectMock;
    close = closeMock;
  }
  return { Client: MockClient };
});
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", async () => {
  const { EventEmitter } = await import("node:events");
  class MockTransport extends EventEmitter {
    public stderr = new EventEmitter();
    public options: unknown;
    constructor(options: unknown) {
      super();
      this.options = options;
      transportInstances.push({ options, stderr: this.stderr });
    }
    close = transportCloseMock;
  }
  return { StdioClientTransport: MockTransport };
});
describe("simulator CLI", () => {
  const originalArgv = process.argv.slice();
  const originalEnv = { ...process.env };
  let consoleLogMock: ReturnType<typeof vi.spyOn>;
  beforeEach(() => {
    process.argv = originalArgv.slice(0, 2);
    for (const key of Object.keys(process.env)) {
      if (!(key in originalEnv)) {
        delete process.env[key];
      }
    }
    Object.assign(process.env, originalEnv);
    transportInstances.length = 0;
    connectMock.mockResolvedValue(undefined);
    closeMock.mockResolvedValue(undefined);
    transportCloseMock.mockResolvedValue(undefined);
    listToolsMock.mockClear();
    callToolMock.mockClear();
    consoleLogMock = vi.spyOn(console, "log").mockImplementation(() => {});
  });
  afterEach(() => {
    process.argv = originalArgv.slice();
    for (const key of Object.keys(process.env)) {
      if (!(key in originalEnv)) {
        delete process.env[key];
      }
    }
    Object.assign(process.env, originalEnv);
    consoleLogMock.mockRestore();
    connectMock.mockReset();
    closeMock.mockReset();
    transportCloseMock.mockReset();
  });
  it("lists available tools", async () => {
    process.argv = ["node", "simulator", "list"];
    const { main } = await import("../src/simulator.js");
    await main();
    expect(listToolsMock).toHaveBeenCalledTimes(1);
    expect(consoleLogMock).toHaveBeenCalled();
  });
  it("invokes a tool with provided payload", async () => {
    process.argv = ["node", "simulator", "call", "sshExecute", '{"profile":"training"}'];
    const { main } = await import("../src/simulator.js");
    await main();
    expect(callToolMock).toHaveBeenCalledWith({ name: "sshExecute", arguments: { profile: "training" } });
    expect(consoleLogMock).toHaveBeenCalled();
  });
  it("passes environment overrides to the transport", async () => {
    process.env.MCP_SERVER_COMMAND = "custom-node";
    process.env.MCP_SERVER_ARGS = "server.js, --flag";
    process.env.MCP_SERVER_CWD = "/tmp";
    process.argv = ["node", "simulator", "list"]; // shortlist run
    const { main } = await import("../src/simulator.js");
    await main();
    expect(transportInstances).toHaveLength(1);
    expect(transportInstances[0]?.options).toMatchObject({
      command: "custom-node",
      args: ["server.js", "--flag"],
      cwd: "/tmp"
    });
  });
});