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 });