genlayer
Version:
GenLayer Command Line Tool
528 lines (431 loc) • 20.3 kB
text/typescript
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);
});
});