finishes install + uninstall

This commit is contained in:
Ben 2025-04-09 10:27:00 +02:00
parent 7730176ecc
commit 0ed0b0a218
No known key found for this signature in database
GPG Key ID: 0F16E812E736C24B
11 changed files with 232 additions and 30 deletions

View File

@ -0,0 +1,8 @@
import { vi } from "vitest";
export const mockInstaller = {
isCodexInstalled: vi.fn(),
getCodexVersion: vi.fn(),
installCodex: vi.fn(),
uninstallCodex: vi.fn(),
};

View File

@ -6,6 +6,9 @@ export const mockUiService = {
showErrorMessage: vi.fn(),
askMultipleChoice: vi.fn(),
askPrompt: vi.fn(),
createAndStartSpinner: vi.fn(),
stopSpinnerSuccess: vi.fn(),
stopSpinnerError: vi.fn(),
};
export const mockConfigService = {
@ -21,6 +24,7 @@ export const mockFsService = {
isFile: vi.fn(),
readDir: vi.fn(),
makeDir: vi.fn(),
deleteDir: vi.fn(),
};
export const mockShellService = {

View File

@ -39,6 +39,11 @@ export class Installer {
processCallbacks.installSuccessful();
};
uninstallCodex = () => {
this.fs.deleteDir(this.config.codexInstallPath);
this.fs.deleteDir(this.config.dataDir);
};
arePrerequisitesCorrect = async (processCallbacks) => {
if (await this.isCodexInstalled()) {
processCallbacks.warn("Codex is already installed.");
@ -74,7 +79,7 @@ export class Installer {
};
installCodexUnix = async (processCallbacks) => {
if (!await this.ensureUnixDependencies(processCallbacks)) return;
if (!(await this.ensureUnixDependencies(processCallbacks))) return;
await this.shell.run(
"curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh",
);

View File

@ -241,7 +241,9 @@ describe("Installer", () => {
it("ensures unix dependencies", async () => {
await installer.installCodexUnix(processCallbacks);
expect(installer.ensureUnixDependencies).toHaveBeenCalled(processCallbacks);
expect(installer.ensureUnixDependencies).toHaveBeenCalled(
processCallbacks,
);
});
it("returns early if unix dependencies are not met", async () => {
@ -255,9 +257,9 @@ describe("Installer", () => {
});
describe("when dependencies are met", () => {
beforeEach(() =>{
beforeEach(() => {
installer.ensureUnixDependencies.mockResolvedValue(true);
})
});
it("runs the curl command to download the installer", async () => {
await installer.installCodexUnix(processCallbacks);
@ -385,4 +387,20 @@ describe("Installer", () => {
expect(mockConfigService.saveConfig).toHaveBeenCalled();
});
});
describe("uninstallCodex", () => {
it("deletes the codex install path", () => {
installer.uninstallCodex();
expect(mockFsService.deleteDir).toHaveBeenCalledWith(
config.codexInstallPath,
);
});
it("deletes the codex data path", () => {
installer.uninstallCodex();
expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.dataDir);
});
});
});

View File

@ -105,8 +105,18 @@ export async function main() {
const numberSelector = new NumberSelector(uiService);
const shellService = new ShellService();
const osService = new OsService();
const installer = new Installer(configService, shellService, osService, fsService);
const installMenu = new InstallMenu(uiService, configService, pathSelector, installer);
const installer = new Installer(
configService,
shellService,
osService,
fsService,
);
const installMenu = new InstallMenu(
uiService,
configService,
pathSelector,
installer,
);
const configMenu = new ConfigMenu(
uiService,
new MenuLoop(),

View File

@ -50,4 +50,8 @@ export class FsService {
moveDir = (oldPath, newPath) => {
fs.moveSync(oldPath, newPath);
};
deleteDir = (dir) => {
fs.rmSync(dir, { recursive: true, force: true });
};
}

View File

@ -91,7 +91,7 @@ export class UiService {
createAndStartSpinner = (message) => {
return createSpinner(message).start();
}
};
stopSpinnerSuccess = (spinner) => {
if (spinner == undefined) return;

View File

@ -13,20 +13,7 @@ export class InstallMenu {
} else {
await this.showInstallMenu();
}
}
showUninstallMenu = async () => {
await this.ui.askMultipleChoice("Codex is installed", [
{
label: "Uninstall",
action: this.performUninstall,
},
{
label: "Cancel",
action: this.doNothing,
},
]);
}
};
showInstallMenu = async () => {
await this.ui.askMultipleChoice("Configure your Codex installation", [
@ -49,6 +36,41 @@ export class InstallMenu {
]);
};
showUninstallMenu = async () => {
await this.ui.askMultipleChoice("Codex is installed", [
{
label: "Uninstall",
action: this.showConfirmUninstall,
},
{
label: "Cancel",
action: this.doNothing,
},
]);
};
showConfirmUninstall = async () => {
this.ui.showInfoMessage(
"You are about to:\n" +
" - Uninstall the Codex application\n" +
" - Delete the data stored in your Codex node",
);
await this.ui.askMultipleChoice(
"Are you sure you want to uninstall Codex?",
[
{
label: "No",
action: this.doNothing,
},
{
label: "Yes",
action: this.performUninstall,
},
],
);
};
selectInstallPath = async () => {
this.config.codexInstallPath = await this.pathSelector.show(
this.config.codexInstallPath,
@ -66,7 +88,9 @@ export class InstallMenu {
await this.installer.installCodex(this);
};
performUninstall = async () => {};
performUninstall = async () => {
this.installer.uninstallCodex();
};
doNothing = async () => {};
@ -77,12 +101,12 @@ export class InstallMenu {
downloadSuccessful = () => {
this.ui.showInfoMessage("Download successful...");
}
};
installSuccessful = () => {
this.ui.showInfoMessage("Installation successful!");
this.ui.stopSpinnerSuccess(this.installSpinner);
}
};
warn = (message) => {
this.ui.showErrorMessage(message);

View File

@ -3,6 +3,7 @@ import { InstallMenu } from "./installMenu.js";
import { mockUiService } from "../__mocks__/service.mocks.js";
import { mockConfigService } from "../__mocks__/service.mocks.js";
import { mockPathSelector } from "../__mocks__/utils.mocks.js";
import { mockInstaller } from "../__mocks__/handler.mocks.js";
describe("InstallMenu", () => {
const config = {
@ -18,11 +19,35 @@ describe("InstallMenu", () => {
mockUiService,
mockConfigService,
mockPathSelector,
mockInstaller,
);
});
describe("show", () => {
beforeEach(() => {
installMenu.showInstallMenu = vi.fn();
installMenu.showUninstallMenu = vi.fn();
});
it("shows uninstall menu when codex is installed", async () => {
mockInstaller.isCodexInstalled.mockResolvedValue(true);
await installMenu.show();
expect(installMenu.showUninstallMenu).toHaveBeenCalled();
});
it("shows install menu when codex is not installed", async () => {
mockInstaller.uninstallCodex.mockResolvedValue(false);
await installMenu.show();
expect(installMenu.showInstallMenu).toHaveBeenCalled();
});
});
it("displays the install menu", async () => {
await installMenu.show();
await installMenu.showInstallMenu();
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
"Configure your Codex installation",
[
@ -46,6 +71,47 @@ describe("InstallMenu", () => {
);
});
it("displays the uninstall menu", async () => {
await installMenu.showUninstallMenu();
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
"Codex is installed",
[
{
label: "Uninstall",
action: installMenu.showConfirmUninstall,
},
{
label: "Cancel",
action: installMenu.doNothing,
},
],
);
});
it("confirms uninstall", async () => {
await installMenu.showConfirmUninstall();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"You are about to:\n" +
" - Uninstall the Codex application\n" +
" - Delete the data stored in your Codex node",
);
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
"Are you sure you want to uninstall Codex?",
[
{
label: "No",
action: installMenu.doNothing,
},
{
label: "Yes",
action: installMenu.performUninstall,
},
],
);
});
it("allows selecting the install path", async () => {
const originalPath = config.codexInstallPath;
const newPath = "/new/path";
@ -68,4 +134,66 @@ describe("InstallMenu", () => {
"This option is not currently available.",
);
});
it("calls installed for installation", async () => {
await installMenu.performInstall();
expect(mockInstaller.installCodex).toHaveBeenCalledWith(installMenu);
});
it("calls installer for deinstallation", async () => {
await installMenu.performUninstall();
expect(mockInstaller.uninstallCodex).toHaveBeenCalled();
});
describe("process callback handling", () => {
const mockSpinner = {
isRealSpinner: "no srry",
};
beforeEach(() => {
mockUiService.createAndStartSpinner.mockReturnValue(mockSpinner);
});
it("creates spinner on installStarts", () => {
installMenu.installStarts();
expect(installMenu.installSpinner).toBe(mockSpinner);
expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith(
"Installing...",
);
});
it("shows download success message", () => {
installMenu.downloadSuccessful();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"Download successful...",
);
});
it("shows install success message", () => {
installMenu.installSpinner = mockSpinner;
installMenu.installSuccessful();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"Installation successful!",
);
expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith(
mockSpinner,
);
});
it("shows warnings", () => {
const message = "warning!";
installMenu.installSpinner = mockSpinner;
installMenu.warn(message);
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(message);
expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith(mockSpinner);
});
});
});

View File

@ -143,8 +143,7 @@ export class PathSelector {
try {
const entries = this.fs.readDir(fullPath);
return entries.filter((entry) => this.isSubDir(entry));
}
catch {
} catch {
return [];
}
};

View File

@ -102,12 +102,14 @@ describe("PathSelector", () => {
});
it("handles non-existing paths", async () => {
mockFsService.readDir.mockImplementationOnce(() => { throw new Error("A!"); });
mockFsService.readDir.mockImplementationOnce(() => {
throw new Error("A!");
});
await pathSelector.downOne();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"There are no subdirectories here."
"There are no subdirectories here.",
);
});