/ src / handlers / processControl.test.js
processControl.test.js
  1  import { describe, beforeEach, it, expect, vi } from "vitest";
  2  import {
  3    mockShellService,
  4    mockOsService,
  5    mockFsService,
  6    mockCodexGlobals,
  7  } from "../__mocks__/service.mocks.js";
  8  import { mockConfigService } from "../__mocks__/service.mocks.js";
  9  import { ProcessControl } from "./processControl.js";
 10  
 11  describe("ProcessControl", () => {
 12    let processControl;
 13    const mockEthProvider = "mockEthProvider";
 14  
 15    beforeEach(() => {
 16      vi.resetAllMocks();
 17      mockCodexGlobals.getEthProvider.mockReturnValue(mockEthProvider);
 18  
 19      processControl = new ProcessControl(
 20        mockConfigService,
 21        mockShellService,
 22        mockOsService,
 23        mockFsService,
 24        mockCodexGlobals,
 25      );
 26  
 27      processControl.sleep = vi.fn();
 28    });
 29  
 30    describe("getCodexProcesses", () => {
 31      const processes = [
 32        { id: 0, name: "a.exe", cmd: "" },
 33        { id: 1, name: "codex", cmd: "<defunct>" },
 34        { id: 2, name: "codex", cmd: "" },
 35        { id: 3, name: "codex.exe", cmd: "<defunct>" },
 36        { id: 4, name: "notcodex", cmd: "" },
 37        { id: 5, name: "alsonotcodex.exe", cmd: "" },
 38      ];
 39  
 40      beforeEach(() => {
 41        mockOsService.listProcesses.mockResolvedValue(processes);
 42      });
 43  
 44      it("returns codex.exe processes on windows", async () => {
 45        mockOsService.isWindows.mockReturnValue(true);
 46  
 47        const p = await processControl.getCodexProcesses();
 48  
 49        expect(p.length).toBe(1);
 50        expect(p[0]).toBe(processes[3]);
 51      });
 52  
 53      it("returns codex processes on non-windows", async () => {
 54        mockOsService.isWindows.mockReturnValue(false);
 55  
 56        const p = await processControl.getCodexProcesses();
 57  
 58        expect(p.length).toBe(1);
 59        expect(p[0]).toBe(processes[2]);
 60      });
 61    });
 62  
 63    describe("getNumberOfCodexProcesses", () => {
 64      it("counts the results of getCodexProcesses", async () => {
 65        processControl.getCodexProcesses = vi.fn();
 66        processControl.getCodexProcesses.mockResolvedValue(["a", "b", "c"]);
 67  
 68        expect(await processControl.getNumberOfCodexProcesses()).toBe(3);
 69      });
 70    });
 71  
 72    describe("stopCodexProcess", () => {
 73      beforeEach(() => {
 74        processControl.getCodexProcesses = vi.fn();
 75      });
 76  
 77      it("throws when no codex processes are found", async () => {
 78        processControl.getCodexProcesses.mockResolvedValue([]);
 79  
 80        await expect(processControl.stopCodexProcess).rejects.toThrow(
 81          "No codex process found",
 82        );
 83      });
 84  
 85      it("calls stopProcess with pid of first codex process", async () => {
 86        const pid = 12345;
 87        processControl.getCodexProcesses.mockResolvedValue([
 88          { pid: pid },
 89          { pid: 111 },
 90          { pid: 222 },
 91        ]);
 92  
 93        processControl.stopProcess = vi.fn();
 94        await processControl.stopCodexProcess();
 95  
 96        expect(processControl.stopProcess).toHaveBeenCalledWith(pid);
 97      });
 98    });
 99  
100    describe("stopProcess", () => {
101      const pid = 234;
102      beforeEach(() => {
103        processControl.isProcessRunning = vi.fn();
104      });
105  
106      it("stops the process", async () => {
107        processControl.isProcessRunning.mockResolvedValue(false);
108  
109        await processControl.stopProcess(pid);
110  
111        expect(mockOsService.stopProcess).toHaveBeenCalledWith(pid);
112      });
113  
114      it("sleeps", async () => {
115        processControl.isProcessRunning.mockResolvedValue(false);
116  
117        await processControl.stopProcess(pid);
118  
119        expect(processControl.sleep).toHaveBeenCalled();
120      });
121  
122      it("terminates process if it is running after stop", async () => {
123        processControl.isProcessRunning.mockResolvedValue(true);
124  
125        await processControl.stopProcess(pid);
126  
127        expect(mockOsService.terminateProcess).toHaveBeenCalledWith(pid);
128      });
129    });
130  
131    describe("isProcessRunning", () => {
132      const pid = 345;
133  
134      it("is true when process is in process list", async () => {
135        mockOsService.listProcesses.mockResolvedValue([{ pid: pid }]);
136  
137        expect(await processControl.isProcessRunning(pid)).toBeTruthy();
138      });
139  
140      it("is false when process is not in process list", async () => {
141        mockOsService.listProcesses.mockResolvedValue([{ pid: pid + 11 }]);
142  
143        expect(await processControl.isProcessRunning(pid)).toBeFalsy();
144      });
145    });
146  
147    describe("startCodexProcess", () => {
148      beforeEach(() => {
149        processControl.saveCodexConfigFile = vi.fn();
150        processControl.startCodex = vi.fn();
151      });
152  
153      it("saves the config, starts codex, and sleeps", async () => {
154        await processControl.startCodexProcess();
155  
156        expect(processControl.saveCodexConfigFile).toHaveBeenCalled();
157        expect(processControl.startCodex).toHaveBeenCalled();
158        expect(processControl.sleep).toHaveBeenCalled();
159      });
160    });
161  
162    describe("saveCodexConfigFile", () => {
163      const publicIp = "1.2.3.4";
164      const bootNodes = ["a", "b", "c"];
165  
166      beforeEach(() => {
167        mockCodexGlobals.getPublicIp.mockResolvedValue(publicIp);
168        mockCodexGlobals.getTestnetSPRs.mockResolvedValue(bootNodes);
169      });
170  
171      it("writes codex config file using public IP and testnet bootstrap nodes", async () => {
172        await processControl.saveCodexConfigFile();
173  
174        expect(mockConfigService.writeCodexConfigFile).toHaveBeenCalledWith(
175          publicIp,
176          bootNodes,
177        );
178      });
179    });
180  
181    describe("startCodex", () => {
182      const config = {
183        codexRoot: "/codex-root",
184      };
185      const exe = "abc.exe";
186      const configFile = "/codex/config.toml";
187  
188      beforeEach(() => {
189        mockConfigService.getCodexExe.mockReturnValue(exe);
190        mockConfigService.get.mockReturnValue(config);
191        mockConfigService.getCodexConfigFilePath.mockReturnValue(configFile);
192      });
193  
194      it("spawns a detached codex process in the codex root working directory with the config file as argument", async () => {
195        await processControl.startCodex();
196  
197        expect(mockShellService.spawnDetachedProcess).toHaveBeenCalledWith(
198          exe,
199          config.codexRoot,
200          [
201            `--config-file=${configFile}`,
202            "persistence",
203            `--eth-provider=${mockEthProvider}`,
204            `--eth-private-key=eth.key`, // duplicated in configService.
205          ],
206        );
207      });
208    });
209  });