/ src / handlers / installer.test.js
installer.test.js
  1  import { describe, beforeEach, it, expect, vi } from "vitest";
  2  import {
  3    mockShellService,
  4    mockOsService,
  5    mockFsService,
  6    mockConfigService,
  7    mockMarketplaceSetup,
  8  } from "../__mocks__/service.mocks.js";
  9  import { Installer } from "./installer.js";
 10  
 11  describe("Installer", () => {
 12    const config = {
 13      codexRoot: "/codex-root",
 14    };
 15    const workingDir = "/working-dir";
 16    const exe = "abc.exe";
 17    const processCallbacks = {
 18      installStarts: vi.fn(),
 19      downloadSuccessful: vi.fn(),
 20      installSuccessful: vi.fn(),
 21      warn: vi.fn(),
 22    };
 23    let installer;
 24  
 25    beforeEach(() => {
 26      vi.resetAllMocks();
 27      mockConfigService.get.mockReturnValue(config);
 28      mockOsService.getWorkingDir.mockReturnValue(workingDir);
 29      mockConfigService.getCodexExe.mockReturnValue(exe);
 30  
 31      installer = new Installer(
 32        mockConfigService,
 33        mockShellService,
 34        mockOsService,
 35        mockFsService,
 36        mockMarketplaceSetup,
 37      );
 38    });
 39  
 40    describe("getCodexVersion", () => {
 41      it("checks if the codex exe file exists", async () => {
 42        mockFsService.isFile.mockReturnValue(true);
 43        mockShellService.run.mockResolvedValueOnce("a");
 44        await installer.getCodexVersion();
 45        expect(mockFsService.isFile).toHaveBeenCalledWith(exe);
 46      });
 47  
 48      it("throws when codex exe is not a file", async () => {
 49        mockFsService.isFile.mockReturnValue(false);
 50        await expect(installer.getCodexVersion()).rejects.toThrow(
 51          "Codex not installed.",
 52        );
 53      });
 54  
 55      it("throws when version info is not found", async () => {
 56        mockFsService.isFile.mockReturnValue(true);
 57        mockShellService.run.mockResolvedValueOnce("");
 58        await expect(installer.getCodexVersion()).rejects.toThrow(
 59          "Version info not found.",
 60        );
 61      });
 62  
 63      it("returns version info", async () => {
 64        mockFsService.isFile.mockReturnValue(true);
 65        const versionInfo = "versionInfo";
 66        mockShellService.run.mockResolvedValueOnce(versionInfo);
 67        const version = await installer.getCodexVersion();
 68        expect(version).toBe(versionInfo);
 69      });
 70    });
 71  
 72    describe("isCodexInstalled", () => {
 73      it("return true when getCodexVersion succeeds", async () => {
 74        installer.getCodexVersion = vi.fn();
 75        expect(await installer.isCodexInstalled()).toBe(true);
 76      });
 77  
 78      it("returns false when getCodexVersion fails", async () => {
 79        installer.getCodexVersion = vi.fn(() => {
 80          throw new Error("Codex not installed.");
 81        });
 82        expect(await installer.isCodexInstalled()).toBe(false);
 83      });
 84    });
 85  
 86    describe("installCodex", () => {
 87      beforeEach(() => {
 88        installer.arePrerequisitesCorrect = vi.fn();
 89        installer.installCodexWindows = vi.fn();
 90        installer.installCodexUnix = vi.fn();
 91        installer.isCodexInstalled = vi.fn();
 92      });
 93  
 94      it("ensures codex root dir exists", async () => {
 95        installer.arePrerequisitesCorrect.mockResolvedValue(false);
 96        await installer.installCodex(processCallbacks);
 97  
 98        expect(mockFsService.ensureDirExists).toHaveBeenCalledWith(
 99          config.codexRoot,
100        );
101      });
102  
103      it("returns early when prerequisites are not correct", async () => {
104        installer.arePrerequisitesCorrect.mockResolvedValue(false);
105        await installer.installCodex(processCallbacks);
106        expect(processCallbacks.installStarts).not.toHaveBeenCalled();
107        expect(processCallbacks.installSuccessful).not.toHaveBeenCalled();
108        expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled();
109        expect(installer.isCodexInstalled).not.toHaveBeenCalled();
110        expect(installer.installCodexWindows).not.toHaveBeenCalled();
111        expect(installer.installCodexUnix).not.toHaveBeenCalled();
112      });
113  
114      it("returns early when marketplace client wizard returns false", async () => {
115        installer.arePrerequisitesCorrect.mockResolvedValue(true);
116        mockMarketplaceSetup.runClientWizard.mockResolvedValue(false);
117        await installer.installCodex(processCallbacks);
118        expect(processCallbacks.installStarts).not.toHaveBeenCalled();
119        expect(processCallbacks.installSuccessful).not.toHaveBeenCalled();
120        expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled();
121        expect(installer.isCodexInstalled).not.toHaveBeenCalled();
122        expect(installer.installCodexWindows).not.toHaveBeenCalled();
123        expect(installer.installCodexUnix).not.toHaveBeenCalled();
124      });
125  
126      describe("prerequisites OK", () => {
127        beforeEach(() => {
128          installer.arePrerequisitesCorrect.mockResolvedValue(true);
129          mockMarketplaceSetup.runClientWizard.mockResolvedValue(true);
130          installer.isCodexInstalled.mockResolvedValue(true);
131        });
132  
133        it("calls installStarts when prerequisites are correct", async () => {
134          await installer.installCodex(processCallbacks);
135          expect(processCallbacks.installStarts).toHaveBeenCalled();
136        });
137  
138        it("calls installCodexWindows when OS is Windows", async () => {
139          mockOsService.isWindows.mockReturnValue(true);
140          await installer.installCodex(processCallbacks);
141          expect(installer.installCodexWindows).toHaveBeenCalledWith(
142            processCallbacks,
143          );
144        });
145  
146        it("calls installCodexUnix when OS is not Windows", async () => {
147          mockOsService.isWindows.mockReturnValue(false);
148          await installer.installCodex(processCallbacks);
149          expect(installer.installCodexUnix).toHaveBeenCalledWith(
150            processCallbacks,
151          );
152        });
153  
154        it("throws when codex is not installed after installation", async () => {
155          installer.isCodexInstalled.mockResolvedValue(false);
156          await expect(installer.installCodex(processCallbacks)).rejects.toThrow(
157            "Codex installation failed.",
158          );
159        });
160  
161        it("warns user when codex is not installed after installation", async () => {
162          installer.isCodexInstalled.mockResolvedValue(false);
163          await expect(installer.installCodex(processCallbacks)).rejects.toThrow(
164            "Codex installation failed.",
165          );
166          expect(processCallbacks.warn).toHaveBeenCalledWith(
167            "Codex failed to install.",
168          );
169        });
170  
171        it("calls installSuccessful when installation is successful", async () => {
172          await installer.installCodex(processCallbacks);
173          expect(processCallbacks.installSuccessful).toHaveBeenCalled();
174        });
175      });
176    });
177  
178    describe("arePrerequisitesCorrect", () => {
179      beforeEach(() => {
180        installer.isCodexInstalled = vi.fn();
181        installer.isCurlAvailable = vi.fn();
182      });
183  
184      it("returns false when codex is already installed", async () => {
185        installer.isCodexInstalled.mockResolvedValue(true);
186        expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
187          false,
188        );
189        expect(processCallbacks.warn).toHaveBeenCalledWith(
190          "Codex is already installed.",
191        );
192      });
193  
194      it("checks if the root path exists", async () => {
195        expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
196          false,
197        );
198        expect(mockFsService.isDir).toHaveBeenCalledWith(config.codexRoot);
199      });
200  
201      it("returns false when root path does not exist", async () => {
202        mockFsService.isDir.mockReturnValue(false);
203        expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
204          false,
205        );
206        expect(processCallbacks.warn).toHaveBeenCalledWith(
207          "Root path doesn't exist.",
208        );
209      });
210  
211      it("returns false when curl is not available", async () => {
212        installer.isCodexInstalled.mockResolvedValue(false);
213        mockFsService.isDir.mockReturnValue(true);
214        installer.isCurlAvailable.mockResolvedValue(false);
215        expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
216          false,
217        );
218        expect(processCallbacks.warn).toHaveBeenCalledWith(
219          "Curl is not available.",
220        );
221      });
222  
223      it("returns true when all prerequisites are correct", async () => {
224        installer.isCodexInstalled.mockResolvedValue(false);
225        mockFsService.isDir.mockReturnValue(true);
226        installer.isCurlAvailable.mockResolvedValue(true);
227        const result = await installer.arePrerequisitesCorrect(processCallbacks);
228        expect(result).toBe(true);
229      });
230    });
231  
232    describe("isCurlAvailable", () => {
233      it("returns true when curl version is found", async () => {
234        mockShellService.run.mockResolvedValueOnce("curl version");
235        const result = await installer.isCurlAvailable();
236        expect(mockShellService.run).toHaveBeenCalledWith("curl --version");
237        expect(result).toBe(true);
238      });
239  
240      it("returns false when curl version is not found", async () => {
241        mockShellService.run.mockResolvedValueOnce("");
242        const result = await installer.isCurlAvailable();
243        expect(mockShellService.run).toHaveBeenCalledWith("curl --version");
244        expect(result).toBe(false);
245      });
246    });
247  
248    describe("install functions", () => {
249      beforeEach(() => {
250        installer.saveCodexInstallPath = vi.fn();
251      });
252  
253      describe("installCodexWindows", () => {
254        it("runs the curl command to download the installer", async () => {
255          await installer.installCodexWindows(processCallbacks);
256          expect(mockShellService.run).toHaveBeenCalledWith(
257            "curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd",
258          );
259        });
260  
261        it("calls downloadSuccessful", async () => {
262          await installer.installCodexWindows(processCallbacks);
263          expect(processCallbacks.downloadSuccessful).toHaveBeenCalled();
264        });
265  
266        it("runs installer script", async () => {
267          await installer.installCodexWindows(processCallbacks);
268          expect(mockShellService.run).toHaveBeenCalledWith(
269            `set "INSTALL_DIR=${config.codexRoot}" && "${workingDir}\\install.cmd"`,
270          );
271        });
272  
273        it("deletes the installer script", async () => {
274          await installer.installCodexWindows(processCallbacks);
275          expect(mockShellService.run).toHaveBeenCalledWith("del /f install.cmd");
276        });
277      });
278  
279      describe("installCodexUnix", () => {
280        beforeEach(() => {
281          installer.ensureUnixDependencies = vi.fn();
282          installer.runInstallerDarwin = vi.fn();
283          installer.runInstallerLinux = vi.fn();
284        });
285  
286        it("ensures unix dependencies", async () => {
287          await installer.installCodexUnix(processCallbacks);
288          expect(installer.ensureUnixDependencies).toHaveBeenCalled(
289            processCallbacks,
290          );
291        });
292  
293        it("returns early if unix dependencies are not met", async () => {
294          installer.ensureUnixDependencies.mockResolvedValue(false);
295  
296          await installer.installCodexUnix(processCallbacks);
297  
298          expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled();
299          expect(installer.runInstallerDarwin).not.toHaveBeenCalled();
300          expect(installer.runInstallerLinux).not.toHaveBeenCalled();
301        });
302  
303        describe("when dependencies are met", () => {
304          beforeEach(() => {
305            installer.ensureUnixDependencies.mockResolvedValue(true);
306          });
307  
308          it("runs the curl command to download the installer", async () => {
309            await installer.installCodexUnix(processCallbacks);
310            expect(mockShellService.run).toHaveBeenCalledWith(
311              "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh",
312            );
313          });
314  
315          it("calls downloadSuccessful", async () => {
316            await installer.installCodexUnix(processCallbacks);
317            expect(processCallbacks.downloadSuccessful).toHaveBeenCalled();
318          });
319  
320          it("runs installer for darwin ", async () => {
321            mockOsService.isDarwin.mockReturnValue(true);
322            await installer.installCodexUnix(processCallbacks);
323            expect(installer.runInstallerDarwin).toHaveBeenCalled();
324          });
325  
326          it("runs installer for linux", async () => {
327            mockOsService.isDarwin.mockReturnValue(false);
328            await installer.installCodexUnix(processCallbacks);
329            expect(installer.runInstallerLinux).toHaveBeenCalled();
330          });
331  
332          it("deletes the installer script", async () => {
333            await installer.installCodexUnix(processCallbacks);
334            expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh");
335          });
336        });
337      });
338  
339      describe("runInstallerDarwin", () => {
340        it("runs the installer script for darwin with custom timeout command", async () => {
341          const timeoutCommand = `perl -e '
342      eval {
343          local $SIG{ALRM} = sub { die "timeout\\n" };
344          alarm(120);
345          system("INSTALL_DIR=\\"${config.codexRoot}\\" bash install.sh");
346          alarm(0);
347      };
348      die if $@;
349  '`;
350          await installer.runInstallerDarwin();
351          expect(mockShellService.run).toHaveBeenCalledWith(timeoutCommand);
352        });
353      });
354  
355      describe("runInstallerLinux", () => {
356        it("runs the installer script using unix timeout command", async () => {
357          await installer.runInstallerLinux();
358          expect(mockShellService.run).toHaveBeenCalledWith(
359            `INSTALL_DIR="${config.codexRoot}" timeout 120 bash install.sh`,
360          );
361        });
362      });
363    });
364  
365    describe("ensureUnixDependencies", () => {
366      it("returns true when libgomp is installed", async () => {
367        mockShellService.run.mockResolvedValueOnce("yes");
368        expect(await installer.ensureUnixDependencies(processCallbacks)).toBe(
369          true,
370        );
371        expect(mockShellService.run).toHaveBeenCalledWith(
372          "ldconfig -p | grep libgomp",
373        );
374      });
375  
376      it("returns false when libgomp is not found", async () => {
377        mockShellService.run.mockResolvedValue("");
378        expect(await installer.ensureUnixDependencies(processCallbacks)).toBe(
379          false,
380        );
381        expect(mockShellService.run).toHaveBeenCalledWith(
382          "ldconfig -p | grep libgomp",
383        );
384      });
385  
386      it("it calls warn in processCallbacks when libgomp is not found", async () => {
387        mockShellService.run.mockResolvedValue("");
388        await installer.ensureUnixDependencies(processCallbacks);
389        expect(processCallbacks.warn).toHaveBeenCalledWith("libgomp not found.");
390      });
391    });
392  
393    describe("uninstallCodex", () => {
394      it("deletes the codex root path", () => {
395        installer.uninstallCodex();
396  
397        expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.codexRoot);
398      });
399    });
400  });