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