diff --git a/src/commands/validators/index.ts b/src/commands/validators/index.ts new file mode 100644 index 00000000..b7737421 --- /dev/null +++ b/src/commands/validators/index.ts @@ -0,0 +1,94 @@ +import { Command } from "commander"; +import { ValidatorsAction } from "./validators"; + +export function initializeValidatorCommands(program: Command) { + const validatorsAction = new ValidatorsAction(); + + const validatorsCommand = program + .command("validators") + .description("Manage validator operations"); + + validatorsCommand + .command("get") + + .description("Retrieve details of a specific validator or all validators") + .option("--address ", "The address of the validator to retrieve (omit to retrieve all validators)") + .action(async (options) => { + await validatorsAction.getValidator({ address: options.address }); + }); + + validatorsCommand + .command("delete") + .description("Delete a specific validator or all validators") + .option("--address ", "The address of the validator to delete (omit to delete all validators)") + .action(async (options) => { + await validatorsAction.deleteValidator({ address: options.address }); + }); + + validatorsCommand + .command("count") + .description("Count all validators") + .action(async () => { + await validatorsAction.countValidators(); + }); + + validatorsCommand + .command("update ") + .description("Update a validator's details") + .option("--stake ", "New stake for the validator") + .option("--provider ", "New provider for the validator") + .option("--model ", "New model for the validator") + .option("--config ", "New JSON config for the validator") + .action(async (validatorAddress, options) => { + await validatorsAction.updateValidator({ + address: validatorAddress, + stake: options.stake, + provider: options.provider, + model: options.model, + config: options.config, + }); + }); + + validatorsCommand + .command("create-random") + .description("Create random validators") + .option("--count ", "Number of validators to create", "1") // Default to "1" + .option( + "--providers ", + "Space-separated list of provider names (e.g., openai ollama)", + [] + ) + .option( + "--models ", + "Space-separated list of model names (e.g., gpt-4 gpt-4o)", + [] + ) + .action(async (options) => { + await validatorsAction.createRandomValidators({ + count: options.count, + providers: options.providers, + models: options.models, + }); + }); + + validatorsCommand + .command("create") + .description("Create a new validator") + .option("--stake ", "Stake amount for the validator (default: 1)", "1") + .option( + "--config ", + 'Optional JSON configuration for the validator (e.g., \'{"max_tokens": 500, "temperature": 0.75}\')' + ) + .option("--provider ", "Specify the provider for the validator") + .option("--model ", "Specify the model for the validator") + .action(async (options) => { + await validatorsAction.createValidator({ + stake: options.stake, + config: options.config, + provider: options.provider, + model: options.model, + }); + }); + + return program; +} diff --git a/src/commands/validators/validators.ts b/src/commands/validators/validators.ts new file mode 100644 index 00000000..99a4024e --- /dev/null +++ b/src/commands/validators/validators.ts @@ -0,0 +1,274 @@ +import inquirer from "inquirer"; +import { rpcClient } from "../../lib/clients/jsonRpcClient"; +import { BaseAction } from "../../lib/actions/BaseAction"; + +export interface ValidatorOptions { + address?: string; +} + +export interface UpdateValidatorOptions { + address: string; + stake?: string; + provider?: string; + model?: string; + config?: string; +} + +export interface CreateRandomValidatorsOptions { + count: string; + providers: string[]; + models: string[]; +} + +export interface CreateValidatorOptions { + stake: string; + config?: string; + model?: string; + provider?: string; +} + +export class ValidatorsAction extends BaseAction { + public async getValidator(options: ValidatorOptions): Promise { + try { + if (options.address) { + console.log(`Fetching validator with address: ${options.address}`); + + const result = await rpcClient.request({ + method: "sim_getValidator", + params: [options.address], + }); + + console.log("Validator Details:", result.result); + } else { + console.log("Fetching all validators..."); + + const result = await rpcClient.request({ + method: "sim_getAllValidators", + params: [], + }); + + console.log("All Validators:", result.result); + } + } catch (error) { + console.error("Error fetching validators:", error); + } + } + + public async deleteValidator(options: ValidatorOptions): Promise { + 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}`); + + const result = await rpcClient.request({ + method: "sim_deleteValidator", + params: [options.address], + }); + + console.log("Deleted Address:", result.result); + } else { + await this.confirmPrompt(`This command will delete all validators. Do you want to continue?`); + console.log("Deleting all validators..."); + + await rpcClient.request({ + method: "sim_deleteAllValidators", + params: [], + }); + + console.log("Successfully deleted all validators"); + } + } catch (error) { + console.error("Error deleting validators:", error); + } + } + + public async countValidators(): Promise { + try { + console.log("Counting all validators..."); + + const result = await rpcClient.request({ + method: "sim_countValidators", + params: [], + }); + + console.log("Total Validators:", result.result); + } catch (error) { + console.error("Error counting validators:", error); + } + } + + public async updateValidator(options: UpdateValidatorOptions): Promise { + try { + console.log(`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); + + 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."); + } + + const updatedValidator = { + address: options.address, + stake: options.stake || currentValidator.result.stake, + provider: options.provider || currentValidator.result.provider, + model: options.model || currentValidator.result.model, + config: options.config ? JSON.parse(options.config) : currentValidator.result.config, + }; + + console.log("Updated Validator Details:", updatedValidator); + + const result = await rpcClient.request({ + method: "sim_updateValidator", + params: [ + updatedValidator.address, + updatedValidator.stake, + updatedValidator.provider, + updatedValidator.model, + updatedValidator.config, + ], + }); + + console.log("Validator successfully updated:", result.result); + } catch (error) { + console.error("Error updating validator:", error); + } + } + + public async createRandomValidators(options: CreateRandomValidatorsOptions): Promise { + try { + const count = parseInt(options.count, 10); + if (isNaN(count) || count < 1) { + return console.error("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"}`); + + const result = await rpcClient.request({ + method: "sim_createRandomValidators", + params: [count, 1, 10, options.providers, options.models], + }); + + console.log("Random validators successfully created:", result.result); + } catch (error) { + console.error("Error creating random validators:", error); + } + } + + public async createValidator(options: CreateValidatorOptions): Promise { + try { + const stake = parseInt(options.stake, 10); + if (isNaN(stake) || stake < 1) { + return console.error("Invalid stake. Please provide a positive integer."); + } + + if (options.model && !options.provider) { + return console.error("You must specify a provider if using a model."); + } + + console.log("Fetching available providers and models..."); + + const providersAndModels = await rpcClient.request({ + method: "sim_getProvidersAndModels", + params: [], + }); + + if (!providersAndModels.result || providersAndModels.result.length === 0) { + return console.error("No providers or models available."); + } + + const availableProviders = [ + ...new Map( + providersAndModels.result + .filter((entry: any) => entry.is_available) + .map((entry: any) => [entry.provider, entry]) + ).values(), + ]; + + let provider = options.provider + + if(!provider){ + const { selectedProvider } = await inquirer.prompt([ + { + type: "list", + name: "selectedProvider", + message: "Select a provider:", + choices: availableProviders.map((entry: any) => entry.provider), + }, + ]); + + provider = selectedProvider; + } + + const availableModels = providersAndModels.result.filter( + (entry: any) => entry.provider === provider && entry.is_model_available + ); + + if (availableModels.length === 0) { + return console.error("No models available for the selected provider."); + } + + let model = options.model; + + if(!model){ + const { selectedModel } = await inquirer.prompt([ + { + type: "list", + name: "selectedModel", + message: "Select a model:", + choices: availableModels.map((entry: any) => entry.model), + }, + ]); + + model = selectedModel; + } + + const modelDetails = availableModels.find( + (entry: any) => entry.model === model + ); + + if (!modelDetails) { + return console.error("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 result = await rpcClient.request({ + method: "sim_createValidator", + params: [ + stake, + modelDetails.provider, + modelDetails.model, + config, + modelDetails.plugin, + modelDetails.plugin_config, + ], + }); + + console.log("Validator successfully created:", result.result); + } catch (error) { + console.error("Error creating validator:", error); + } + } +} diff --git a/src/index.ts b/src/index.ts index 7563c308..8f98875c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { initializeGeneralCommands } from "../src/commands/general"; import { initializeKeygenCommands } from "../src/commands/keygen"; import { initializeContractsCommands } from "../src/commands/contracts"; import { initializeConfigCommands } from "../src/commands/config"; +import {initializeValidatorCommands} from "../src/commands/validators"; import { initializeUpdateCommands } from "../src/commands/update"; export function initializeCLI() { @@ -15,6 +16,7 @@ export function initializeCLI() { initializeContractsCommands(program); initializeConfigCommands(program); initializeUpdateCommands(program) + initializeValidatorCommands(program); program.parse(process.argv); } diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts new file mode 100644 index 00000000..93dab902 --- /dev/null +++ b/src/lib/actions/BaseAction.ts @@ -0,0 +1,19 @@ +import inquirer from "inquirer"; + +export class BaseAction { + protected async confirmPrompt(message: string): Promise { + const answer = await inquirer.prompt([ + { + type: "confirm", + name: "confirmAction", + message: message, + default: true, + }, + ]); + + if (!answer.confirmAction) { + console.log("Operation aborted!"); + process.exit(0); + } + } +} diff --git a/src/lib/clients/jsonRpcClient.ts b/src/lib/clients/jsonRpcClient.ts index 412921a4..25274a72 100644 --- a/src/lib/clients/jsonRpcClient.ts +++ b/src/lib/clients/jsonRpcClient.ts @@ -16,27 +16,26 @@ export class JsonRpcClient { } async request({method, params}: JsonRPCParams): Promise { - try { - const response = await fetch(this.serverUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: uuidv4(), - method, - params, - }), - }); - - if (response.ok) { - return response.json(); - } - } catch (error: any) { - throw new Error(`Fetch Error: ${error.message}`); + const response = await fetch(this.serverUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: uuidv4(), + method, + params, + }), + }); + + if (response.ok) { + return response.json(); } - return null; + const result = await response.json(); + + throw new Error(result?.error?.message || response.statusText); + } } export const rpcClient = new JsonRpcClient(DEFAULT_JSON_RPC_URL); diff --git a/src/lib/services/simulator.ts b/src/lib/services/simulator.ts index 18109a8d..f757892b 100644 --- a/src/lib/services/simulator.ts +++ b/src/lib/services/simulator.ts @@ -190,13 +190,13 @@ export class SimulatorService implements ISimulatorService { public createRandomValidators(numValidators: number, llmProviders: AiProviders[]): Promise { return rpcClient.request({ - method: "create_random_validators", + method: "sim_createRandomValidators", params: [numValidators, 1, 10, llmProviders], }); } public deleteAllValidators(): Promise { - return rpcClient.request({method: "delete_all_validators", params: []}); + return rpcClient.request({method: "sim_deleteAllValidators", params: []}); } public getAiProvidersOptions(withHint: boolean = true): Array<{name: string; value: string}> { diff --git a/tests/actions/validators.test.ts b/tests/actions/validators.test.ts new file mode 100644 index 00000000..3e9b9979 --- /dev/null +++ b/tests/actions/validators.test.ts @@ -0,0 +1,619 @@ +import { describe, test, vi, beforeEach, afterEach, expect } from "vitest"; +import { ValidatorsAction } from "../../src/commands/validators/validators"; +import { rpcClient } from "../../src/lib/clients/jsonRpcClient"; +import inquirer from "inquirer"; + +vi.mock("../../src/lib/clients/jsonRpcClient", () => ({ + rpcClient: { + request: vi.fn(), + }, +})); + +vi.mock("inquirer"); + +describe("ValidatorsAction", () => { + let validatorsAction: ValidatorsAction; + + beforeEach(() => { + vi.clearAllMocks(); + validatorsAction = new ValidatorsAction(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getValidator", () => { + test("should fetch a specific validator by address", async () => { + const mockAddress = "mocked_address"; + const mockResponse = { result: { id: 1, name: "Validator1" } }; + vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); + + console.log = vi.fn(); + + await validatorsAction.getValidator({ address: mockAddress }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(console.log).toHaveBeenCalledWith("Validator Details:", mockResponse.result); + }); + + 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(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getAllValidators", + params: [], + }); + expect(console.log).toHaveBeenCalledWith("All Validators:", mockResponse.result); + }); + + test("should log an error if an exception occurs while fetching a specific validator", async () => { + const mockAddress = "mocked_address"; + const mockError = new Error("Unexpected error"); + + vi.mocked(rpcClient.request).mockRejectedValue(mockError); + + console.error = vi.fn(); + + await validatorsAction.getValidator({ address: mockAddress }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(console.error).toHaveBeenCalledWith("Error fetching validators:", mockError); + }); + + test("should log an error if an exception occurs while fetching all validators", async () => { + const mockError = new Error("Unexpected error"); + + vi.mocked(rpcClient.request).mockRejectedValue(mockError); + + console.error = vi.fn(); + + await validatorsAction.getValidator({}); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getAllValidators", + params: [], + }); + expect(console.error).toHaveBeenCalledWith("Error fetching validators:", mockError); + }); + }); + + describe("deleteValidator", () => { + test("should delete a specific validator", async () => { + const mockAddress = "mocked_address"; + vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true }); + vi.mocked(rpcClient.request).mockResolvedValue({ result: { id: 1 } }); + + console.log = vi.fn(); + + await validatorsAction.deleteValidator({ address: mockAddress }); + + expect(inquirer.prompt).toHaveBeenCalled(); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_deleteValidator", + params: [mockAddress], + }); + expect(console.log).toHaveBeenCalledWith("Deleted Address:", { id: 1 }); + }); + + 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(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_deleteAllValidators", + params: [], + }); + expect(console.log).toHaveBeenCalledWith("Successfully deleted all validators"); + }); + + 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" }) + + expect(inquirer.prompt).toHaveBeenCalled(); + expect(console.log).toHaveBeenCalledWith("Operation aborted!"); + expect(rpcClient.request).not.toHaveBeenCalled(); + }); + }); + + describe("countValidators", () => { + test("should count all validators", async () => { + const mockResponse = { result: 42 }; + vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); + + console.log = vi.fn(); + + await validatorsAction.countValidators(); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_countValidators", + params: [], + }); + expect(console.log).toHaveBeenCalledWith("Total Validators:", 42); + }); + + test("should log an error if an exception occurs while counting validators", async () => { + const mockError = new Error("Unexpected error"); + + vi.mocked(rpcClient.request).mockRejectedValue(mockError); + + console.error = vi.fn(); + + await validatorsAction.countValidators(); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_countValidators", + params: [], + }); + expect(console.error).toHaveBeenCalledWith("Error counting validators:", mockError); + }); + }); + + describe("createValidator", () => { + test("should create a validator with selected provider and model", async () => { + const mockProvidersAndModels = [ + { + provider: "Provider1", + is_available: true, + is_model_available: true, + model: "Model1", + config: { max_tokens: 500 }, + plugin: "Plugin1", + plugin_config: { api_key_env_var: "KEY1" }, + }, + ]; + const mockResponse = { result: { id: 123 } }; + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce({ result: mockProvidersAndModels }) + .mockResolvedValueOnce(mockResponse); + + vi.mocked(inquirer.prompt) + .mockResolvedValueOnce({ selectedProvider: "Provider1" }) + .mockResolvedValueOnce({ selectedModel: "Model1" }); + + console.log = vi.fn(); + + await validatorsAction.createValidator({ stake: "10" }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getProvidersAndModels", + params: [], + }); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_createValidator", + params: [ + 10, + "Provider1", + "Model1", + { max_tokens: 500 }, + "Plugin1", + { api_key_env_var: "KEY1" }, + ], + }); + expect(console.log).toHaveBeenCalledWith("Validator successfully created:", { id: 123 }); + }); + + 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(rpcClient.request).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(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getProvidersAndModels", + params: [], + }); + expect(console.error).toHaveBeenCalledWith("No providers or models available."); + }); + + test("should log an error if no models are available for the selected provider", async () => { + const mockProvidersAndModels = [ + { provider: "Provider1", is_available: true, is_model_available: false }, + ]; + + 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."); + }); + + test("should log an error if selected model details are not found", async () => { + const mockProvidersAndModels = [ + { + provider: "Provider1", + is_available: true, + is_model_available: true, + model: "Model1", + }, + ]; + + vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: mockProvidersAndModels }); + vi.mocked(inquirer.prompt) + .mockResolvedValueOnce({ selectedProvider: "Provider1" }) + .mockResolvedValueOnce({ selectedModel: "NonExistentModel" }); + + console.error = vi.fn(); + + await validatorsAction.createValidator({ stake: "10" }); + + expect(console.error).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); + }); + + test("should use user-provided config if specified", async () => { + const mockProvidersAndModels = [ + { + provider: "Provider1", + is_available: true, + is_model_available: true, + model: "Model1", + config: { max_tokens: 500 }, + plugin: "Plugin1", + plugin_config: { api_key_env_var: "KEY1" }, + }, + ]; + const mockResponse = { result: { id: 123 } }; + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce({ result: mockProvidersAndModels }) + .mockResolvedValueOnce(mockResponse); + + vi.mocked(inquirer.prompt) + .mockResolvedValueOnce({ selectedProvider: "Provider1" }) + .mockResolvedValueOnce({ selectedModel: "Model1" }); + + console.log = vi.fn(); + + const customConfig = '{"custom_key":"custom_value"}'; + await validatorsAction.createValidator({ stake: "10", config: customConfig }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_createValidator", + params: [ + 10, + "Provider1", + "Model1", + { custom_key: "custom_value" }, + "Plugin1", + { api_key_env_var: "KEY1" }, + ], + }); + expect(console.log).toHaveBeenCalledWith("Validator successfully created:", { id: 123 }); + }); + }); + 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(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); + }); + + 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(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); + }); + + 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(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); + }); + + 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(rpcClient.request).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(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_createRandomValidators", + params: [5, 1, 10, ["Provider1"], []], + }); + expect(console.error).toHaveBeenCalledWith("Error creating random validators:", mockError); + }); + }); + + describe("updateValidator", () => { + test("should fetch and update a validator with new stake", async () => { + const mockAddress = "mocked_address"; + const mockCurrentValidator = { + result: { + address: "mocked_address", + stake: 100, + provider: "Provider1", + model: "Model1", + config: { max_tokens: 500 }, + }, + }; + const mockResponse = { result: { success: true } }; + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce(mockCurrentValidator) + .mockResolvedValueOnce(mockResponse); + + console.log = vi.fn(); + + await validatorsAction.updateValidator({ address: mockAddress, stake: "200" }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_updateValidator", + params: [ + "mocked_address", + "200", + "Provider1", + "Model1", + { max_tokens: 500 }, + ], + }); + expect(console.log).toHaveBeenCalledWith("Validator successfully updated:", mockResponse.result); + }); + + test("should fetch and update a validator with new provider and model", async () => { + const mockAddress = "mocked_address"; + const mockCurrentValidator = { + result: { + address: "mocked_address", + stake: "100", + provider: "Provider1", + model: "Model1", + config: { max_tokens: 500 }, + }, + }; + const mockResponse = { result: { success: true } }; + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce(mockCurrentValidator) + .mockResolvedValueOnce(mockResponse); + + console.log = vi.fn(); + + await validatorsAction.updateValidator({ + address: mockAddress, + provider: "Provider2", + model: "Model2", + }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_updateValidator", + params: [ + "mocked_address", + "100", + "Provider2", + "Model2", + { max_tokens: 500 }, + ], + }); + expect(console.log).toHaveBeenCalledWith("Validator successfully updated:", mockResponse.result); + }); + + test("should fetch and update a validator with new config", async () => { + const mockAddress = "mocked_address"; + const mockCurrentValidator = { + result: { + address: "mocked_address", + stake: "100", + provider: "Provider1", + model: "Model1", + config: { max_tokens: 500 }, + }, + }; + const mockResponse = { result: { success: true } }; + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce(mockCurrentValidator) + .mockResolvedValueOnce(mockResponse); + + console.log = vi.fn(); + + const newConfig = '{"max_tokens":1000}'; + await validatorsAction.updateValidator({ address: mockAddress, config: newConfig }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_updateValidator", + params: [ + "mocked_address", + "100", + "Provider1", + "Model1", + { 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); + }); + + test("should log an error if updateValidator RPC call fails", async () => { + const mockAddress = "mocked_address"; + const mockCurrentValidator = { + result: { + address: "mocked_address", + stake: "100", + provider: "Provider1", + model: "Model1", + config: { max_tokens: 500 }, + }, + }; + const mockError = new Error("RPC failure"); + + vi.mocked(rpcClient.request) + .mockResolvedValueOnce(mockCurrentValidator) + .mockRejectedValueOnce(mockError); + + console.error = vi.fn(); + + await validatorsAction.updateValidator({ address: mockAddress, stake: "200" }); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_getValidator", + params: [mockAddress], + }); + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_updateValidator", + params: [ + "mocked_address", + "200", + "Provider1", + "Model1", + { 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(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" }); + + expect(console.error).toHaveBeenCalledWith("You must specify a provider if using a model."); + expect(rpcClient.request).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/tests/commands/validator.test.ts b/tests/commands/validator.test.ts new file mode 100644 index 00000000..1ffd6b9d --- /dev/null +++ b/tests/commands/validator.test.ts @@ -0,0 +1,127 @@ +import { Command } from "commander"; +import { vi, describe, beforeEach, afterEach, test, expect } from "vitest"; +import { initializeValidatorCommands } from "../../src/commands/validators"; +import { ValidatorsAction } from "../../src/commands/validators/validators"; + +vi.mock("../../src/commands/validators/validators"); + +describe("validators command", () => { + let program: Command; + + beforeEach(() => { + program = new Command(); + initializeValidatorCommands(program); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("ValidatorsAction.getValidator is called with address option", async () => { + program.parse(["node", "test", "validators", "get", "--address", "mocked_address"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.getValidator).toHaveBeenCalledWith({ + address: "mocked_address", + }); + }); + + test("ValidatorsAction.getValidator is called without address option", async () => { + program.parse(["node", "test", "validators", "get"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.getValidator).toHaveBeenCalledWith({}); + }); + + test("ValidatorsAction.deleteValidator is called with address option", async () => { + program.parse(["node", "test", "validators", "delete", "--address", "mocked_address"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.deleteValidator).toHaveBeenCalledWith({ + address: "mocked_address", + }); + }); + + test("ValidatorsAction.deleteValidator is called without address option", async () => { + program.parse(["node", "test", "validators", "delete"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.deleteValidator).toHaveBeenCalledWith({}); + }); + + test("ValidatorsAction.countValidators is called", async () => { + program.parse(["node", "test", "validators", "count"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.countValidators).toHaveBeenCalled(); + }); + + test("ValidatorsAction.updateValidator is called with all options", async () => { + program.parse([ + "node", + "test", + "validators", + "update", + "mocked_address", + "--stake", + "10", + "--provider", + "mocked_provider", + "--model", + "mocked_model", + '--config', + '{"max_tokens":500}', + ]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.updateValidator).toHaveBeenCalledWith({ + address: "mocked_address", + stake: "10", + provider: "mocked_provider", + model: "mocked_model", + config: '{"max_tokens":500}', + }); + }); + + test("ValidatorsAction.createRandomValidators is called with count and providers", async () => { + program.parse([ + "node", + "test", + "validators", + "create-random", + "--count", + "3", + "--providers", + "provider1", + "provider2", + ]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.createRandomValidators).toHaveBeenCalledWith({ + count: "3", + providers: ["provider1", "provider2"], + models: [] + }); + }); + + test("ValidatorsAction.createValidator is called with default stake", async () => { + program.parse(["node", "test", "validators", "create"]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.createValidator).toHaveBeenCalledWith({ + stake: "1", + config: undefined, + }); + }); + + test("ValidatorsAction.createValidator is called with stake and config", async () => { + program.parse([ + "node", + "test", + "validators", + "create", + "--stake", + "5", + '--config', + '{"temperature":0.8}', + ]); + expect(ValidatorsAction).toHaveBeenCalledTimes(1); + expect(ValidatorsAction.prototype.createValidator).toHaveBeenCalledWith({ + stake: "5", + config: '{"temperature":0.8}', + }); + }); + +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index e72ba16c..723315f5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -25,6 +25,10 @@ vi.mock("../src/commands/config", () => ({ initializeConfigCommands: vi.fn(), })); +vi.mock("../src/commands/validators", () => ({ + initializeValidatorCommands: vi.fn(), +})); + vi.mock("../src/commands/update", () => ({ initializeUpdateCommands: vi.fn(), })); diff --git a/tests/libs/jsonRpcClient.test.ts b/tests/libs/jsonRpcClient.test.ts index 95de7452..33a0b537 100644 --- a/tests/libs/jsonRpcClient.test.ts +++ b/tests/libs/jsonRpcClient.test.ts @@ -40,6 +40,7 @@ describe("JsonRpcClient - Successful and Unsuccessful Requests", () => { test("should return null when the fetch response is not ok", async () => { (fetch as Mock).mockResolvedValueOnce({ ok: false, + statusText: "Something went wrong", json: async () => ({ error: "Something went wrong" }), }); @@ -48,9 +49,7 @@ describe("JsonRpcClient - Successful and Unsuccessful Requests", () => { params: ["param1", "param2"], }; - const response = await rpcClient.request(params); - - expect(response).toBeNull(); + await expect(rpcClient.request(params)).rejects.toThrowError("Something went wrong"); expect(fetch).toHaveBeenCalledWith(mockServerUrl, { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/tests/services/simulator.test.ts b/tests/services/simulator.test.ts index 65cac3b1..c82628b8 100644 --- a/tests/services/simulator.test.ts +++ b/tests/services/simulator.test.ts @@ -200,7 +200,7 @@ describe("SimulatorService - Basic Tests", () => { const mockResponse = { success: true }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); const result = await simulatorService.deleteAllValidators(); - expect(rpcClient.request).toHaveBeenCalledWith({ method: "delete_all_validators", params: [] }); + expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_deleteAllValidators", params: [] }); expect(result).toBe(mockResponse); }); @@ -465,4 +465,18 @@ describe('normalizeLocalnetVersion', () => { mockExit.mockRestore(); mockConsoleError.mockRestore(); }); + test("should log an error if an exception occurs while cleaning the database", async () => { + const mockError = new Error("Database cleanup error"); + vi.mocked(rpcClient.request).mockRejectedValue(mockError); + + console.error = vi.fn(); + + await simulatorService.cleanDatabase(); + + expect(rpcClient.request).toHaveBeenCalledWith({ + method: "sim_clearDbTables", + params: [['current_state', 'transactions']], + }); + expect(console.error).toHaveBeenCalledWith(mockError); + }); });