From 39ec17bc20a8759bc0d9e74cf78006be554dab8d Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Fri, 14 Feb 2025 04:45:16 -0300 Subject: [PATCH 1/3] feat: new log and spinner class --- package-lock.json | 289 ++++++++++++++---- package.json | 2 + src/commands/general/stop.ts | 11 +- src/commands/update/ollama.ts | 110 ++++--- src/commands/validators/validators.ts | 103 +++---- src/lib/actions/BaseAction.ts | 74 ++++- tests/actions/ollama.test.ts | 87 ++---- tests/actions/stop.test.ts | 27 +- tests/actions/validators.test.ts | 415 +++++++++++++++++--------- tests/libs/baseAction.test.ts | 175 +++++++++++ 10 files changed, 921 insertions(+), 372 deletions(-) create mode 100644 tests/libs/baseAction.test.ts diff --git a/package-lock.json b/package-lock.json index 9bcc27d7..c19b4918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "chalk": "^5.4.1", "commander": "^12.0.0", "dockerode": "^4.0.2", "dotenv": "^16.4.5", @@ -18,6 +19,7 @@ "inquirer": "^9.2.19", "node-fetch": "^2.7.0", "open": "^10.1.0", + "ora": "^8.2.0", "update-check": "^1.5.4", "uuid": "^9.0.1", "viem": "^2.21.54", @@ -2330,19 +2332,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.0.tgz", - "integrity": "sha512-ZkD35Mx92acjB2yNJgziGqT9oKHEOxjTBTDRpOsRWtdecL/0jM3z5kM/CTzHWvHIen1GvkM85p6TuFfDGfc8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/boxen/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -2588,16 +2577,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -4139,6 +4124,22 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4561,7 +4562,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5227,6 +5227,45 @@ "node": ">=18" } }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6258,6 +6297,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/loupe": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", @@ -6421,7 +6476,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6808,28 +6862,176 @@ } }, "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/os-name": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/os-name/-/os-name-5.1.0.tgz", @@ -8442,7 +8644,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8620,7 +8821,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9330,19 +9530,6 @@ "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.0.tgz", - "integrity": "sha512-ZkD35Mx92acjB2yNJgziGqT9oKHEOxjTBTDRpOsRWtdecL/0jM3z5kM/CTzHWvHIen1GvkM85p6TuFfDGfc8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index eccdfc12..9d5af5a6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "typescript": "^5.4.5" }, "dependencies": { + "chalk": "^5.4.1", "commander": "^12.0.0", "dockerode": "^4.0.2", "dotenv": "^16.4.5", @@ -64,6 +65,7 @@ "inquirer": "^9.2.19", "node-fetch": "^2.7.0", "open": "^10.1.0", + "ora": "^8.2.0", "update-check": "^1.5.4", "uuid": "^9.0.1", "viem": "^2.21.54", diff --git a/src/commands/general/stop.ts b/src/commands/general/stop.ts index 4583f647..39da59bd 100644 --- a/src/commands/general/stop.ts +++ b/src/commands/general/stop.ts @@ -11,15 +11,16 @@ export class StopAction extends BaseAction { } public async stop(): Promise { - try{ + try { await this.confirmPrompt( "Are you sure you want to stop all running GenLayer containers? This will halt all active processes." ); - console.log(`Stopping Docker containers...`); + + this.startSpinner("Stopping Docker containers..."); await this.simulatorService.stopDockerContainers(); - console.log(`All running GenLayer containers have been successfully stopped.`); - }catch (error) { - console.error("An error occurred while stopping the containers:", error) + this.succeedSpinner("All running GenLayer containers have been successfully stopped."); + } catch (error) { + this.failSpinner("An error occurred while stopping the containers.", error); } } } diff --git a/src/commands/update/ollama.ts b/src/commands/update/ollama.ts index 6f96f54e..6fb88441 100644 --- a/src/commands/update/ollama.ts +++ b/src/commands/update/ollama.ts @@ -1,72 +1,67 @@ import Docker from "dockerode"; import { rpcClient } from "../../lib/clients/jsonRpcClient"; +import { BaseAction } from "../../lib/actions/BaseAction"; -export class OllamaAction { +export class OllamaAction extends BaseAction { private docker: Docker; constructor() { + super(); this.docker = new Docker(); } async updateModel(modelName: string) { - const providersAndModels = await rpcClient.request({ - method: "sim_getProvidersAndModels", - params: [], - }); - - const existingOllamaProvider = providersAndModels.result.find( - (entry: any) => entry.plugin === "ollama" - ); - - if (!existingOllamaProvider) { - throw new Error("No existing 'ollama' provider found. Unable to add/update a model."); - } + try { + this.startSpinner(`Updating model "${modelName}"...`); - await this.executeModelCommand( - "pull", - modelName, - `Model "${modelName}" updated successfully` - ); - - const existingModel = providersAndModels.result.some( - (entry: any) => - entry.plugin === "ollama" && entry.model === modelName - ); - - if (!existingModel) { - console.log(`Model "${modelName}" not found in Provider Presets. Adding...`); - - const newModelConfig = { - config: existingOllamaProvider.config, - model: modelName, - plugin: "ollama", - plugin_config: existingOllamaProvider.plugin_config, - provider: "ollama", - }; - - await rpcClient.request({ - method: "sim_addProvider", - params: [newModelConfig], + const providersAndModels = await rpcClient.request({ + method: "sim_getProvidersAndModels", + params: [], }); - console.log(`Model "${modelName}" added successfully.`); + const existingOllamaProvider = providersAndModels.result.find( + (entry: any) => entry.plugin === "ollama" + ); + + if (!existingOllamaProvider) { + return this.failSpinner("No existing 'ollama' provider found. Unable to add/update a model."); + } + + await this.executeModelCommand("pull", modelName, `Model "${modelName}" updated successfully`); + + const existingModel = providersAndModels.result.some( + (entry: any) => entry.plugin === "ollama" && entry.model === modelName + ); + if (!existingModel) { + this.startSpinner(`Adding model "${modelName}" to Provider Presets...`); + + const newModelConfig = { + config: existingOllamaProvider.config, + model: modelName, + plugin: "ollama", + plugin_config: existingOllamaProvider.plugin_config, + provider: "ollama", + }; + + await rpcClient.request({ + method: "sim_addProvider", + params: [newModelConfig], + }); + this.succeedSpinner(`Model "${modelName}" added successfully.`); + } + } catch (error) { + this.failSpinner(`Error updating model "${modelName}"`, error); } } async removeModel(modelName: string) { - await this.executeModelCommand( - "rm", - modelName, - `Model "${modelName}" removed successfully` - ); + await this.executeModelCommand("rm", modelName, `Model "${modelName}" removed successfully`); } - private async executeModelCommand( - command: string, - modelName: string, - successMessage: string - ) { + private async executeModelCommand(command: string, modelName: string, successMessage: string) { try { + this.startSpinner(`Executing '${command}' command on model "${modelName}"...`); + let success = false; const ollamaContainer = this.docker.getContainer("ollama"); const exec = await ollamaContainer.exec({ @@ -74,12 +69,13 @@ export class OllamaAction { AttachStdout: true, AttachStderr: true, }); - const stream = await exec.start({Detach: false, Tty: false}); + const stream = await exec.start({ Detach: false, Tty: false }); stream.on("data", (chunk: any) => { - const chunkStr = chunk.toString(); - console.log(chunkStr); - if (chunkStr.includes("success") || chunkStr.includes("deleted")) { + const output = chunk.toString(); + this.setSpinnerText(output.trim()); + + if (output.includes("success") || output.includes("deleted")) { success = true; } }); @@ -87,17 +83,17 @@ export class OllamaAction { await new Promise((resolve, reject) => { stream.on("end", () => { if (success) { + this.succeedSpinner(successMessage); resolve(); } else { + this.failSpinner(`Failed to execute '${command}' on model "${modelName}".`); reject('internal error'); } }); stream.on("error", reject); }); - - console.log(successMessage); - }catch (error) { - console.error(`Error executing command "${command}" on model "${modelName}":`, error); + } catch (error) { + this.failSpinner(`Error executing command "${command}" on model "${modelName}"`, error); } } } diff --git a/src/commands/validators/validators.ts b/src/commands/validators/validators.ts index 99a4024e..1c8a4fe4 100644 --- a/src/commands/validators/validators.ts +++ b/src/commands/validators/validators.ts @@ -31,26 +31,25 @@ export class ValidatorsAction extends BaseAction { public async getValidator(options: ValidatorOptions): Promise { try { if (options.address) { - console.log(`Fetching validator with address: ${options.address}`); + this.startSpinner(`Fetching validator with address: ${options.address}`); const result = await rpcClient.request({ method: "sim_getValidator", params: [options.address], }); - console.log("Validator Details:", result.result); + this.succeedSpinner(`Successfully fetched validator with address: ${options.address}`, result.result); } else { - console.log("Fetching all validators..."); + this.startSpinner(`Fetching all validators...`); const result = await rpcClient.request({ method: "sim_getAllValidators", params: [], }); - - console.log("All Validators:", result.result); + this.succeedSpinner('Successfully fetched all validators.', result.result) } } catch (error) { - console.error("Error fetching validators:", error); + this.failSpinner("Error fetching validators", error); } } @@ -58,65 +57,60 @@ export class ValidatorsAction extends BaseAction { try { if (options.address) { await this.confirmPrompt(`This command will delete the validator with the address: ${options.address}. Do you want to continue?`); - console.log(`Deleting validator with address: ${options.address}`); + this.startSpinner(`Deleting validator with address: ${options.address}`); const result = await rpcClient.request({ method: "sim_deleteValidator", params: [options.address], }); - console.log("Deleted Address:", result.result); + this.succeedSpinner(`Deleted Address: ${result.result}`); } else { await this.confirmPrompt(`This command will delete all validators. Do you want to continue?`); - console.log("Deleting all validators..."); + this.startSpinner("Deleting all validators..."); await rpcClient.request({ method: "sim_deleteAllValidators", params: [], }); - console.log("Successfully deleted all validators"); + this.succeedSpinner("Successfully deleted all validators"); } } catch (error) { - console.error("Error deleting validators:", error); + this.failSpinner("Error deleting validators", error); } } public async countValidators(): Promise { try { - console.log("Counting all validators..."); + this.startSpinner("Counting all validators..."); const result = await rpcClient.request({ method: "sim_countValidators", params: [], }); - - console.log("Total Validators:", result.result); + this.succeedSpinner(`Total Validators: ${result.result}`); } catch (error) { - console.error("Error counting validators:", error); + this.failSpinner("Error counting validators", error); } } public async updateValidator(options: UpdateValidatorOptions): Promise { try { - console.log(`Fetching validator with address: ${options.address}...`); + this.startSpinner(`Fetching validator with address: ${options.address}...`); const currentValidator = await rpcClient.request({ method: "sim_getValidator", params: [options.address], }); - if (!currentValidator.result) { - throw new Error(`Validator with address ${options.address} not found.`); - } - - console.log("Current Validator Details:", currentValidator.result); + this.log("Current Validator Details:", currentValidator.result); const parsedStake = options.stake ? parseInt(options.stake, 10) : currentValidator.result.stake; if (isNaN(parsedStake) || parsedStake < 0) { - return console.error("Invalid stake value. Stake must be a positive integer."); + return this.failSpinner("Invalid stake value. Stake must be a positive integer."); } const updatedValidator = { @@ -127,7 +121,9 @@ export class ValidatorsAction extends BaseAction { config: options.config ? JSON.parse(options.config) : currentValidator.result.config, }; - console.log("Updated Validator Details:", updatedValidator); + this.log("Updated Validator Details:", updatedValidator); + + this.setSpinnerText('Updating Validator...'); const result = await rpcClient.request({ method: "sim_updateValidator", @@ -140,9 +136,9 @@ export class ValidatorsAction extends BaseAction { ], }); - console.log("Validator successfully updated:", result.result); + this.succeedSpinner("Validator successfully updated", result.result); } catch (error) { - console.error("Error updating validator:", error); + this.failSpinner("Error updating validator", error); } } @@ -150,21 +146,21 @@ export class ValidatorsAction extends BaseAction { try { const count = parseInt(options.count, 10); if (isNaN(count) || count < 1) { - return console.error("Invalid count. Please provide a positive integer."); + return this.logError("Invalid count. Please provide a positive integer."); } - console.log(`Creating ${count} random validator(s)...`); - console.log(`Providers: ${options.providers.length > 0 ? options.providers.join(", ") : "None"}`); - console.log(`Models: ${options.models.length > 0 ? options.models.join(", ") : "None"}`); + this.startSpinner(`Creating ${count} random validator(s)...`); + this.log(`Providers: ${options.providers.length > 0 ? options.providers.join(", ") : "All"}`); + this.log(`Models: ${options.models.length > 0 ? options.models.join(", ") : "All"}`); const result = await rpcClient.request({ method: "sim_createRandomValidators", params: [count, 1, 10, options.providers, options.models], }); - console.log("Random validators successfully created:", result.result); + this.succeedSpinner("Random validators successfully created", result.result); } catch (error) { - console.error("Error creating random validators:", error); + this.failSpinner("Error creating random validators", error); } } @@ -172,22 +168,23 @@ export class ValidatorsAction extends BaseAction { try { const stake = parseInt(options.stake, 10); if (isNaN(stake) || stake < 1) { - return console.error("Invalid stake. Please provide a positive integer."); + return this.logError("Invalid stake. Please provide a positive integer."); } if (options.model && !options.provider) { - return console.error("You must specify a provider if using a model."); + return this.logError("You must specify a provider if using a model."); } - console.log("Fetching available providers and models..."); + this.startSpinner("Fetching available providers and models..."); const providersAndModels = await rpcClient.request({ method: "sim_getProvidersAndModels", params: [], }); + this.stopSpinner(); if (!providersAndModels.result || providersAndModels.result.length === 0) { - return console.error("No providers or models available."); + return this.logError("No providers or models available."); } const availableProviders = [ @@ -218,7 +215,7 @@ export class ValidatorsAction extends BaseAction { ); if (availableModels.length === 0) { - return console.error("No models available for the selected provider."); + return this.logError("No models available for the selected provider."); } let model = options.model; @@ -241,34 +238,30 @@ export class ValidatorsAction extends BaseAction { ); if (!modelDetails) { - return console.error("Selected model details not found."); + return this.logError("Selected model details not found."); } const config = options.config ? JSON.parse(options.config) : modelDetails.config; - - console.log("Creating validator with the following details:"); - console.log(`Stake: ${stake}`); - console.log(`Provider: ${modelDetails.provider}`); - console.log(`Model: ${modelDetails.model}`); - console.log(`Config:`, config); - console.log(`Plugin:`, modelDetails.plugin); - console.log(`Plugin Config:`, modelDetails.plugin_config); + const params = [ + stake, + modelDetails.provider, + modelDetails.model, + config, + modelDetails.plugin, + modelDetails.plugin_config, + ] + + this.log("Validator details:", params); + this.startSpinner('Creating validator...'); const result = await rpcClient.request({ method: "sim_createValidator", - params: [ - stake, - modelDetails.provider, - modelDetails.model, - config, - modelDetails.plugin, - modelDetails.plugin_config, - ], + params, }); - console.log("Validator successfully created:", result.result); + this.succeedSpinner("Validator successfully created:", result.result); } catch (error) { - console.error("Error creating validator:", error); + this.failSpinner("Error creating validator", error); } } } diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index 93dab902..5ffba7b6 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -1,19 +1,87 @@ import inquirer from "inquirer"; +import chalk from "chalk"; +import ora, { Ora } from "ora"; export class BaseAction { + private spinner: Ora; + + constructor() { + this.spinner = ora({ text: "", spinner: "dots" }); + } + protected async confirmPrompt(message: string): Promise { const answer = await inquirer.prompt([ { type: "confirm", name: "confirmAction", - message: message, + message: chalk.yellow(message), default: true, }, ]); if (!answer.confirmAction) { - console.log("Operation aborted!"); + this.logError("Operation aborted!"); process.exit(0); } } -} + + private formatOutput(data: any): string { + if (data instanceof Error) { + const errorDetails = { + name: data.name, + message: data.message, + ...(Object.keys(data).length ? data : {}), + }; + return JSON.stringify(errorDetails, null, 2); + } + return typeof data === "object" ? JSON.stringify(data, null, 2) : String(data); + } + + protected log(message: string, data?: any): void { + console.log(chalk.white(`\n${message}`)); + if (data) console.log(this.formatOutput(data)); + } + + protected logSuccess(message: string, data?: any): void { + console.log(chalk.green(`\n✔ ${message}`)); + if (data) console.log(chalk.green(this.formatOutput(data))); + } + + protected logInfo(message: string, data?: any): void { + console.log(chalk.blue(`\nℹ ${message}`)); + if (data) console.log(chalk.blue(this.formatOutput(data))); + } + + protected logWarning(message: string, data?: any): void { + console.log(chalk.yellow(`\n⚠ ${message}`)); + if (data) console.log(chalk.yellow(this.formatOutput(data))); + } + + protected logError(message: string, error?: any): void { + console.error(chalk.red(`\n✖ ${message}`)); + if (error) console.error(chalk.red(this.formatOutput(error))); + } + + protected startSpinner(message: string) { + this.spinner.text = chalk.blue(`${message}`); + this.spinner.start(); + } + + protected succeedSpinner(message: string, data?: any): void { + if (data) this.log('Result:', data); + this.spinner.succeed(chalk.green(message)); + } + + protected failSpinner(message: string, error?:any): void { + if (error) this.log('Error:', error); + this.spinner.fail(chalk.red(message)); + } + + protected stopSpinner(): void { + this.spinner.stop(); + } + + protected setSpinnerText(message: string): void { + this.spinner.text = chalk.blue(message); + } +} \ No newline at end of file diff --git a/tests/actions/ollama.test.ts b/tests/actions/ollama.test.ts index 6827c089..76aa080d 100644 --- a/tests/actions/ollama.test.ts +++ b/tests/actions/ollama.test.ts @@ -1,7 +1,6 @@ import { describe, test, vi, beforeEach, afterEach, expect, Mock } from "vitest"; import { OllamaAction } from "../../src/commands/update/ollama"; import { rpcClient } from "../../src/lib/clients/jsonRpcClient"; - import Docker from "dockerode"; vi.mock("dockerode"); @@ -37,6 +36,11 @@ describe("OllamaAction", () => { } as unknown as Docker.Container); Docker.prototype.getContainer = mockGetContainer; + + vi.spyOn(ollamaAction as any, "startSpinner").mockImplementation(() => {}); + vi.spyOn(ollamaAction as any, "setSpinnerText").mockImplementation(() => {}); + vi.spyOn(ollamaAction as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(ollamaAction as any, "failSpinner").mockImplementation(() => {}); }); afterEach(() => { @@ -49,6 +53,7 @@ describe("OllamaAction", () => { config: { key: "value" }, plugin_config: { pluginKey: "pluginValue" }, }; + vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: [mockProvider], }); @@ -62,10 +67,9 @@ describe("OllamaAction", () => { } }); - console.log = vi.fn(); - await ollamaAction.updateModel("mocked_model"); + expect(ollamaAction["startSpinner"]).toHaveBeenCalledWith(`Updating model "mocked_model"...`); expect(mockGetContainer).toHaveBeenCalledWith("ollama"); expect(mockExec).toHaveBeenCalledWith({ Cmd: ["ollama", "pull", "mocked_model"], @@ -73,10 +77,8 @@ describe("OllamaAction", () => { AttachStderr: true, }); expect(mockStart).toHaveBeenCalledWith({ Detach: false, Tty: false }); - expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function)); - expect(console.log).toHaveBeenCalledWith("Mocked output success"); - expect(console.log).toHaveBeenCalledWith('Model "mocked_model" updated successfully'); + expect(ollamaAction["setSpinnerText"]).toHaveBeenCalledWith("Mocked output success"); + expect(ollamaAction["succeedSpinner"]).toHaveBeenCalledWith(`Model "mocked_model" updated successfully`); }); test("should remove the model using 'rm'", async () => { @@ -89,10 +91,9 @@ describe("OllamaAction", () => { } }); - console.log = vi.fn(); - await ollamaAction.removeModel("mocked_model"); + expect(ollamaAction["startSpinner"]).toHaveBeenCalledWith(`Executing 'rm' command on model "mocked_model"...`); expect(mockGetContainer).toHaveBeenCalledWith("ollama"); expect(mockExec).toHaveBeenCalledWith({ Cmd: ["ollama", "rm", "mocked_model"], @@ -100,10 +101,8 @@ describe("OllamaAction", () => { AttachStderr: true, }); expect(mockStart).toHaveBeenCalledWith({ Detach: false, Tty: false }); - expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function)); - expect(console.log).toHaveBeenCalledWith("Mocked output success"); - expect(console.log).toHaveBeenCalledWith('Model "mocked_model" removed successfully'); + expect(ollamaAction["setSpinnerText"]).toHaveBeenCalledWith("Mocked output success"); + expect(ollamaAction["succeedSpinner"]).toHaveBeenCalledWith(`Model "mocked_model" removed successfully`); }); test("should log an error if an exception occurs during 'pull'", async () => { @@ -112,48 +111,26 @@ describe("OllamaAction", () => { config: { key: "value" }, plugin_config: { pluginKey: "pluginValue" }, }; + vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: [mockProvider], }); const error = new Error("Mocked error"); - mockGetContainer.mockReturnValueOnce( - { - exec: () => { - throw new Error("Mocked error"); - } - } - ); - console.error = vi.fn(); + mockExec.mockRejectedValue(error); await ollamaAction.updateModel("mocked_model"); - expect(mockGetContainer).toHaveBeenCalledWith("ollama"); - expect(console.error).toHaveBeenCalledWith( - 'Error executing command "pull" on model "mocked_model":', - error - ); + expect(ollamaAction["failSpinner"]).toHaveBeenCalledWith(`Error executing command "pull" on model "mocked_model"`, error); }); test("should log an error if an exception occurs during 'rm'", async () => { const error = new Error("Mocked error"); - mockGetContainer.mockReturnValueOnce( - { - exec: () => { - throw new Error("Mocked error"); - } - } - ); - - console.error = vi.fn(); + mockExec.mockRejectedValue(error); await ollamaAction.removeModel("mocked_model"); - expect(mockGetContainer).toHaveBeenCalledWith("ollama"); - expect(console.error).toHaveBeenCalledWith( - 'Error executing command "rm" on model "mocked_model":', - error - ); + expect(ollamaAction["failSpinner"]).toHaveBeenCalledWith(`Error executing command "rm" on model "mocked_model"`, error); }); test("should throw an error if no 'ollama' provider exists during updateModel", async () => { @@ -161,21 +138,14 @@ describe("OllamaAction", () => { result: [], }); - const modelName = "mocked_model"; + await ollamaAction.updateModel("mocked_model"); - await expect(ollamaAction.updateModel(modelName)).rejects.toThrowError( + expect(ollamaAction["failSpinner"]).toHaveBeenCalledWith( "No existing 'ollama' provider found. Unable to add/update a model." ); - - expect(rpcClient.request).toHaveBeenCalledWith({ - method: "sim_getProvidersAndModels", - params: [], - }); }); test("should reject with an error if success is not set to true", async () => { - console.error = vi.fn(); - const mockProvider = { plugin: "ollama", config: { key: "value" }, @@ -195,14 +165,21 @@ describe("OllamaAction", () => { } }); - console.log = vi.fn(); - console.error = vi.fn(); - await ollamaAction.updateModel("mocked_model"); - expect(console.error).toHaveBeenCalledWith( - 'Error executing command "pull" on model "mocked_model":', 'internal error' + expect(ollamaAction["failSpinner"]).toHaveBeenCalledWith( + `Failed to execute 'pull' on model "mocked_model".` ); }); -}); + test("should log an error if an exception occurs inside updateModel", async () => { + const mockError = new Error("Mocked error"); + + vi.mocked(rpcClient.request).mockRejectedValue(mockError); + + await ollamaAction.updateModel("mocked_model"); + + expect(ollamaAction["failSpinner"]).toHaveBeenCalledWith(`Error updating model "mocked_model"`, mockError); + }); + +}); \ No newline at end of file diff --git a/tests/actions/stop.test.ts b/tests/actions/stop.test.ts index 9a308894..fdd1d354 100644 --- a/tests/actions/stop.test.ts +++ b/tests/actions/stop.test.ts @@ -2,6 +2,8 @@ import { describe, test, vi, beforeEach, afterEach, expect } from "vitest"; import { StopAction } from "../../src/commands/general/stop"; import { SimulatorService } from "../../src/lib/services/simulator"; import { ISimulatorService } from "../../src/lib/interfaces/ISimulatorService"; +import chalk from "chalk"; + import inquirer from "inquirer"; vi.mock("../../src/lib/services/simulator"); @@ -22,6 +24,10 @@ describe("StopAction", () => { stopAction = new StopAction(); (stopAction as any).simulatorService = mockSimulatorService; + + vi.spyOn(stopAction as any, "startSpinner").mockImplementation(() => {}); + vi.spyOn(stopAction as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(stopAction as any, "failSpinner").mockImplementation(() => {}); }); afterEach(() => { @@ -37,22 +43,35 @@ describe("StopAction", () => { { type: "confirm", name: "confirmAction", - message: "Are you sure you want to stop all running GenLayer containers? This will halt all active processes.", + message: chalk.yellow("Are you sure you want to stop all running GenLayer containers? This will halt all active processes."), default: true, }, ]); expect(mockSimulatorService.stopDockerContainers).toHaveBeenCalled(); + expect(stopAction["succeedSpinner"]).toHaveBeenCalledWith( + "All running GenLayer containers have been successfully stopped." + ); }); test("should abort if user cancels", async () => { vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: false }); - console.log = vi.fn(); - await stopAction.stop(); expect(inquirer.prompt).toHaveBeenCalled(); - expect(console.log).toHaveBeenCalledWith("Operation aborted!"); expect(mockSimulatorService.stopDockerContainers).not.toHaveBeenCalled(); }); + + test("should handle errors and call failSpinner", async () => { + vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true }); + const error = new Error("Test Error"); + mockSimulatorService.stopDockerContainers = vi.fn().mockRejectedValue(error); + + await stopAction.stop(); + + expect(stopAction["failSpinner"]).toHaveBeenCalledWith( + "An error occurred while stopping the containers.", + error + ); + }); }); diff --git a/tests/actions/validators.test.ts b/tests/actions/validators.test.ts index 3e9b9979..40cc6c9a 100644 --- a/tests/actions/validators.test.ts +++ b/tests/actions/validators.test.ts @@ -17,6 +17,15 @@ describe("ValidatorsAction", () => { beforeEach(() => { vi.clearAllMocks(); validatorsAction = new ValidatorsAction(); + + vi.spyOn(validatorsAction as any, "logSuccess").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "logError").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "startSpinner").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "failSpinner").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "log").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "stopSpinner").mockImplementation(() => {}); + vi.spyOn(validatorsAction as any, "setSpinnerText").mockImplementation(() => {}); }); afterEach(() => { @@ -29,30 +38,44 @@ describe("ValidatorsAction", () => { const mockResponse = { result: { id: 1, name: "Validator1" } }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.getValidator({ address: mockAddress }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith( + `Fetching validator with address: ${mockAddress}` + ); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getValidator", params: [mockAddress], }); - expect(console.log).toHaveBeenCalledWith("Validator Details:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith( + `Successfully fetched validator with address: ${mockAddress}`, + mockResponse.result + ); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should fetch all validators when no address is provided", async () => { const mockResponse = { result: [{ id: 1 }, { id: 2 }] }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.getValidator({}); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching all validators..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getAllValidators", params: [], }); - expect(console.log).toHaveBeenCalledWith("All Validators:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith( + "Successfully fetched all validators.", + mockResponse.result + ); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if an exception occurs while fetching a specific validator", async () => { @@ -61,15 +84,19 @@ describe("ValidatorsAction", () => { vi.mocked(rpcClient.request).mockRejectedValue(mockError); - console.error = vi.fn(); - await validatorsAction.getValidator({ address: mockAddress }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith( + `Fetching validator with address: ${mockAddress}` + ); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getValidator", params: [mockAddress], }); - expect(console.error).toHaveBeenCalledWith("Error fetching validators:", mockError); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error fetching validators", mockError); + expect(validatorsAction["succeedSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if an exception occurs while fetching all validators", async () => { @@ -77,63 +104,78 @@ describe("ValidatorsAction", () => { vi.mocked(rpcClient.request).mockRejectedValue(mockError); - console.error = vi.fn(); - await validatorsAction.getValidator({}); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching all validators..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getAllValidators", params: [], }); - expect(console.error).toHaveBeenCalledWith("Error fetching validators:", mockError); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error fetching validators", mockError); + expect(validatorsAction["succeedSpinner"]).not.toHaveBeenCalled(); }); }); describe("deleteValidator", () => { - test("should delete a specific validator", async () => { - const mockAddress = "mocked_address"; + test("should delete a specific validator", async () => { + const mockAddress = "0x725a9D2D572E8833059a3e9a844791aF185C5Ff4"; vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true }); - vi.mocked(rpcClient.request).mockResolvedValue({ result: { id: 1 } }); - - console.log = vi.fn(); + vi.mocked(rpcClient.request).mockResolvedValue({ result: mockAddress }); await validatorsAction.deleteValidator({ address: mockAddress }); - expect(inquirer.prompt).toHaveBeenCalled(); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith( + `Deleting validator with address: ${mockAddress}` + ); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_deleteValidator", params: [mockAddress], }); - expect(console.log).toHaveBeenCalledWith("Deleted Address:", { id: 1 }); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith( + `Deleted Address: ${mockAddress}` + ); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should delete all validators when no address is provided", async () => { vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true }); vi.mocked(rpcClient.request).mockResolvedValue({}); - console.log = vi.fn(); - await validatorsAction.deleteValidator({}); - expect(inquirer.prompt).toHaveBeenCalled(); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith( + "Deleting all validators..." + ); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_deleteAllValidators", params: [], }); - expect(console.log).toHaveBeenCalledWith("Successfully deleted all validators"); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith( + "Successfully deleted all validators" + ); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should abort deletion if user declines confirmation", async () => { vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: false }); - console.log = vi.fn(); - - await validatorsAction.deleteValidator({ address: "mocked_address" }) + await validatorsAction.deleteValidator({ address: "mocked_address" }); expect(inquirer.prompt).toHaveBeenCalled(); - expect(console.log).toHaveBeenCalledWith("Operation aborted!"); + expect(validatorsAction["logError"]).toHaveBeenCalledWith("Operation aborted!"); expect(rpcClient.request).not.toHaveBeenCalled(); + expect(validatorsAction["startSpinner"]).not.toHaveBeenCalled(); + expect(validatorsAction["succeedSpinner"]).not.toHaveBeenCalled(); }); + }); describe("countValidators", () => { @@ -141,15 +183,17 @@ describe("ValidatorsAction", () => { const mockResponse = { result: 42 }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.countValidators(); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Counting all validators..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_countValidators", params: [], }); - expect(console.log).toHaveBeenCalledWith("Total Validators:", 42); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Total Validators: 42"); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if an exception occurs while counting validators", async () => { @@ -157,16 +201,19 @@ describe("ValidatorsAction", () => { vi.mocked(rpcClient.request).mockRejectedValue(mockError); - console.error = vi.fn(); - await validatorsAction.countValidators(); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Counting all validators..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_countValidators", params: [], }); - expect(console.error).toHaveBeenCalledWith("Error counting validators:", mockError); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error counting validators", mockError); + expect(validatorsAction["succeedSpinner"]).not.toHaveBeenCalled(); }); + }); describe("createValidator", () => { @@ -192,14 +239,37 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce({ selectedProvider: "Provider1" }) .mockResolvedValueOnce({ selectedModel: "Model1" }); - console.log = vi.fn(); - await validatorsAction.createValidator({ stake: "10" }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getProvidersAndModels", params: [], }); + + expect(validatorsAction["stopSpinner"]).toHaveBeenCalled(); + + expect(inquirer.prompt).toHaveBeenCalledWith([ + { + type: "list", + name: "selectedProvider", + message: "Select a provider:", + choices: ["Provider1"], + }, + ]); + + expect(inquirer.prompt).toHaveBeenCalledWith([ + { + type: "list", + name: "selectedModel", + message: "Select a model:", + choices: ["Model1"], + }, + ]); + + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating validator..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createValidator", params: [ @@ -211,30 +281,46 @@ describe("ValidatorsAction", () => { { api_key_env_var: "KEY1" }, ], }); - expect(console.log).toHaveBeenCalledWith("Validator successfully created:", { id: 123 }); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith( + "Validator successfully created:", + mockResponse.result + ); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error for invalid stake", async () => { - console.error = vi.fn(); await validatorsAction.createValidator({ stake: "invalid" }); - expect(console.error).toHaveBeenCalledWith("Invalid stake. Please provide a positive integer."); + expect(validatorsAction["logError"]).toHaveBeenCalledWith( + "Invalid stake. Please provide a positive integer." + ); + expect(rpcClient.request).not.toHaveBeenCalled(); + expect(validatorsAction["startSpinner"]).not.toHaveBeenCalled(); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if no providers or models are available", async () => { vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: [] }); - console.error = vi.fn(); - await validatorsAction.createValidator({ stake: "10" }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getProvidersAndModels", params: [], }); - expect(console.error).toHaveBeenCalledWith("No providers or models available."); + + expect(validatorsAction["stopSpinner"]).toHaveBeenCalled(); + + expect(validatorsAction["logError"]).toHaveBeenCalledWith("No providers or models available."); + + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); + expect(validatorsAction["succeedSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if no models are available for the selected provider", async () => { @@ -245,11 +331,22 @@ describe("ValidatorsAction", () => { vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: mockProvidersAndModels }); vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedProvider: "Provider1" }); - console.error = vi.fn(); - await validatorsAction.createValidator({ stake: "10" }); - expect(console.error).toHaveBeenCalledWith("No models available for the selected provider."); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + + expect(validatorsAction["stopSpinner"]).toHaveBeenCalled(); + + expect(inquirer.prompt).toHaveBeenCalledWith([ + { + type: "list", + name: "selectedProvider", + message: "Select a provider:", + choices: ["Provider1"], + }, + ]); + + expect(validatorsAction["logError"]).toHaveBeenCalledWith("No models available for the selected provider."); }); test("should log an error if selected model details are not found", async () => { @@ -267,22 +364,42 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce({ selectedProvider: "Provider1" }) .mockResolvedValueOnce({ selectedModel: "NonExistentModel" }); - console.error = vi.fn(); - await validatorsAction.createValidator({ stake: "10" }); - expect(console.error).toHaveBeenCalledWith("Selected model details not found."); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + + expect(validatorsAction["stopSpinner"]).toHaveBeenCalled(); + + expect(inquirer.prompt).toHaveBeenCalledWith([ + { + type: "list", + name: "selectedProvider", + message: "Select a provider:", + choices: ["Provider1"], + }, + ]); + + expect(inquirer.prompt).toHaveBeenCalledWith([ + { + type: "list", + name: "selectedModel", + message: "Select a model:", + choices: ["Model1"], + }, + ]); + + expect(validatorsAction["logError"]).toHaveBeenCalledWith("Selected model details not found."); }); test("should log an error if an exception occurs during the process", async () => { const mockError = new Error("Unexpected error"); vi.mocked(rpcClient.request).mockRejectedValue(mockError); - console.error = vi.fn(); - await validatorsAction.createValidator({ stake: "10" }); - expect(console.error).toHaveBeenCalledWith("Error creating validator:", mockError); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error creating validator", mockError); }); test("should use user-provided config if specified", async () => { @@ -307,11 +424,22 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce({ selectedProvider: "Provider1" }) .mockResolvedValueOnce({ selectedModel: "Model1" }); - console.log = vi.fn(); - const customConfig = '{"custom_key":"custom_value"}'; await validatorsAction.createValidator({ stake: "10", config: customConfig }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Fetching available providers and models..."); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getProvidersAndModels", + params: [], + }); + expect(validatorsAction["stopSpinner"]).toHaveBeenCalled(); + expect(inquirer.prompt).toHaveBeenCalledWith([ + { type: "list", name: "selectedProvider", message: "Select a provider:", choices: ["Provider1"] }, + ]); + expect(inquirer.prompt).toHaveBeenCalledWith([ + { type: "list", name: "selectedModel", message: "Select a model:", choices: ["Model1"] }, + ]); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating validator..."); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createValidator", params: [ @@ -323,84 +451,100 @@ describe("ValidatorsAction", () => { { api_key_env_var: "KEY1" }, ], }); - expect(console.log).toHaveBeenCalledWith("Validator successfully created:", { id: 123 }); + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Validator successfully created:", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); + }); + + test("should log an error if model is provided without provider", async () => { + vi.spyOn(validatorsAction as any, "logError").mockImplementation(() => {}); + + await validatorsAction.createValidator({ stake: "10", model: "Model1" }); + + expect(validatorsAction["logError"]).toHaveBeenCalledWith("You must specify a provider if using a model."); + expect(rpcClient.request).not.toHaveBeenCalled(); }); }); + describe("createRandomValidators", () => { test("should create random validators with valid count and providers", async () => { const mockResponse = { result: { success: true } }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.createRandomValidators({ count: "5", providers: ["Provider1", "Provider2"], models: [] }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating 5 random validator(s)..."); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Providers: Provider1, Provider2"); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Models: All"); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createRandomValidators", params: [5, 1, 10, ["Provider1", "Provider2"], []], }); - expect(console.log).toHaveBeenCalledWith("Creating 5 random validator(s)..."); - expect(console.log).toHaveBeenCalledWith("Providers: Provider1, Provider2"); - expect(console.log).toHaveBeenCalledWith("Random validators successfully created:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Random validators successfully created", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should create random validators with valid count, providers and models", async () => { const mockResponse = { result: { success: true } }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.createRandomValidators({ count: "10", providers: ["Provider3"], models: ["Model1", "Model2"] }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating 10 random validator(s)..."); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Providers: Provider3"); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Models: Model1, Model2"); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createRandomValidators", params: [10, 1, 10, ["Provider3"], ["Model1", "Model2"]], }); - expect(console.log).toHaveBeenCalledWith("Creating 10 random validator(s)..."); - expect(console.log).toHaveBeenCalledWith("Providers: Provider3"); - expect(console.log).toHaveBeenCalledWith("Models: Model1, Model2"); - expect(console.log).toHaveBeenCalledWith("Random validators successfully created:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Random validators successfully created", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should create random validators with default provider message when providers list is empty", async () => { const mockResponse = { result: { success: true } }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.log = vi.fn(); - await validatorsAction.createRandomValidators({ count: "3", providers: [], models: [] }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating 3 random validator(s)..."); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Providers: All"); + expect(validatorsAction["log"]).toHaveBeenCalledWith("Models: All"); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createRandomValidators", params: [3, 1, 10, [], []], }); - expect(console.log).toHaveBeenCalledWith("Creating 3 random validator(s)..."); - expect(console.log).toHaveBeenCalledWith("Providers: None"); - expect(console.log).toHaveBeenCalledWith("Random validators successfully created:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Random validators successfully created", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should throw an error for invalid count", async () => { - console.error = vi.fn(); - await validatorsAction.createRandomValidators({ count: "invalid", providers: ["Provider1"], models: [] }); - expect(console.error).toHaveBeenCalledWith("Invalid count. Please provide a positive integer."); + expect(validatorsAction["logError"]).toHaveBeenCalledWith("Invalid count. Please provide a positive integer."); expect(rpcClient.request).not.toHaveBeenCalled(); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if rpc request fails", async () => { const mockError = new Error("RPC failure"); vi.mocked(rpcClient.request).mockRejectedValue(mockError); - console.error = vi.fn(); - await validatorsAction.createRandomValidators({ count: "5", providers: ["Provider1"], models: [] }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith("Creating 5 random validator(s)..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createRandomValidators", params: [5, 1, 10, ["Provider1"], []], }); - expect(console.error).toHaveBeenCalledWith("Error creating random validators:", mockError); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error creating random validators", mockError); }); }); @@ -422,14 +566,17 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce(mockCurrentValidator) .mockResolvedValueOnce(mockResponse); - console.log = vi.fn(); - await validatorsAction.updateValidator({ address: mockAddress, stake: "200" }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith(`Fetching validator with address: ${mockAddress}...`); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getValidator", params: [mockAddress], }); + + expect(validatorsAction["log"]).toHaveBeenCalledWith("Current Validator Details:", mockCurrentValidator.result); + expect(validatorsAction["setSpinnerText"]).toHaveBeenCalledWith("Updating Validator..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_updateValidator", params: [ @@ -440,7 +587,9 @@ describe("ValidatorsAction", () => { { max_tokens: 500 }, ], }); - expect(console.log).toHaveBeenCalledWith("Validator successfully updated:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Validator successfully updated", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should fetch and update a validator with new provider and model", async () => { @@ -460,18 +609,17 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce(mockCurrentValidator) .mockResolvedValueOnce(mockResponse); - console.log = vi.fn(); - - await validatorsAction.updateValidator({ - address: mockAddress, - provider: "Provider2", - model: "Model2", - }); + await validatorsAction.updateValidator({ address: mockAddress, provider: "Provider2", model: "Model2" }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith(`Fetching validator with address: ${mockAddress}...`); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getValidator", params: [mockAddress], }); + + expect(validatorsAction["log"]).toHaveBeenCalledWith("Current Validator Details:", mockCurrentValidator.result); + expect(validatorsAction["setSpinnerText"]).toHaveBeenCalledWith("Updating Validator..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_updateValidator", params: [ @@ -482,7 +630,9 @@ describe("ValidatorsAction", () => { { max_tokens: 500 }, ], }); - expect(console.log).toHaveBeenCalledWith("Validator successfully updated:", mockResponse.result); + + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Validator successfully updated", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should fetch and update a validator with new config", async () => { @@ -502,15 +652,18 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce(mockCurrentValidator) .mockResolvedValueOnce(mockResponse); - console.log = vi.fn(); - const newConfig = '{"max_tokens":1000}'; await validatorsAction.updateValidator({ address: mockAddress, config: newConfig }); + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith(`Fetching validator with address: ${mockAddress}...`); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_getValidator", params: [mockAddress], }); + + expect(validatorsAction["log"]).toHaveBeenCalledWith("Current Validator Details:", mockCurrentValidator.result); + expect(validatorsAction["setSpinnerText"]).toHaveBeenCalledWith("Updating Validator..."); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_updateValidator", params: [ @@ -521,28 +674,9 @@ describe("ValidatorsAction", () => { { max_tokens: 1000 }, ], }); - expect(console.log).toHaveBeenCalledWith("Validator successfully updated:", mockResponse.result); - }); - - test("should throw an error if validator is not found", async () => { - const mockAddress = "mocked_address"; - const mockResponse = { result: null }; - - vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); - console.error = vi.fn(); - - await validatorsAction.updateValidator({ address: mockAddress }); - - expect(rpcClient.request).toHaveBeenCalledWith({ - method: "sim_getValidator", - params: [mockAddress], - }); - expect(console.error).toHaveBeenCalledWith( - "Error updating validator:", - new Error(`Validator with address ${mockAddress} not found.`) - ); - expect(rpcClient.request).toHaveBeenCalledTimes(1); + expect(validatorsAction["succeedSpinner"]).toHaveBeenCalledWith("Validator successfully updated", mockResponse.result); + expect(validatorsAction["failSpinner"]).not.toHaveBeenCalled(); }); test("should log an error if updateValidator RPC call fails", async () => { @@ -562,7 +696,7 @@ describe("ValidatorsAction", () => { .mockResolvedValueOnce(mockCurrentValidator) .mockRejectedValueOnce(mockError); - console.error = vi.fn(); + vi.spyOn(validatorsAction as any, "failSpinner").mockImplementation(() => {}); await validatorsAction.updateValidator({ address: mockAddress, stake: "200" }); @@ -570,6 +704,7 @@ describe("ValidatorsAction", () => { method: "sim_getValidator", params: [mockAddress], }); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_updateValidator", params: [ @@ -580,40 +715,36 @@ describe("ValidatorsAction", () => { { max_tokens: 500 }, ], }); - expect(console.error).toHaveBeenCalledWith("Error updating validator:", mockError); - }); - }); - test("should log an error for invalid stake value", async () => { - const mockAddress = "mocked_address"; - const mockCurrentValidator = { - result: { - address: "mocked_address", - stake: 100, - provider: "Provider1", - model: "Model1", - config: { max_tokens: 500 }, - }, - }; - - vi.mocked(rpcClient.request).mockResolvedValue(mockCurrentValidator); - - console.error = vi.fn(); - - await validatorsAction.updateValidator({ address: mockAddress, stake: "-10" }); - - expect(rpcClient.request).toHaveBeenCalledWith({ - method: "sim_getValidator", - params: [mockAddress], + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Error updating validator", mockError); }); - expect(console.error).toHaveBeenCalledWith("Invalid stake value. Stake must be a positive integer."); - expect(rpcClient.request).toHaveBeenCalledTimes(1); - }); - test("should log an error if model is provided without provider", async () => { - console.error = vi.fn(); - await validatorsAction.createValidator({ stake: "10", model: "Model1" }); + test("should log an error for invalid stake value", async () => { + const mockAddress = "mocked_address"; + const mockCurrentValidator = { + result: { + address: "mocked_address", + stake: 100, + provider: "Provider1", + model: "Model1", + config: { max_tokens: 500 }, + }, + }; + + vi.mocked(rpcClient.request).mockResolvedValue(mockCurrentValidator); - expect(console.error).toHaveBeenCalledWith("You must specify a provider if using a model."); - expect(rpcClient.request).not.toHaveBeenCalled(); + vi.spyOn(validatorsAction as any, "failSpinner").mockImplementation(() => {}); + + await validatorsAction.updateValidator({ address: mockAddress, stake: "-10" }); + + expect(validatorsAction["startSpinner"]).toHaveBeenCalledWith(`Fetching validator with address: ${mockAddress}...`); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + + expect(validatorsAction["failSpinner"]).toHaveBeenCalledWith("Invalid stake value. Stake must be a positive integer."); + expect(rpcClient.request).toHaveBeenCalledTimes(1); + }); }); }); \ No newline at end of file diff --git a/tests/libs/baseAction.test.ts b/tests/libs/baseAction.test.ts new file mode 100644 index 00000000..1ac3bad7 --- /dev/null +++ b/tests/libs/baseAction.test.ts @@ -0,0 +1,175 @@ +import {describe, test, vi, beforeEach, afterEach, expect, Mock} from "vitest"; +import { BaseAction } from "../../src/lib/actions/BaseAction"; +import inquirer from "inquirer"; +import ora, { Ora } from "ora"; +import chalk from "chalk"; + +vi.mock("inquirer"); +vi.mock("ora"); + +describe("BaseAction", () => { + let baseAction: BaseAction; + let mockSpinner: Ora; + let consoleSpy: any; + let consoleErrorSpy: any; + + beforeEach(() => { + vi.clearAllMocks(); + consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + mockSpinner = { + start: vi.fn(), + stop: vi.fn(), + succeed: vi.fn(), + fail: vi.fn(), + text: "", + } as unknown as Ora; + + (ora as unknown as Mock).mockReturnValue(mockSpinner); + + baseAction = new BaseAction(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("should start the spinner with a message", () => { + baseAction["startSpinner"]("Loading..."); + expect(mockSpinner.start).toHaveBeenCalled(); + expect(mockSpinner.text).toBe(chalk.blue("Loading...")); + }); + + test("should succeed the spinner with a message", () => { + baseAction["succeedSpinner"]("Success"); + expect(mockSpinner.succeed).toHaveBeenCalledWith(expect.stringContaining("Success")); + }); + + test("should fail the spinner with an error message", () => { + baseAction["failSpinner"]("Failure", new Error("Something went wrong")); + expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining("Failure")); + }); + + test("should stop the spinner", () => { + baseAction["stopSpinner"](); + expect(mockSpinner.stop).toHaveBeenCalled(); + }); + + test("should set spinner text", () => { + baseAction["setSpinnerText"]("Updated text"); + expect(mockSpinner.text).toBe(chalk.blue("Updated text")); + }); + + test("should confirm prompt and proceed when confirmed", async () => { + vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true }); + + await expect(baseAction["confirmPrompt"]("Are you sure?")).resolves.not.toThrow(); + expect(inquirer.prompt).toHaveBeenCalled(); + }); + + test("should confirm prompt and exit when declined", async () => { + vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: false }); + const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process exited"); + }); + + await expect(baseAction["confirmPrompt"]("Are you sure?")).rejects.toThrow("process exited"); + expect(inquirer.prompt).toHaveBeenCalled(); + expect(processExitSpy).toHaveBeenCalledWith(0); + }); + + test("should log a success message", () => { + baseAction["logSuccess"]("Success message"); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("✔ Success message")); + }); + + test("should log an error message", () => { + baseAction["logError"]("Error message"); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("✖ Error message")); + }); + + test("should log a info message", () => { + baseAction["logInfo"]("Info message"); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("ℹ Info message")); + }); + + test("should log a warning message", () => { + baseAction["logWarning"]("Warning message"); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("⚠ Warning message")); + }); + + test("should log a success message with data", () => { + const data = { key: "value" }; + + baseAction["logSuccess"]("Success message", data); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("✔ Success message")); + expect(consoleSpy).toHaveBeenCalledWith(chalk.green(JSON.stringify(data, null, 2))); + }); + + test("should log an error message with error details", () => { + const error = new Error("Something went wrong"); + + baseAction["logError"]("Error message", error); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("✖ Error message")); + expect(consoleErrorSpy).toHaveBeenCalledWith(chalk.red(JSON.stringify({ + name: error.name, + message: error.message, + }, null, 2))); + }); + + test("should log an info message with data", () => { + const data = { info: "This is some info" }; + + baseAction["logInfo"]("Info message", data); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("ℹ Info message")); + expect(consoleSpy).toHaveBeenCalledWith(chalk.blue(JSON.stringify(data, null, 2))); + }); + + test("should log a warning message with data", () => { + const data = { warning: "This is a warning" }; + + baseAction["logWarning"]("Warning message", data); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("⚠ Warning message")); + expect(consoleSpy).toHaveBeenCalledWith(chalk.yellow(JSON.stringify(data, null, 2))); + }); + + test("should succeed the spinner with a message and log result if data is provided", () => { + const mockData = { key: "value" }; + + baseAction["succeedSpinner"]("Success", mockData); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Result:")); + expect(consoleSpy).toHaveBeenCalledWith(JSON.stringify(mockData, null, 2)); + expect(mockSpinner.succeed).toHaveBeenCalledWith(expect.stringContaining("Success")); + }); + + test("should format an Error instance with name, message, and additional properties", () => { + const error = new Error("Something went wrong"); + (error as any).code = 500; + + const result = (baseAction as any).formatOutput(error); + expect(result).toBe(JSON.stringify({ name: "Error", message: "Something went wrong", code: 500 }, null, 2)); + }); + + test("should format an object as JSON string", () => { + const data = { key: "value", num: 42 }; + const result = (baseAction as any).formatOutput(data); + + expect(result).toBe(JSON.stringify(data, null, 2)); + }); + + test("should return a string representation of a primitive", () => { + expect((baseAction as any).formatOutput("Hello")).toBe("Hello"); + expect((baseAction as any).formatOutput(42)).toBe("42"); + expect((baseAction as any).formatOutput(true)).toBe("true"); + }); + +}); From 134df7f99099bbb226d0197ba36c48f015fecf99 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Fri, 14 Feb 2025 12:34:39 -0300 Subject: [PATCH 2/3] feat: adding spinner to keygen command --- src/commands/keygen/create.ts | 13 +++++++------ tests/actions/create.test.ts | 33 +++++++++++++-------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/commands/keygen/create.ts b/src/commands/keygen/create.ts index b5150f23..918bfc8e 100644 --- a/src/commands/keygen/create.ts +++ b/src/commands/keygen/create.ts @@ -1,26 +1,28 @@ import { writeFileSync, existsSync } from "fs"; import { ethers } from "ethers"; import { ConfigFileManager } from "../../lib/config/ConfigFileManager"; +import { BaseAction } from "../../lib/actions/BaseAction"; export interface CreateKeypairOptions { output: string; overwrite: boolean; } -export class KeypairCreator { +export class KeypairCreator extends BaseAction{ private filePathManager: ConfigFileManager; constructor() { + super() this.filePathManager = new ConfigFileManager(); } createKeypairAction(options: CreateKeypairOptions) { try { - + this.startSpinner(`Creating keypair...`); const outputPath = this.filePathManager.getFilePath(options.output); if(existsSync(outputPath) && !options.overwrite) { - console.warn( + this.failSpinner( `The file at ${outputPath} already exists. Use the '--overwrite' option to replace it.` ); return; @@ -35,10 +37,9 @@ export class KeypairCreator { writeFileSync(outputPath, JSON.stringify(keypairData, null, 2)); this.filePathManager.writeConfig('keyPairPath', outputPath); - console.log(`Keypair successfully created and saved to: ${outputPath}`); + this.succeedSpinner(`Keypair successfully created and saved to: ${outputPath}`); } catch (error) { - console.error("Failed to generate keypair:", error); - process.exit(1); + this.failSpinner("Failed to generate keypair:", error); } } } \ No newline at end of file diff --git a/tests/actions/create.test.ts b/tests/actions/create.test.ts index 7c61fef6..101aa734 100644 --- a/tests/actions/create.test.ts +++ b/tests/actions/create.test.ts @@ -31,6 +31,9 @@ describe("KeypairCreator", () => { beforeEach(() => { vi.clearAllMocks(); + vi.spyOn(keypairCreator as any, "startSpinner").mockImplementation(() => {}); + vi.spyOn(keypairCreator as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(keypairCreator as any, "failSpinner").mockImplementation(() => {}); vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet); }); @@ -39,16 +42,16 @@ describe("KeypairCreator", () => { }); test("successfully creates and saves a keypair", () => { - const consoleLogSpy = vi.spyOn(console, "log"); vi.mocked(existsSync).mockReturnValue(false); const options = { output: "keypair.json", overwrite: false }; keypairCreator.createKeypairAction(options); + expect(keypairCreator["startSpinner"]).toHaveBeenCalledWith("Creating keypair..."); expect(ethers.Wallet.createRandom).toHaveBeenCalledTimes(1); - expect(keypairCreator["filePathManager"].getFilePath).toHaveBeenCalledWith("keypair.json"); + expect(writeFileSync).toHaveBeenCalledWith( "/mocked/path/keypair.json", JSON.stringify( @@ -66,15 +69,12 @@ describe("KeypairCreator", () => { "/mocked/path/keypair.json" ); - expect(consoleLogSpy).toHaveBeenCalledWith( + expect(keypairCreator["succeedSpinner"]).toHaveBeenCalledWith( "Keypair successfully created and saved to: /mocked/path/keypair.json" ); }); test("skips creation if file exists and overwrite is false", () => { - - - const consoleWarnSpy = vi.spyOn(console, "warn"); vi.mocked(existsSync).mockReturnValue(true); const options = { output: "keypair.json", overwrite: false }; @@ -82,20 +82,19 @@ describe("KeypairCreator", () => { expect(ethers.Wallet.createRandom).not.toHaveBeenCalled(); expect(writeFileSync).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith( + expect(keypairCreator["failSpinner"]).toHaveBeenCalledWith( "The file at /mocked/path/keypair.json already exists. Use the '--overwrite' option to replace it." ); }); test("overwrites the file if overwrite is true", () => { - const consoleLogSpy = vi.spyOn(console, "log"); vi.mocked(existsSync).mockReturnValue(true); const options = { output: "keypair.json", overwrite: true }; keypairCreator.createKeypairAction(options); + expect(keypairCreator["startSpinner"]).toHaveBeenCalledWith("Creating keypair..."); expect(ethers.Wallet.createRandom).toHaveBeenCalledTimes(1); - expect(keypairCreator["filePathManager"].getFilePath).toHaveBeenCalledWith("keypair.json"); expect(writeFileSync).toHaveBeenCalledWith( @@ -115,26 +114,20 @@ describe("KeypairCreator", () => { "/mocked/path/keypair.json" ); - expect(consoleLogSpy).toHaveBeenCalledWith( + expect(keypairCreator["succeedSpinner"]).toHaveBeenCalledWith( "Keypair successfully created and saved to: /mocked/path/keypair.json" ); }); test("handles errors during keypair creation", () => { - const consoleErrorSpy = vi.spyOn(console, "error"); - const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit"); - }); + const mockError = new Error("Mocked write error"); vi.mocked(writeFileSync).mockImplementation(() => { - throw new Error("Mocked write error"); + throw mockError; }); - expect(() => { - keypairCreator.createKeypairAction({ output: "keypair.json", overwrite: true }); - }).toThrowError("process.exit"); + keypairCreator.createKeypairAction({ output: "keypair.json", overwrite: true }); - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to generate keypair:", expect.any(Error)); - expect(processExitSpy).toHaveBeenCalledWith(1); + expect(keypairCreator["failSpinner"]).toHaveBeenCalledWith("Failed to generate keypair:", mockError); }); }); From 25967f9d743d9a26c4cd49bc93d9b43a990504b5 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Fri, 14 Feb 2025 14:49:08 -0300 Subject: [PATCH 3/3] feat: adding spinner and logs to command config --- src/commands/config/getSetReset.ts | 30 +++++++++----- tests/actions/getSetReset.test.ts | 63 ++++++++++++------------------ 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/commands/config/getSetReset.ts b/src/commands/config/getSetReset.ts index 2aae7c00..4774edea 100644 --- a/src/commands/config/getSetReset.ts +++ b/src/commands/config/getSetReset.ts @@ -1,44 +1,54 @@ import { ConfigFileManager } from "../../lib/config/ConfigFileManager"; +import { BaseAction } from "../../lib/actions/BaseAction"; -export class ConfigActions { +export class ConfigActions extends BaseAction { private configManager: ConfigFileManager; constructor() { + super(); this.configManager = new ConfigFileManager(); } set(keyValue: string): void { const [key, value] = keyValue.split("="); + this.startSpinner(`Updating configuration: ${key}`); + if (!key || value === undefined) { - console.error("Invalid format. Use key=value."); - process.exit(1); + this.failSpinner("Invalid format. Use 'key=value'."); + return; } + this.configManager.writeConfig(key, value); - console.log(`Configuration updated: ${key}=${value}`); + this.succeedSpinner(`Configuration successfully updated`); } get(key?: string): void { + this.startSpinner(key ? `Retrieving value for: ${key}` : "Retrieving all configurations"); + if (key) { const value = this.configManager.getConfigByKey(key); if (value === null) { - console.log(`No value set for key: ${key}`); + this.failSpinner(`No configuration found for '${key}'.`); } else { - console.log(`${key}=${value}`); + this.succeedSpinner(`Configuration successfully retrieved`, `${key}=${value}`); } } else { const config = this.configManager.getConfig(); - console.log("Current configuration:", JSON.stringify(config, null, 2)); + this.succeedSpinner("All configurations successfully retrieved", JSON.stringify(config, null, 2)); } } reset(key: string): void { + this.startSpinner(`Resetting configuration: ${key}`); + const config = this.configManager.getConfig(); - if (config[key] === undefined) { - console.log(`Key does not exist in the configuration: ${key}`); + if (!(key in config)) { + this.failSpinner(`Configuration key '${key}' does not exist.`); return; } + delete config[key]; this.configManager.writeConfig(key, undefined); - console.log(`Configuration key reset: ${key}`); + this.succeedSpinner(`Configuration successfully reset`); } } diff --git a/tests/actions/getSetReset.test.ts b/tests/actions/getSetReset.test.ts index a4bb9549..8a8ac17c 100644 --- a/tests/actions/getSetReset.test.ts +++ b/tests/actions/getSetReset.test.ts @@ -10,92 +10,79 @@ describe("ConfigActions", () => { beforeEach(() => { configActions = new ConfigActions(); vi.clearAllMocks(); - }); - new ConfigFileManager(); + vi.spyOn(configActions as any, "startSpinner").mockImplementation(() => {}); + vi.spyOn(configActions as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(configActions as any, "failSpinner").mockImplementation(() => {}); + }); afterEach(() => { vi.restoreAllMocks(); }); test("set method writes key-value pair to the configuration", () => { - const consoleLogSpy = vi.spyOn(console, "log"); - configActions.set("defaultNetwork=testnet"); expect(configActions["configManager"].writeConfig).toHaveBeenCalledWith("defaultNetwork", "testnet"); - expect(consoleLogSpy).toHaveBeenCalledWith("Configuration updated: defaultNetwork=testnet"); + expect(configActions["startSpinner"]).toHaveBeenCalledWith("Updating configuration: defaultNetwork"); + expect(configActions["succeedSpinner"]).toHaveBeenCalledWith("Configuration successfully updated"); }); - test("set method throws error for invalid format", () => { - const consoleErrorSpy = vi.spyOn(console, "error"); - const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit"); - }); + test("set method fails for invalid format", () => { + configActions.set("invalidFormat"); - expect(() => configActions.set("invalidFormat")).toThrowError("process.exit"); - - expect(consoleErrorSpy).toHaveBeenCalledWith("Invalid format. Use key=value."); - expect(processExitSpy).toHaveBeenCalledWith(1); + expect(configActions["failSpinner"]).toHaveBeenCalledWith("Invalid format. Use 'key=value'."); + expect(configActions["configManager"].writeConfig).not.toHaveBeenCalled(); }); test("get method retrieves value for a specific key", () => { vi.mocked(ConfigFileManager.prototype.getConfigByKey).mockReturnValue("testnet"); - const consoleLogSpy = vi.spyOn(console, "log"); - configActions.get("defaultNetwork"); expect(configActions["configManager"].getConfigByKey).toHaveBeenCalledWith("defaultNetwork"); - expect(consoleLogSpy).toHaveBeenCalledWith("defaultNetwork=testnet"); + expect(configActions["startSpinner"]).toHaveBeenCalledWith("Retrieving value for: defaultNetwork"); + expect(configActions["succeedSpinner"]).toHaveBeenCalledWith("Configuration successfully retrieved", "defaultNetwork=testnet"); }); - test("get method prints message when key has no value", () => { + test("get method prints failure message when key does not exist", () => { vi.mocked(ConfigFileManager.prototype.getConfigByKey).mockReturnValue(null); - const consoleLogSpy = vi.spyOn(console, "log"); - configActions.get("nonexistentKey"); expect(configActions["configManager"].getConfigByKey).toHaveBeenCalledWith("nonexistentKey"); - expect(consoleLogSpy).toHaveBeenCalledWith("No value set for key: nonexistentKey"); + expect(configActions["failSpinner"]).toHaveBeenCalledWith("No configuration found for 'nonexistentKey'."); }); test("get method retrieves the entire configuration when no key is provided", () => { const mockConfig = { defaultNetwork: "testnet" }; vi.mocked(ConfigFileManager.prototype.getConfig).mockReturnValue(mockConfig); - const consoleLogSpy = vi.spyOn(console, "log"); - configActions.get(); - expect(configActions["configManager"].getConfig).toHaveBeenCalledTimes(1); - expect(consoleLogSpy).toHaveBeenCalledWith("Current configuration:", JSON.stringify(mockConfig, null, 2)); + expect(configActions["configManager"].getConfig).toHaveBeenCalled(); + expect(configActions["startSpinner"]).toHaveBeenCalledWith("Retrieving all configurations"); + expect(configActions["succeedSpinner"]).toHaveBeenCalledWith("All configurations successfully retrieved", JSON.stringify(mockConfig, null, 2)); }); test("reset method removes key from configuration", () => { const mockConfig = { defaultNetwork: "testnet" }; vi.mocked(ConfigFileManager.prototype.getConfig).mockReturnValue(mockConfig); - const consoleLogSpy = vi.spyOn(console, "log"); - configActions.reset("defaultNetwork"); - expect(configActions["configManager"].getConfig).toHaveBeenCalledTimes(1); + expect(configActions["configManager"].getConfig).toHaveBeenCalled(); + expect(configActions["startSpinner"]).toHaveBeenCalledWith("Resetting configuration: defaultNetwork"); expect(configActions["configManager"].writeConfig).toHaveBeenCalledWith("defaultNetwork", undefined); - expect(consoleLogSpy).toHaveBeenCalledWith("Configuration key reset: defaultNetwork"); + expect(configActions["succeedSpinner"]).toHaveBeenCalledWith("Configuration successfully reset"); }); - test("reset method prints message when key does not exist", () => { - const mockConfig = {}; - vi.mocked(ConfigFileManager.prototype.getConfig).mockReturnValue(mockConfig); - - const consoleLogSpy = vi.spyOn(console, "log"); + test("reset method prints failure message when key does not exist", () => { + vi.mocked(ConfigFileManager.prototype.getConfig).mockReturnValue({}); configActions.reset("nonexistentKey"); - expect(configActions["configManager"].getConfig).toHaveBeenCalledTimes(1); - expect(configActions["configManager"].writeConfig).not.toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith("Key does not exist in the configuration: nonexistentKey"); + expect(configActions["configManager"].getConfig).toHaveBeenCalled(); + expect(configActions["failSpinner"]).toHaveBeenCalledWith("Configuration key 'nonexistentKey' does not exist."); }); -}); +}); \ No newline at end of file