UNPKG

genlayer

Version:
528 lines (431 loc) 20.3 kB
import { describe, beforeEach, test, expect, vi, Mock } from "vitest"; import * as path from "path"; import * as fs from "fs"; import * as dotenv from "dotenv"; import simulatorService from "../../src/lib/services/simulator"; import { getVersion, executeCommand, openUrl, checkCommand, } from "../../src/lib/clients/system"; import { CONTAINERS_NAME_PREFIX, VERSION_REQUIREMENTS, STARTING_TIMEOUT_ATTEMPTS, DEFAULT_RUN_SIMULATOR_COMMAND, localnetCompatibleVersion, IMAGES_NAME_PREFIX, } from "../../src/lib/config/simulator"; import { rpcClient } from "../../src/lib/clients/jsonRpcClient"; import * as semver from "semver"; import Docker from "dockerode"; import {VersionRequiredError} from "../../src/lib/errors/versionRequired"; import updateCheck from "update-check"; vi.mock("../../package.json", () => ({ default: { version: "1.0.0", name: "genlayer" }, })); vi.mock("update-check", () => ({ default: vi.fn(), })); vi.mock("dockerode"); vi.mock("fs"); vi.mock("path"); vi.mock("dotenv"); vi.mock("semver", () => ({ satisfies: vi.fn(), })); vi.mock("../../src/lib/clients/system", () => ({ checkCommand: vi.fn(), getVersion: vi.fn(), executeCommand: vi.fn(), openUrl: vi.fn(), listDockerContainers: vi.fn(), stopDockerContainer: vi.fn(), removeDockerContainer: vi.fn(), })); vi.mock("../../src/lib/clients/jsonRpcClient", () => ({ rpcClient: { request: vi.fn(), }, })); describe("SimulatorService - Basic Tests", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(path.join).mockImplementation((...args) => args.join("/")); }); test("should read the correct frontend URL from .env config", () => { const mockEnvContent = "FRONTEND_PORT=8080"; const mockEnvConfig = { FRONTEND_PORT: "8080" }; vi.mocked(fs.readFileSync).mockReturnValue(mockEnvContent); vi.mocked(dotenv.parse).mockReturnValue(mockEnvConfig); const frontendUrl = simulatorService.getFrontendUrl(); expect(frontendUrl).toBe("http://localhost:8080"); }); test("should check version requirements and return missing versions", async () => { vi.mocked(getVersion).mockResolvedValueOnce("12.0.0").mockResolvedValueOnce("18.0.0"); vi.mocked(semver.satisfies).mockImplementation((version, range) => { if (range === VERSION_REQUIREMENTS.node) return version === "18.0.0"; return false; }); const missingVersions = await simulatorService.checkVersionRequirements(); expect(missingVersions.node).toBe(VERSION_REQUIREMENTS.node); expect(missingVersions.docker).toBe(VERSION_REQUIREMENTS.docker); }); test("should handle error when checkVersion throws VersionRequiredError", async () => { vi.mocked(getVersion).mockResolvedValueOnce("10.0.0"); vi.mocked(semver.satisfies).mockReturnValue(false); await expect(simulatorService.checkVersion("14.0.0", "node")).rejects.toThrow(); }); test("should return initialized true when simulator responds with OK (result.status = OK)", async () => { vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: {status: 'OK'} }); const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS); expect(result).toEqual({ initialized: true }); expect(rpcClient.request).toHaveBeenCalledWith({ method: "ping", params: [] }); }); test("should return initialized true when simulator responds with OK (result.data.status = OK)", async () => { vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: {data: {status: 'OK'}} }); const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS); expect(result).toEqual({ initialized: true }); expect(rpcClient.request).toHaveBeenCalledWith({ method: "ping", params: [] }); }); test("should return initialized true when simulator responds with OK (result = OK)", async () => { vi.mocked(rpcClient.request).mockResolvedValueOnce({ result: 'OK' }); const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS); expect(result).toEqual({ initialized: true }); expect(rpcClient.request).toHaveBeenCalledWith({ method: "ping", params: [] }); }); test("should return initialized false with errorCode TIMEOUT after retries", async () => { vi.mocked(rpcClient.request).mockResolvedValue(undefined); const result = await simulatorService.waitForSimulatorToBeReady(1); expect(result).toEqual({ initialized: false, errorCode: "TIMEOUT" }); }); test("should return initialized false with errorCode ERROR on non-retryable error", async () => { const nonRetryableError = new Error("Unexpected error"); vi.mocked(rpcClient.request).mockRejectedValue(nonRetryableError); const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS); expect(result).toEqual({ initialized: false, errorCode: "ERROR", errorMessage: nonRetryableError.message }); }); test("should execute the correct run simulator command based on simulator location", async () => { (executeCommand as Mock).mockResolvedValue({ stdout: "Simulator started", stderr: "", }); const result = await simulatorService.runSimulator(); const expectedCommand = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorService.location, ''); expect(executeCommand).toHaveBeenCalledWith(expectedCommand); expect(result).toEqual({ stdout: "Simulator started", stderr: "" }); }); test("should execute the correct run simulator command based on headless option", async () => { (executeCommand as Mock).mockResolvedValue({ stdout: "Simulator started", stderr: "", }); simulatorService.setComposeOptions(true) const commandOption = simulatorService.getComposeOptions(); const result = await simulatorService.runSimulator(); const expectedCommand = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorService.location, commandOption); expect(executeCommand).toHaveBeenCalledWith(expectedCommand); expect(result).toEqual({ stdout: "Simulator started", stderr: "" }); }); test("should create a backup of the .env file and add new config", () => { const envFilePath = `/.env`; const originalEnvContent = "KEY1=value1\nKEY2=value2"; const parsedEnvConfig = { KEY1: "value1", KEY2: "value2" }; const newConfig = { KEY3: "value3", KEY2: "newValue2" }; vi.mocked(fs.readFileSync).mockImplementation((filePath: any) => { if (filePath === envFilePath) return originalEnvContent; return ""; }); vi.mocked(dotenv.parse).mockReturnValue(parsedEnvConfig); const writeFileSyncMock = vi.mocked(fs.writeFileSync); simulatorService.addConfigToEnvFile(newConfig); const expectedUpdatedContent = `KEY1=value1\nKEY2=newValue2\nKEY3=value3`; expect(writeFileSyncMock).toHaveBeenCalledWith(envFilePath, expectedUpdatedContent); }); test("should handle empty .env file and add new config", () => { const envFilePath = `/.env`; const newConfig = { NEW_KEY: "newValue" }; vi.mocked(fs.readFileSync).mockReturnValue(""); vi.mocked(dotenv.parse).mockReturnValue({}); const writeFileSyncMock = vi.mocked(fs.writeFileSync); simulatorService.addConfigToEnvFile(newConfig); const expectedUpdatedContent = `NEW_KEY=newValue`; expect(writeFileSyncMock).toHaveBeenCalledWith(envFilePath, expectedUpdatedContent); }); test("should throw error when .env file does not exist", () => { vi.mocked(fs.readFileSync).mockImplementation(() => { throw new Error("File not found"); }); expect(() => simulatorService.addConfigToEnvFile({ KEY: "value" })).toThrow( "File not found" ); }); test("should open the frontend URL and return true", async () => { vi.spyOn(simulatorService, "getFrontendUrl").mockReturnValue("http://localhost:8080"); const result = await simulatorService.openFrontend(); expect(simulatorService.getFrontendUrl).toHaveBeenCalled(); expect(openUrl).toHaveBeenCalledWith("http://localhost:8080"); expect(result).toBe(true); }); test("should call rpcClient.request with correct parameters and return the response", async () => { const mockResponse = { success: true }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); const result = await simulatorService.deleteAllValidators(); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_deleteAllValidators", params: [] }); expect(result).toBe(mockResponse); }); test("should return node missing version", async () => { const unexpectedError = new VersionRequiredError('node', VERSION_REQUIREMENTS.node); vi.spyOn(simulatorService, "checkVersion") .mockRejectedValueOnce(unexpectedError) .mockResolvedValueOnce(); await expect(simulatorService.checkVersionRequirements()).resolves.toStrictEqual({ "docker": "", "node": VERSION_REQUIREMENTS.node }) }); test("should return docker missing version", async () => { const unexpectedError = new VersionRequiredError('node', VERSION_REQUIREMENTS.docker); vi.spyOn(simulatorService, "checkVersion") .mockResolvedValueOnce() .mockRejectedValueOnce(unexpectedError) await expect(simulatorService.checkVersionRequirements()).resolves.toStrictEqual({ "docker": VERSION_REQUIREMENTS.docker, "node": "" }) }); test("should throw an unexpected error when checking node version requirements", async () => { const unexpectedError = new Error("Unexpected error (node)"); vi.spyOn(simulatorService, "checkVersion").mockRejectedValueOnce(unexpectedError); await expect(simulatorService.checkVersionRequirements()).rejects.toThrow("Unexpected error (node)"); }); test("should throw an unexpected error when checking docker version requirements", async () => { vi.spyOn(simulatorService, "checkVersion") .mockResolvedValueOnce(undefined) .mockRejectedValueOnce(new Error("Unexpected error (docker)")); await expect(simulatorService.checkVersionRequirements()).rejects.toThrow("Unexpected error (docker)"); }); test("should throw an unexpected error when checking git installation requirement", async () => { vi.mocked(checkCommand).mockRejectedValueOnce(new Error("Unexpected git error")); await expect(simulatorService.checkInstallRequirements()).rejects.toThrow("Unexpected git error"); const requirementsInstalled = { git: false, docker: false }; expect(requirementsInstalled.git).toBe(false); }); test("should retry when response is not 'OK' and reach sleep path", async () => { vi.mocked(rpcClient.request).mockResolvedValue({ result: { status: "NOT_OK" } }); const result = await simulatorService.waitForSimulatorToBeReady(1); expect(result).toEqual({ initialized: false, errorCode: "TIMEOUT" }); }); test("should retry on fetch error and reach sleep path", async () => { const fetchError = new Error("Fetch Error"); fetchError.name = "FetchError"; vi.mocked(rpcClient.request).mockRejectedValue(fetchError); const result = await simulatorService.waitForSimulatorToBeReady(1); expect(result).toEqual({ initialized: false, errorCode: "ERROR", errorMessage: fetchError.message }); }); test("should call executeCommand if docker ps command fails", async () => { vi.mocked(checkCommand) .mockResolvedValueOnce(undefined) const result = await simulatorService.checkInstallRequirements(); expect(result.docker).toBe(true); }); test("should return providers without errors", () => { expect(simulatorService.getAiProvidersOptions(true)).toEqual(expect.any(Array)); expect(simulatorService.getAiProvidersOptions(false)).toEqual(expect.any(Array)); }); test("clean simulator should success", async () => { vi.mocked(rpcClient.request).mockResolvedValueOnce('Success'); await expect(simulatorService.cleanDatabase).not.toThrow(); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_clearDbTables", params: [['current_state', 'transactions']] }); }); test("should create random validators", async () => { const numValidators = 5; const llmProviders = ["openai", "ollama"]; const mockResponse = { success: true }; vi.mocked(rpcClient.request).mockResolvedValue(mockResponse); const result = await simulatorService.createRandomValidators(numValidators, llmProviders); expect(rpcClient.request).toHaveBeenCalledWith({ method: "sim_createRandomValidators", params: [numValidators, 1, 10, llmProviders], }); expect(result).toEqual(mockResponse); }); }); describe("SimulatorService - Docker Tests", () => { let mockGetContainer: Mock; let mockListContainers: Mock; let mockListImages: Mock; let mockGetImage: Mock; let mockPing: Mock; beforeEach(() => { vi.clearAllMocks(); mockGetContainer = vi.mocked(Docker.prototype.getContainer); mockListContainers = vi.mocked(Docker.prototype.listContainers); mockListImages = vi.mocked(Docker.prototype.listImages); mockGetImage = vi.mocked(Docker.prototype.getImage); mockPing = vi.mocked(Docker.prototype.ping); }); test("should stop and remove Docker containers with the specified prefix", async () => { const mockContainers = [ { Id: "container1", Names: [`${CONTAINERS_NAME_PREFIX}container1`], State: "running", }, { Id: "container2", Names: [`${CONTAINERS_NAME_PREFIX}container2`], State: "exited", }, { Id: "container3", Names: ["/unrelated-container"], State: "running", }, ]; mockListContainers.mockResolvedValue(mockContainers); const mockStop = vi.fn().mockResolvedValue(undefined); const mockRemove = vi.fn().mockResolvedValue(undefined); mockGetContainer.mockImplementation(() => ({ stop: mockStop, remove: mockRemove, } as unknown as Docker.Container)); const result = await simulatorService.resetDockerContainers(); expect(result).toBe(undefined); expect(mockListContainers).toHaveBeenCalledWith({ all: true }); // Ensure only the relevant containers were stopped and removed expect(mockGetContainer).toHaveBeenCalledWith("container1"); expect(mockGetContainer).toHaveBeenCalledWith("container2"); expect(mockGetContainer).not.toHaveBeenCalledWith("container3"); expect(mockStop).toHaveBeenCalledTimes(1); expect(mockRemove).toHaveBeenCalledTimes(2); }); test("should stop all running GenLayer containers", async () => { const mockContainers = [ { Id: "container1", Names: [`${CONTAINERS_NAME_PREFIX}container1`], State: "running", }, { Id: "container2", Names: [`${CONTAINERS_NAME_PREFIX}container2`], State: "exited", }, ]; vi.mocked(Docker.prototype.listContainers).mockResolvedValue(mockContainers as any); const mockStop = vi.fn().mockResolvedValue(undefined); const mockGetContainer = vi.mocked(Docker.prototype.getContainer); mockGetContainer.mockImplementation(() => ({ stop: mockStop, } as unknown as Docker.Container)); await simulatorService.stopDockerContainers(); expect(mockGetContainer).toHaveBeenCalledWith("container1"); expect(mockGetContainer).toHaveBeenCalledWith("container2"); expect(mockStop).toHaveBeenCalledTimes(1); }); test("should remove Docker images with the specified prefix", async () => { const mockImages = [ { Id: "image1", RepoTags: [`${IMAGES_NAME_PREFIX}image1:${localnetCompatibleVersion}`], }, { Id: "image2", RepoTags: [`${IMAGES_NAME_PREFIX}image2:${localnetCompatibleVersion}`], }, { Id: "image3", RepoTags: ["unrelated-image:latest"], }, ]; mockListImages.mockResolvedValue(mockImages); const mockRemove = vi.fn().mockResolvedValue(undefined); mockGetImage.mockImplementation(() => ({ remove: mockRemove, } as unknown as Docker.Image)); const result = await simulatorService.resetDockerImages(); expect(result).toBe(undefined); expect(mockListImages).toHaveBeenCalled(); expect(mockGetImage).toHaveBeenCalledWith("image1"); expect(mockGetImage).toHaveBeenCalledWith("image2"); expect(mockGetImage).not.toHaveBeenCalledWith("image3"); expect(mockRemove).toHaveBeenCalledTimes(2); expect(mockRemove).toHaveBeenCalledWith({ force: true }); }); test("should execute command when docker is installed but is not available", async () => { vi.mocked(checkCommand) .mockResolvedValueOnce(undefined) mockPing.mockRejectedValueOnce(""); await simulatorService.checkInstallRequirements(); expect(executeCommand).toHaveBeenCalledTimes(1); }); test("should call execute command again to start docker service", async () => { vi.mocked(checkCommand) .mockResolvedValueOnce(undefined) .mockRejectedValue(undefined); mockPing.mockRejectedValueOnce(""); await expect(simulatorService.checkInstallRequirements()).resolves.toStrictEqual({ docker: true }); }); test("should warn the user when an update is available", async () => { const update = { latest: "1.1.0" }; (updateCheck as any).mockResolvedValue(update); const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); await simulatorService.checkCliVersion(); expect(consoleWarnSpy).toHaveBeenCalledWith( `\nA new version (${update.latest}) is available! You're using version 1.0.0.\nRun npm install -g genlayer to update\n` ); consoleWarnSpy.mockRestore(); }); test("should not warn the user when the CLI is up-to-date", async () => { const update = { latest: "1.0.0" }; (updateCheck as any).mockResolvedValue(update); const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); await simulatorService.checkCliVersion(); expect(consoleWarnSpy).not.toHaveBeenCalled(); consoleWarnSpy.mockRestore(); }); test("should handle update-check returning undefined", async () => { (updateCheck as any).mockResolvedValue(undefined); const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); await simulatorService.checkCliVersion(); expect(consoleWarnSpy).not.toHaveBeenCalled(); consoleWarnSpy.mockRestore(); }); }); describe('normalizeLocalnetVersion', () => { test('should add "v" if not present', () => { expect(simulatorService.normalizeLocalnetVersion("0.26.0")).toBe("v0.26.0"); }); test('should preserve "v" if already present', () => { expect(simulatorService.normalizeLocalnetVersion("v0.26.0")).toBe("v0.26.0"); }); test('should retain suffixes like "-test000"', () => { expect(simulatorService.normalizeLocalnetVersion("0.25.0-test000")).toBe("v0.25.0-test000"); expect(simulatorService.normalizeLocalnetVersion("v1.0.0-alpha")).toBe("v1.0.0-alpha"); }); test('should handle versions with numbers only', () => { expect(simulatorService.normalizeLocalnetVersion("1.0.0")).toBe("v1.0.0"); }); test('should throw an error and exit for invalid versions', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { return undefined as never}); const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); simulatorService.normalizeLocalnetVersion("invalid-version"); expect(mockConsoleError).toHaveBeenCalledWith( 'Invalid version format. Expected format: v0.0.0 or v0.0.0-suffix' ); expect(mockExit).toHaveBeenCalledWith(1); 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); }); });