genlayer 0.38.8 → 0.38.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.eslintignore +2 -0
  2. package/.github/workflows/cli-docs.yml +124 -0
  3. package/.github/workflows/publish.yml +55 -0
  4. package/.github/workflows/smoke.yml +27 -0
  5. package/.github/workflows/validate-code.yml +51 -0
  6. package/.prettierignore +19 -0
  7. package/.prettierrc +12 -0
  8. package/.release-it.json +66 -0
  9. package/CHANGELOG.md +545 -0
  10. package/CLAUDE.md +55 -0
  11. package/CONTRIBUTING.md +117 -0
  12. package/dist/index.js +221 -62
  13. package/docs/api-references/_meta.json +9 -0
  14. package/docs/api-references/accounts/_meta.json +3 -0
  15. package/docs/api-references/accounts/account/create.mdx +19 -0
  16. package/docs/api-references/accounts/account/export.mdx +19 -0
  17. package/docs/api-references/accounts/account/import.mdx +22 -0
  18. package/docs/api-references/accounts/account/list.mdx +15 -0
  19. package/docs/api-references/accounts/account/lock.mdx +16 -0
  20. package/docs/api-references/accounts/account/remove.mdx +20 -0
  21. package/docs/api-references/accounts/account/send.mdx +24 -0
  22. package/docs/api-references/accounts/account/show.mdx +17 -0
  23. package/docs/api-references/accounts/account/unlock.mdx +17 -0
  24. package/docs/api-references/accounts/account/use.mdx +19 -0
  25. package/docs/api-references/accounts/account.mdx +32 -0
  26. package/docs/api-references/configuration/_meta.json +4 -0
  27. package/docs/api-references/configuration/config/get.mdx +21 -0
  28. package/docs/api-references/configuration/config/reset.mdx +21 -0
  29. package/docs/api-references/configuration/config/set.mdx +21 -0
  30. package/docs/api-references/configuration/config.mdx +25 -0
  31. package/docs/api-references/configuration/network/info.mdx +15 -0
  32. package/docs/api-references/configuration/network/list.mdx +15 -0
  33. package/docs/api-references/configuration/network/set.mdx +21 -0
  34. package/docs/api-references/configuration/network.mdx +25 -0
  35. package/docs/api-references/contracts/_meta.json +7 -0
  36. package/docs/api-references/contracts/call.mdx +21 -0
  37. package/docs/api-references/contracts/code.mdx +20 -0
  38. package/docs/api-references/contracts/deploy.mdx +17 -0
  39. package/docs/api-references/contracts/schema.mdx +20 -0
  40. package/docs/api-references/contracts/write.mdx +21 -0
  41. package/docs/api-references/environment/_meta.json +7 -0
  42. package/docs/api-references/environment/init.mdx +20 -0
  43. package/docs/api-references/environment/new.mdx +21 -0
  44. package/docs/api-references/environment/stop.mdx +15 -0
  45. package/docs/api-references/environment/up.mdx +20 -0
  46. package/docs/api-references/environment/update/ollama.mdx +16 -0
  47. package/docs/api-references/environment/update.mdx +23 -0
  48. package/docs/api-references/index.mdx +35 -0
  49. package/docs/api-references/localnet/_meta.json +3 -0
  50. package/docs/api-references/localnet/localnet/validators/count.mdx +15 -0
  51. package/docs/api-references/localnet/localnet/validators/create-random.mdx +16 -0
  52. package/docs/api-references/localnet/localnet/validators/create.mdx +19 -0
  53. package/docs/api-references/localnet/localnet/validators/delete.mdx +16 -0
  54. package/docs/api-references/localnet/localnet/validators/get.mdx +16 -0
  55. package/docs/api-references/localnet/localnet/validators/update.mdx +23 -0
  56. package/docs/api-references/localnet/localnet/validators.mdx +28 -0
  57. package/docs/api-references/localnet/localnet.mdx +23 -0
  58. package/docs/api-references/staking/_meta.json +3 -0
  59. package/docs/api-references/staking/staking/active-validators.mdx +18 -0
  60. package/docs/api-references/staking/staking/banned-validators.mdx +18 -0
  61. package/docs/api-references/staking/staking/delegation-info.mdx +25 -0
  62. package/docs/api-references/staking/staking/delegator-claim.mdx +26 -0
  63. package/docs/api-references/staking/staking/delegator-exit.mdx +26 -0
  64. package/docs/api-references/staking/staking/delegator-join.mdx +26 -0
  65. package/docs/api-references/staking/staking/epoch-info.mdx +19 -0
  66. package/docs/api-references/staking/staking/prime-all.mdx +20 -0
  67. package/docs/api-references/staking/staking/quarantined-validators.mdx +18 -0
  68. package/docs/api-references/staking/staking/set-identity.mdx +33 -0
  69. package/docs/api-references/staking/staking/set-operator.mdx +26 -0
  70. package/docs/api-references/staking/staking/validator-claim.mdx +24 -0
  71. package/docs/api-references/staking/staking/validator-deposit.mdx +25 -0
  72. package/docs/api-references/staking/staking/validator-exit.mdx +25 -0
  73. package/docs/api-references/staking/staking/validator-history.mdx +29 -0
  74. package/docs/api-references/staking/staking/validator-info.mdx +25 -0
  75. package/docs/api-references/staking/staking/validator-join.mdx +22 -0
  76. package/docs/api-references/staking/staking/validator-prime.mdx +25 -0
  77. package/docs/api-references/staking/staking/validators.mdx +19 -0
  78. package/docs/api-references/staking/staking/wizard.mdx +20 -0
  79. package/docs/api-references/staking/staking.mdx +42 -0
  80. package/docs/api-references/transactions/_meta.json +6 -0
  81. package/docs/api-references/transactions/appeal-bond.mdx +20 -0
  82. package/docs/api-references/transactions/appeal.mdx +21 -0
  83. package/docs/api-references/transactions/receipt.mdx +25 -0
  84. package/docs/api-references/transactions/trace.mdx +21 -0
  85. package/docs/delegator-guide.md +203 -0
  86. package/docs/validator-guide.md +329 -0
  87. package/esbuild.config.dev.js +17 -0
  88. package/esbuild.config.js +22 -0
  89. package/esbuild.config.prod.js +17 -0
  90. package/eslint.config.js +60 -0
  91. package/package.json +2 -11
  92. package/renovate.json +22 -0
  93. package/scripts/generate-cli-docs.mjs +68 -5
  94. package/src/commands/account/create.ts +30 -0
  95. package/src/commands/account/export.ts +106 -0
  96. package/src/commands/account/import.ts +135 -0
  97. package/src/commands/account/index.ts +129 -0
  98. package/src/commands/account/list.ts +34 -0
  99. package/src/commands/account/lock.ts +39 -0
  100. package/src/commands/account/remove.ts +30 -0
  101. package/src/commands/account/send.ts +162 -0
  102. package/src/commands/account/show.ts +74 -0
  103. package/src/commands/account/unlock.ts +56 -0
  104. package/src/commands/account/use.ts +21 -0
  105. package/src/commands/config/getSetReset.ts +51 -0
  106. package/src/commands/config/index.ts +30 -0
  107. package/src/commands/contracts/call.ts +39 -0
  108. package/src/commands/contracts/code.ts +33 -0
  109. package/src/commands/contracts/deploy.ts +161 -0
  110. package/src/commands/contracts/index.ts +150 -0
  111. package/src/commands/contracts/schema.ts +31 -0
  112. package/src/commands/contracts/write.ts +49 -0
  113. package/src/commands/general/index.ts +45 -0
  114. package/src/commands/general/init.ts +180 -0
  115. package/src/commands/general/start.ts +128 -0
  116. package/src/commands/general/stop.ts +26 -0
  117. package/src/commands/localnet/index.ts +100 -0
  118. package/src/commands/localnet/validators.ts +269 -0
  119. package/src/commands/network/index.ts +29 -0
  120. package/src/commands/network/setNetwork.ts +77 -0
  121. package/src/commands/scaffold/index.ts +16 -0
  122. package/src/commands/scaffold/new.ts +34 -0
  123. package/src/commands/staking/StakingAction.ts +279 -0
  124. package/src/commands/staking/delegatorClaim.ts +41 -0
  125. package/src/commands/staking/delegatorExit.ts +56 -0
  126. package/src/commands/staking/delegatorJoin.ts +44 -0
  127. package/src/commands/staking/index.ts +357 -0
  128. package/src/commands/staking/setIdentity.ts +78 -0
  129. package/src/commands/staking/setOperator.ts +46 -0
  130. package/src/commands/staking/stakingInfo.ts +584 -0
  131. package/src/commands/staking/validatorClaim.ts +43 -0
  132. package/src/commands/staking/validatorDeposit.ts +48 -0
  133. package/src/commands/staking/validatorExit.ts +63 -0
  134. package/src/commands/staking/validatorHistory.ts +300 -0
  135. package/src/commands/staking/validatorJoin.ts +47 -0
  136. package/src/commands/staking/validatorPrime.ts +73 -0
  137. package/src/commands/staking/wizard.ts +809 -0
  138. package/src/commands/transactions/appeal.ts +83 -0
  139. package/src/commands/transactions/index.ts +60 -0
  140. package/src/commands/transactions/receipt.ts +90 -0
  141. package/src/commands/transactions/trace.ts +42 -0
  142. package/src/commands/update/index.ts +25 -0
  143. package/src/commands/update/ollama.ts +103 -0
  144. package/src/lib/actions/BaseAction.ts +301 -0
  145. package/src/lib/clients/jsonRpcClient.ts +41 -0
  146. package/src/lib/clients/system.ts +73 -0
  147. package/src/lib/config/ConfigFileManager.ts +194 -0
  148. package/src/lib/config/KeychainManager.ts +89 -0
  149. package/src/lib/config/simulator.ts +68 -0
  150. package/src/lib/config/text.ts +2 -0
  151. package/src/lib/errors/missingRequirement.ts +9 -0
  152. package/src/lib/errors/versionRequired.ts +9 -0
  153. package/src/lib/interfaces/ISimulatorService.ts +39 -0
  154. package/src/lib/services/simulator.ts +386 -0
  155. package/src/types/node-fetch.d.ts +1 -0
  156. package/tests/actions/appeal.test.ts +141 -0
  157. package/tests/actions/call.test.ts +94 -0
  158. package/tests/actions/code.test.ts +87 -0
  159. package/tests/actions/create.test.ts +65 -0
  160. package/tests/actions/deploy.test.ts +420 -0
  161. package/tests/actions/getSetReset.test.ts +88 -0
  162. package/tests/actions/init.test.ts +483 -0
  163. package/tests/actions/lock.test.ts +86 -0
  164. package/tests/actions/new.test.ts +80 -0
  165. package/tests/actions/ollama.test.ts +193 -0
  166. package/tests/actions/receipt.test.ts +261 -0
  167. package/tests/actions/schema.test.ts +94 -0
  168. package/tests/actions/setNetwork.test.ts +161 -0
  169. package/tests/actions/staking.test.ts +280 -0
  170. package/tests/actions/start.test.ts +257 -0
  171. package/tests/actions/stop.test.ts +77 -0
  172. package/tests/actions/unlock.test.ts +139 -0
  173. package/tests/actions/validators.test.ts +750 -0
  174. package/tests/actions/write.test.ts +102 -0
  175. package/tests/commands/account.test.ts +146 -0
  176. package/tests/commands/appeal.test.ts +97 -0
  177. package/tests/commands/call.test.ts +78 -0
  178. package/tests/commands/code.test.ts +69 -0
  179. package/tests/commands/config.test.ts +54 -0
  180. package/tests/commands/deploy.test.ts +83 -0
  181. package/tests/commands/init.test.ts +101 -0
  182. package/tests/commands/localnet.test.ts +131 -0
  183. package/tests/commands/network.test.ts +60 -0
  184. package/tests/commands/new.test.ts +68 -0
  185. package/tests/commands/parseArg.test.ts +156 -0
  186. package/tests/commands/receipt.test.ts +142 -0
  187. package/tests/commands/schema.test.ts +67 -0
  188. package/tests/commands/staking.test.ts +329 -0
  189. package/tests/commands/stop.test.ts +27 -0
  190. package/tests/commands/up.test.ts +105 -0
  191. package/tests/commands/update.test.ts +45 -0
  192. package/tests/commands/write.test.ts +76 -0
  193. package/tests/index.test.ts +56 -0
  194. package/tests/libs/baseAction.test.ts +535 -0
  195. package/tests/libs/configFileManager.test.ts +118 -0
  196. package/tests/libs/jsonRpcClient.test.ts +59 -0
  197. package/tests/libs/keychainManager.test.ts +156 -0
  198. package/tests/libs/platformCommands.test.ts +78 -0
  199. package/tests/libs/system.test.ts +148 -0
  200. package/tests/services/simulator.test.ts +789 -0
  201. package/tests/smoke.test.ts +134 -0
  202. package/tests/utils.ts +13 -0
  203. package/tsconfig.json +120 -0
  204. package/vitest.config.ts +13 -0
  205. package/vitest.smoke.config.ts +17 -0
@@ -0,0 +1,789 @@
1
+ import {describe, beforeEach, test, expect, vi, Mock} from "vitest";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import * as dotenv from "dotenv";
5
+ import simulatorService from "../../src/lib/services/simulator";
6
+ import {getVersion, executeCommand, openUrl, checkCommand} from "../../src/lib/clients/system";
7
+ import {
8
+ CONTAINERS_NAME_PREFIX,
9
+ VERSION_REQUIREMENTS,
10
+ STARTING_TIMEOUT_ATTEMPTS,
11
+ DEFAULT_RUN_SIMULATOR_COMMAND,
12
+ localnetCompatibleVersion,
13
+ IMAGES_NAME_PREFIX,
14
+ AiProviders,
15
+ GENLAYER_REQUIRED_CONTAINERS,
16
+ } from "../../src/lib/config/simulator";
17
+ import {rpcClient} from "../../src/lib/clients/jsonRpcClient";
18
+ import * as semver from "semver";
19
+ import Docker from "dockerode";
20
+ import {VersionRequiredError} from "../../src/lib/errors/versionRequired";
21
+ import updateCheck from "update-check";
22
+
23
+ vi.mock("../../package.json", () => ({
24
+ default: {version: "1.0.0", name: "genlayer"},
25
+ }));
26
+
27
+ vi.mock("update-check", () => ({
28
+ default: vi.fn(),
29
+ }));
30
+ vi.mock("dockerode");
31
+ vi.mock("fs");
32
+ vi.mock("path");
33
+ vi.mock("dotenv");
34
+ vi.mock("semver", () => ({
35
+ satisfies: vi.fn(),
36
+ }));
37
+ vi.mock("../../src/lib/clients/system", () => ({
38
+ checkCommand: vi.fn(),
39
+ getVersion: vi.fn(),
40
+ executeCommand: vi.fn(),
41
+ openUrl: vi.fn(),
42
+ listDockerContainers: vi.fn(),
43
+ stopDockerContainer: vi.fn(),
44
+ removeDockerContainer: vi.fn(),
45
+ }));
46
+
47
+ vi.mock("../../src/lib/clients/jsonRpcClient", () => ({
48
+ rpcClient: {
49
+ request: vi.fn(),
50
+ },
51
+ }));
52
+
53
+ describe("SimulatorService - Basic Tests", () => {
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ vi.mocked(path.join).mockImplementation((...args) => args.join("/"));
57
+ });
58
+
59
+ test("should read the correct frontend URL from .env config", () => {
60
+ const mockEnvContent = "FRONTEND_PORT=8080";
61
+ const mockEnvConfig = {FRONTEND_PORT: "8080"};
62
+ vi.mocked(fs.readFileSync).mockReturnValue(mockEnvContent);
63
+ vi.mocked(dotenv.parse).mockReturnValue(mockEnvConfig);
64
+ const frontendUrl = simulatorService.getFrontendUrl();
65
+ expect(frontendUrl).toBe("http://localhost:8080");
66
+ });
67
+
68
+ test("should check version requirements and return missing versions", async () => {
69
+ vi.mocked(getVersion).mockResolvedValueOnce("12.0.0").mockResolvedValueOnce("18.0.0");
70
+ vi.mocked(semver.satisfies).mockImplementation((version, range) => {
71
+ if (range === VERSION_REQUIREMENTS.node) return version === "18.0.0";
72
+ return false;
73
+ });
74
+ const missingVersions = await simulatorService.checkVersionRequirements();
75
+ expect(missingVersions.node).toBe(VERSION_REQUIREMENTS.node);
76
+ expect(missingVersions.docker).toBe(VERSION_REQUIREMENTS.docker);
77
+ });
78
+
79
+ test("should handle error when checkVersion throws VersionRequiredError", async () => {
80
+ vi.mocked(getVersion).mockResolvedValueOnce("10.0.0");
81
+ vi.mocked(semver.satisfies).mockReturnValue(false);
82
+ await expect(simulatorService.checkVersion("14.0.0", "node")).rejects.toThrow();
83
+ });
84
+
85
+ test("should return initialized true when simulator responds with OK (result.status = OK)", async () => {
86
+ vi.mocked(rpcClient.request).mockResolvedValueOnce({result: {status: "OK"}});
87
+ const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS);
88
+ expect(result).toEqual({initialized: true});
89
+ expect(rpcClient.request).toHaveBeenCalledWith({method: "ping", params: []});
90
+ });
91
+
92
+ test("should return initialized true when simulator responds with OK (result.data.status = OK)", async () => {
93
+ vi.mocked(rpcClient.request).mockResolvedValueOnce({result: {data: {status: "OK"}}});
94
+ const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS);
95
+ expect(result).toEqual({initialized: true});
96
+ expect(rpcClient.request).toHaveBeenCalledWith({method: "ping", params: []});
97
+ });
98
+
99
+ test("should return initialized true when simulator responds with OK (result = OK)", async () => {
100
+ vi.mocked(rpcClient.request).mockResolvedValueOnce({result: "OK"});
101
+ const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS);
102
+ expect(result).toEqual({initialized: true});
103
+ expect(rpcClient.request).toHaveBeenCalledWith({method: "ping", params: []});
104
+ });
105
+
106
+ test("should return initialized false with errorCode TIMEOUT after retries", async () => {
107
+ vi.mocked(rpcClient.request).mockResolvedValue(undefined);
108
+ const result = await simulatorService.waitForSimulatorToBeReady(1);
109
+ expect(result).toEqual({initialized: false, errorCode: "TIMEOUT"});
110
+ });
111
+
112
+ test("should return initialized false with errorCode ERROR on non-retryable error", async () => {
113
+ const nonRetryableError = new Error("Unexpected error");
114
+ vi.mocked(rpcClient.request).mockRejectedValue(nonRetryableError);
115
+ const result = await simulatorService.waitForSimulatorToBeReady(STARTING_TIMEOUT_ATTEMPTS);
116
+ expect(result).toEqual({initialized: false, errorCode: "ERROR", errorMessage: nonRetryableError.message});
117
+ });
118
+
119
+ test("should execute the correct run simulator command based on simulator location", async () => {
120
+ (executeCommand as Mock).mockResolvedValue({
121
+ stdout: "Simulator started",
122
+ stderr: "",
123
+ });
124
+ const result = await simulatorService.runSimulator();
125
+ const expectedCommand = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorService.location, "");
126
+ expect(executeCommand).toHaveBeenCalledWith(expectedCommand);
127
+ expect(result).toEqual({stdout: "Simulator started", stderr: ""});
128
+ });
129
+
130
+ test("should execute the correct run simulator command based on headless option", async () => {
131
+ (executeCommand as Mock).mockResolvedValue({
132
+ stdout: "Simulator started",
133
+ stderr: "",
134
+ });
135
+ simulatorService.setComposeOptions(true);
136
+ const commandOption = simulatorService.getComposeOptions();
137
+ const result = await simulatorService.runSimulator();
138
+ const expectedCommand = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorService.location, commandOption);
139
+ expect(executeCommand).toHaveBeenCalledWith(expectedCommand);
140
+ expect(result).toEqual({stdout: "Simulator started", stderr: ""});
141
+ });
142
+
143
+ test("should execute the correct run simulator command based on ollama option", async () => {
144
+ (executeCommand as Mock).mockResolvedValue({
145
+ stdout: "Simulator started",
146
+ stderr: "",
147
+ });
148
+
149
+ simulatorService.setComposeOptions(false, true);
150
+ let commandOptions = simulatorService.getComposeOptions();
151
+ expect(commandOptions).toBe("--profile frontend --profile ollama");
152
+
153
+ simulatorService.setComposeOptions(true, true);
154
+ commandOptions = simulatorService.getComposeOptions();
155
+ expect(commandOptions).toBe("--profile ollama");
156
+
157
+ simulatorService.setComposeOptions(false, false);
158
+ commandOptions = simulatorService.getComposeOptions();
159
+ expect(commandOptions).toBe("--profile frontend");
160
+
161
+ simulatorService.setComposeOptions(true, false);
162
+ commandOptions = simulatorService.getComposeOptions();
163
+ expect(commandOptions).toBe("");
164
+
165
+ await simulatorService.runSimulator();
166
+ const expectedCommand = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorService.location, commandOptions);
167
+ expect(executeCommand).toHaveBeenCalledWith(expectedCommand);
168
+ });
169
+
170
+ test("should create a backup of the .env file and add new config", () => {
171
+ const envFilePath = `/.env`;
172
+ const originalEnvContent = "KEY1=value1\nKEY2=value2";
173
+ const parsedEnvConfig = {KEY1: "value1", KEY2: "value2"};
174
+ const newConfig = {KEY3: "value3", KEY2: "newValue2"};
175
+
176
+ vi.mocked(fs.readFileSync).mockImplementation((filePath: any) => {
177
+ if (filePath === envFilePath) return originalEnvContent;
178
+ return "";
179
+ });
180
+
181
+ vi.mocked(dotenv.parse).mockReturnValue(parsedEnvConfig);
182
+ const writeFileSyncMock = vi.mocked(fs.writeFileSync);
183
+
184
+ simulatorService.addConfigToEnvFile(newConfig);
185
+
186
+ const expectedUpdatedContent = `KEY1=value1\nKEY2=newValue2\nKEY3=value3`;
187
+ expect(writeFileSyncMock).toHaveBeenCalledWith(envFilePath, expectedUpdatedContent);
188
+ });
189
+
190
+ test("should handle empty .env file and add new config", () => {
191
+ const envFilePath = `/.env`;
192
+ const newConfig = {NEW_KEY: "newValue"};
193
+
194
+ vi.mocked(fs.readFileSync).mockReturnValue("");
195
+ vi.mocked(dotenv.parse).mockReturnValue({});
196
+ const writeFileSyncMock = vi.mocked(fs.writeFileSync);
197
+
198
+ simulatorService.addConfigToEnvFile(newConfig);
199
+
200
+ const expectedUpdatedContent = `NEW_KEY=newValue`;
201
+ expect(writeFileSyncMock).toHaveBeenCalledWith(envFilePath, expectedUpdatedContent);
202
+ });
203
+
204
+ test("should throw error when .env file does not exist", () => {
205
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
206
+ throw new Error("File not found");
207
+ });
208
+
209
+ expect(() => simulatorService.addConfigToEnvFile({KEY: "value"})).toThrow("File not found");
210
+ });
211
+
212
+ test("should open the frontend URL and return true", async () => {
213
+ vi.spyOn(simulatorService, "getFrontendUrl").mockReturnValue("http://localhost:8080");
214
+ const result = await simulatorService.openFrontend();
215
+ expect(simulatorService.getFrontendUrl).toHaveBeenCalled();
216
+ expect(openUrl).toHaveBeenCalledWith("http://localhost:8080");
217
+ expect(result).toBe(true);
218
+ });
219
+
220
+ test("should call rpcClient.request with correct parameters and return the response", async () => {
221
+ const mockResponse = {success: true};
222
+ vi.mocked(rpcClient.request).mockResolvedValue(mockResponse);
223
+ const result = await simulatorService.deleteAllValidators();
224
+ expect(rpcClient.request).toHaveBeenCalledWith({method: "sim_deleteAllValidators", params: []});
225
+ expect(result).toBe(mockResponse);
226
+ });
227
+
228
+ test("should return node missing version", async () => {
229
+ const unexpectedError = new VersionRequiredError("node", VERSION_REQUIREMENTS.node);
230
+ vi.spyOn(simulatorService, "checkVersion").mockRejectedValueOnce(unexpectedError).mockResolvedValueOnce();
231
+
232
+ await expect(simulatorService.checkVersionRequirements()).resolves.toStrictEqual({
233
+ docker: "",
234
+ node: VERSION_REQUIREMENTS.node,
235
+ });
236
+ });
237
+
238
+ test("should return docker missing version", async () => {
239
+ const unexpectedError = new VersionRequiredError("node", VERSION_REQUIREMENTS.docker);
240
+ vi.spyOn(simulatorService, "checkVersion").mockResolvedValueOnce().mockRejectedValueOnce(unexpectedError);
241
+
242
+ await expect(simulatorService.checkVersionRequirements()).resolves.toStrictEqual({
243
+ docker: VERSION_REQUIREMENTS.docker,
244
+ node: "",
245
+ });
246
+ });
247
+
248
+ test("should throw an unexpected error when checking node version requirements", async () => {
249
+ const unexpectedError = new Error("Unexpected error (node)");
250
+ vi.spyOn(simulatorService, "checkVersion").mockRejectedValueOnce(unexpectedError);
251
+ await expect(simulatorService.checkVersionRequirements()).rejects.toThrow("Unexpected error (node)");
252
+ });
253
+
254
+ test("should throw an unexpected error when checking docker version requirements", async () => {
255
+ vi.spyOn(simulatorService, "checkVersion")
256
+ .mockResolvedValueOnce(undefined)
257
+ .mockRejectedValueOnce(new Error("Unexpected error (docker)"));
258
+ await expect(simulatorService.checkVersionRequirements()).rejects.toThrow("Unexpected error (docker)");
259
+ });
260
+
261
+ test("should throw an unexpected error when checking git installation requirement", async () => {
262
+ vi.mocked(checkCommand).mockRejectedValueOnce(new Error("Unexpected git error"));
263
+ await expect(simulatorService.checkInstallRequirements()).rejects.toThrow("Unexpected git error");
264
+ const requirementsInstalled = {git: false, docker: false};
265
+ expect(requirementsInstalled.git).toBe(false);
266
+ });
267
+
268
+ test("should retry when response is not 'OK' and reach sleep path", async () => {
269
+ vi.mocked(rpcClient.request).mockResolvedValue({result: {status: "NOT_OK"}});
270
+ const result = await simulatorService.waitForSimulatorToBeReady(1);
271
+ expect(result).toEqual({initialized: false, errorCode: "TIMEOUT"});
272
+ });
273
+
274
+ test("should retry on fetch error and reach sleep path", async () => {
275
+ const fetchError = new Error("Fetch Error");
276
+ fetchError.name = "FetchError";
277
+ vi.mocked(rpcClient.request).mockRejectedValue(fetchError);
278
+ const result = await simulatorService.waitForSimulatorToBeReady(1);
279
+ expect(result).toEqual({initialized: false, errorCode: "ERROR", errorMessage: fetchError.message});
280
+ });
281
+
282
+ test("should call executeCommand if docker ps command fails", async () => {
283
+ vi.mocked(checkCommand).mockResolvedValueOnce(undefined);
284
+
285
+ const result = await simulatorService.checkInstallRequirements();
286
+ expect(result.docker).toBe(true);
287
+ });
288
+
289
+ test("should return providers without errors", () => {
290
+ expect(simulatorService.getAiProvidersOptions(true)).toEqual(expect.any(Array));
291
+ expect(simulatorService.getAiProvidersOptions(false)).toEqual(expect.any(Array));
292
+ });
293
+
294
+ test("should exclude specified providers from the options list", () => {
295
+ const allProviders = simulatorService.getAiProvidersOptions(false);
296
+ const providersWithoutOllama = simulatorService.getAiProvidersOptions(false, ["ollama"]);
297
+
298
+ expect(providersWithoutOllama.length).toBeLessThan(allProviders.length);
299
+
300
+ const ollamaProvider = providersWithoutOllama.find(p => p.value === "ollama");
301
+ expect(ollamaProvider).toBeUndefined();
302
+
303
+ const openaiProvider = providersWithoutOllama.find(p => p.value === "openai");
304
+ expect(openaiProvider).toBeDefined();
305
+ });
306
+
307
+ test("should exclude multiple providers when specified", () => {
308
+ const providersWithoutMultiple = simulatorService.getAiProvidersOptions(false, ["ollama", "openai"]);
309
+
310
+ const ollamaProvider = providersWithoutMultiple.find(p => p.value === "ollama");
311
+ const openaiProvider = providersWithoutMultiple.find(p => p.value === "openai");
312
+
313
+ expect(ollamaProvider).toBeUndefined();
314
+ expect(openaiProvider).toBeUndefined();
315
+
316
+ const heuristaiProvider = providersWithoutMultiple.find(p => p.value === "heuristai");
317
+ expect(heuristaiProvider).toBeDefined();
318
+ });
319
+
320
+ test("clean simulator should success", async () => {
321
+ vi.mocked(rpcClient.request).mockResolvedValueOnce("Success");
322
+ await expect(simulatorService.cleanDatabase).not.toThrow();
323
+ expect(rpcClient.request).toHaveBeenCalledWith({
324
+ method: "sim_clearDbTables",
325
+ params: [["current_state", "transactions"]],
326
+ });
327
+ });
328
+
329
+ test("should create random validators", async () => {
330
+ const numValidators = 5;
331
+ const llmProviders = ["openai", "ollama"] as AiProviders[];
332
+ const mockResponse = {success: true};
333
+ vi.mocked(rpcClient.request).mockResolvedValue(mockResponse);
334
+
335
+ const result = await simulatorService.createRandomValidators(numValidators, llmProviders);
336
+
337
+ expect(rpcClient.request).toHaveBeenCalledWith({
338
+ method: "sim_createRandomValidators",
339
+ params: [numValidators, 1, 10, llmProviders],
340
+ });
341
+ expect(result).toEqual(mockResponse);
342
+ });
343
+ });
344
+ describe("SimulatorService - Docker Tests", () => {
345
+ let mockGetContainer: Mock;
346
+ let mockListContainers: Mock;
347
+ let mockListImages: Mock;
348
+ let mockGetImage: Mock;
349
+ let mockPing: Mock;
350
+
351
+ beforeEach(() => {
352
+ vi.clearAllMocks();
353
+ mockGetContainer = vi.mocked(Docker.prototype.getContainer);
354
+ mockListContainers = vi.mocked(Docker.prototype.listContainers);
355
+ mockListImages = vi.mocked(Docker.prototype.listImages);
356
+ mockGetImage = vi.mocked(Docker.prototype.getImage);
357
+ mockPing = vi.mocked(Docker.prototype.ping);
358
+ });
359
+
360
+ test("isLocalnetRunning should return true when all required containers are running", async () => {
361
+ const mockContainers = [
362
+ {Id: "container1", Names: ["/genlayer-jsonrpc1"], State: "running"},
363
+ {Id: "container2", Names: ["/genlayer-webrequest1"], State: "running"},
364
+ {Id: "container3", Names: ["/genlayer-postgres1"], State: "running"},
365
+ {Id: "container4", Names: ["/genlayer-other-container1"], State: "running"},
366
+ {Id: "container5", Names: ["/genlayer-another-container1"], State: "exited"},
367
+ ];
368
+
369
+ mockListContainers.mockResolvedValue(mockContainers);
370
+ const result = await simulatorService.isLocalnetRunning();
371
+ expect(result).toBe(true);
372
+ });
373
+
374
+ test("isLocalnetRunning should return false when not all required containers are running", async () => {
375
+ const mockContainers = [
376
+ {Id: "container1", Names: ["/genlayer-jsonrpc2"], State: "running"},
377
+ {Id: "container2", Names: ["/genlayer-webrequest2"], State: "running"},
378
+ {Id: "container3", Names: ["/genlayer-postgres2"], State: "exited"},
379
+ {Id: "container4", Names: ["/genlayer-other-container2"], State: "running"},
380
+ {Id: "container5", Names: ["/unrelated-container2"], State: "running"},
381
+ ];
382
+
383
+ mockListContainers.mockResolvedValue(mockContainers);
384
+ const result = await simulatorService.isLocalnetRunning();
385
+ expect(result).toBe(false);
386
+ });
387
+
388
+ test("should stop and remove Docker containers with the specified prefix", async () => {
389
+ const mockContainers = [
390
+ {
391
+ Id: "container1",
392
+ Names: [`${CONTAINERS_NAME_PREFIX}container1`],
393
+ State: "running",
394
+ },
395
+ {
396
+ Id: "container2",
397
+ Names: [`${CONTAINERS_NAME_PREFIX}container2`],
398
+ State: "exited",
399
+ },
400
+ {
401
+ Id: "container3",
402
+ Names: ["/unrelated-container"],
403
+ State: "running",
404
+ },
405
+ ];
406
+
407
+ mockListContainers.mockResolvedValue(mockContainers);
408
+
409
+ const mockStop = vi.fn().mockResolvedValue(undefined);
410
+ const mockRemove = vi.fn().mockResolvedValue(undefined);
411
+ mockGetContainer.mockImplementation(
412
+ () =>
413
+ ({
414
+ stop: mockStop,
415
+ remove: mockRemove,
416
+ }) as unknown as Docker.Container,
417
+ );
418
+
419
+ const result = await simulatorService.resetDockerContainers();
420
+
421
+ expect(result).toBe(undefined);
422
+ expect(mockListContainers).toHaveBeenCalledWith({all: true});
423
+
424
+ // Ensure only the relevant containers were stopped and removed
425
+ expect(mockGetContainer).toHaveBeenCalledWith("container1");
426
+ expect(mockGetContainer).toHaveBeenCalledWith("container2");
427
+ expect(mockGetContainer).not.toHaveBeenCalledWith("container3");
428
+
429
+ expect(mockStop).toHaveBeenCalledTimes(1);
430
+ expect(mockRemove).toHaveBeenCalledTimes(2);
431
+ });
432
+
433
+ test("should stop all running GenLayer containers", async () => {
434
+ const mockContainers = [
435
+ {
436
+ Id: "container1",
437
+ Names: [`${CONTAINERS_NAME_PREFIX}container1`],
438
+ State: "running",
439
+ },
440
+ {
441
+ Id: "container2",
442
+ Names: [`${CONTAINERS_NAME_PREFIX}container2`],
443
+ State: "exited",
444
+ },
445
+ ];
446
+
447
+ vi.mocked(Docker.prototype.listContainers).mockResolvedValue(mockContainers as any);
448
+
449
+ const mockStop = vi.fn().mockResolvedValue(undefined);
450
+ const mockGetContainer = vi.mocked(Docker.prototype.getContainer);
451
+ mockGetContainer.mockImplementation(
452
+ () =>
453
+ ({
454
+ stop: mockStop,
455
+ }) as unknown as Docker.Container,
456
+ );
457
+
458
+ await simulatorService.stopDockerContainers();
459
+
460
+ expect(mockGetContainer).toHaveBeenCalledWith("container1");
461
+ expect(mockGetContainer).toHaveBeenCalledWith("container2");
462
+ expect(mockStop).toHaveBeenCalledTimes(1);
463
+ });
464
+
465
+ test("should remove Docker images with the specified prefix", async () => {
466
+ const mockImages = [
467
+ {
468
+ Id: "image1",
469
+ RepoTags: [`${IMAGES_NAME_PREFIX}image1:${localnetCompatibleVersion}`],
470
+ },
471
+ {
472
+ Id: "image2",
473
+ RepoTags: [`${IMAGES_NAME_PREFIX}image2:${localnetCompatibleVersion}`],
474
+ },
475
+ {
476
+ Id: "image3",
477
+ RepoTags: ["unrelated-image:latest"],
478
+ },
479
+ ];
480
+
481
+ mockListImages.mockResolvedValue(mockImages);
482
+
483
+ const mockRemove = vi.fn().mockResolvedValue(undefined);
484
+ mockGetImage.mockImplementation(
485
+ () =>
486
+ ({
487
+ remove: mockRemove,
488
+ }) as unknown as Docker.Image,
489
+ );
490
+
491
+ const result = await simulatorService.resetDockerImages();
492
+
493
+ expect(result).toBe(undefined);
494
+ expect(mockListImages).toHaveBeenCalled();
495
+ expect(mockGetImage).toHaveBeenCalledWith("image1");
496
+ expect(mockGetImage).toHaveBeenCalledWith("image2");
497
+ expect(mockGetImage).not.toHaveBeenCalledWith("image3");
498
+ expect(mockRemove).toHaveBeenCalledTimes(2);
499
+ expect(mockRemove).toHaveBeenCalledWith({force: true});
500
+ });
501
+
502
+ test("should remove Docker volumes with genlayer_ prefix", async () => {
503
+ const mockVolumes = {
504
+ Volumes: [
505
+ { Name: "genlayer_volume1" },
506
+ { Name: "genlayer_postgres" },
507
+ { Name: "genlayer_data" },
508
+ { Name: "unrelated_volume" },
509
+ { Name: "another_volume" },
510
+ { Name: "hardhat_artifacts" },
511
+ ],
512
+ Warnings: [],
513
+ };
514
+
515
+ const mockListVolumes = vi.mocked(Docker.prototype.listVolumes);
516
+ const mockGetVolume = vi.mocked(Docker.prototype.getVolume);
517
+
518
+ mockListVolumes.mockResolvedValue(mockVolumes as any);
519
+
520
+ const mockRemove = vi.fn().mockResolvedValue(undefined);
521
+ mockGetVolume.mockImplementation(
522
+ () =>
523
+ ({
524
+ remove: mockRemove,
525
+ }) as unknown as Docker.Volume,
526
+ );
527
+
528
+ const result = await simulatorService.resetDockerVolumes();
529
+
530
+ expect(result).toBe(undefined);
531
+ expect(mockListVolumes).toHaveBeenCalled();
532
+ expect(mockGetVolume).toHaveBeenCalledWith("genlayer_volume1");
533
+ expect(mockGetVolume).toHaveBeenCalledWith("genlayer_postgres");
534
+ expect(mockGetVolume).toHaveBeenCalledWith("genlayer_data");
535
+ expect(mockGetVolume).not.toHaveBeenCalledWith("unrelated_volume");
536
+ expect(mockGetVolume).not.toHaveBeenCalledWith("another_volume");
537
+ expect(mockGetVolume).not.toHaveBeenCalledWith("hardhat_artifacts");
538
+ expect(mockRemove).toHaveBeenCalledTimes(3);
539
+ expect(mockRemove).toHaveBeenCalledWith({force: true});
540
+ });
541
+
542
+ test("should execute command when docker is installed but is not available", async () => {
543
+ vi.mocked(checkCommand).mockResolvedValueOnce(undefined);
544
+
545
+ mockPing.mockRejectedValueOnce("");
546
+ await simulatorService.checkInstallRequirements();
547
+ expect(executeCommand).toHaveBeenCalledTimes(1);
548
+ });
549
+
550
+ test("should call execute command again to start docker service", async () => {
551
+ vi.mocked(checkCommand).mockResolvedValueOnce(undefined).mockRejectedValue(undefined);
552
+ mockPing.mockRejectedValueOnce("");
553
+ await expect(simulatorService.checkInstallRequirements()).resolves.toStrictEqual({docker: true});
554
+ });
555
+
556
+ test("should warn the user when an update is available", async () => {
557
+ const update = {latest: "1.1.0"};
558
+ (updateCheck as any).mockResolvedValue(update);
559
+
560
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
561
+
562
+ await simulatorService.checkCliVersion();
563
+
564
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
565
+ `\nA new version (${update.latest}) is available! You're using version 1.0.0.\nRun npm install -g genlayer to update\n`,
566
+ );
567
+
568
+ consoleWarnSpy.mockRestore();
569
+ });
570
+
571
+ test("should not warn the user when the CLI is up-to-date", async () => {
572
+ const update = {latest: "1.0.0"};
573
+ (updateCheck as any).mockResolvedValue(update);
574
+
575
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
576
+
577
+ await simulatorService.checkCliVersion();
578
+
579
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
580
+
581
+ consoleWarnSpy.mockRestore();
582
+ });
583
+
584
+ test("should handle update-check returning undefined", async () => {
585
+ (updateCheck as any).mockResolvedValue(undefined);
586
+
587
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
588
+
589
+ await simulatorService.checkCliVersion();
590
+
591
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
592
+
593
+ consoleWarnSpy.mockRestore();
594
+ });
595
+ });
596
+
597
+ describe("normalizeLocalnetVersion", () => {
598
+ test('should add "v" if not present', () => {
599
+ expect(simulatorService.normalizeLocalnetVersion("0.26.0")).toBe("v0.26.0");
600
+ });
601
+
602
+ test('should preserve "v" if already present', () => {
603
+ expect(simulatorService.normalizeLocalnetVersion("v0.26.0")).toBe("v0.26.0");
604
+ });
605
+
606
+ test('should retain suffixes like "-test000"', () => {
607
+ expect(simulatorService.normalizeLocalnetVersion("0.25.0-test000")).toBe("v0.25.0-test000");
608
+ expect(simulatorService.normalizeLocalnetVersion("v1.0.0-alpha")).toBe("v1.0.0-alpha");
609
+ });
610
+
611
+ test("should handle versions with numbers only", () => {
612
+ expect(simulatorService.normalizeLocalnetVersion("1.0.0")).toBe("v1.0.0");
613
+ });
614
+
615
+ test("should throw an error and exit for invalid versions", () => {
616
+ const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
617
+ return undefined as never;
618
+ });
619
+ const mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {});
620
+
621
+ simulatorService.normalizeLocalnetVersion("invalid-version");
622
+
623
+ expect(mockConsoleError).toHaveBeenCalledWith(
624
+ "Invalid version format. Expected format: v0.0.0 or v0.0.0-suffix",
625
+ );
626
+ expect(mockExit).toHaveBeenCalledWith(1);
627
+
628
+ mockExit.mockRestore();
629
+ mockConsoleError.mockRestore();
630
+ });
631
+ test("should log an error if an exception occurs while cleaning the database", async () => {
632
+ const mockError = new Error("Database cleanup error");
633
+ vi.mocked(rpcClient.request).mockRejectedValue(mockError);
634
+
635
+ console.error = vi.fn();
636
+
637
+ await simulatorService.cleanDatabase();
638
+
639
+ expect(rpcClient.request).toHaveBeenCalledWith({
640
+ method: "sim_clearDbTables",
641
+ params: [["current_state", "transactions"]],
642
+ });
643
+ expect(console.error).toHaveBeenCalledWith(mockError);
644
+ });
645
+ });
646
+
647
+ describe("compareVersions", () => {
648
+ test("should return 0 when versions are equal", () => {
649
+ expect(simulatorService.compareVersions("v1.0.0", "v1.0.0")).toBe(0);
650
+ expect(simulatorService.compareVersions("1.0.0", "v1.0.0")).toBe(0);
651
+ expect(simulatorService.compareVersions("v1.0.0", "1.0.0")).toBe(0);
652
+ expect(simulatorService.compareVersions("1.0.0", "1.0.0")).toBe(0);
653
+ });
654
+
655
+ test("should return -1 when first version is less than second", () => {
656
+ expect(simulatorService.compareVersions("v0.64.0", "v0.65.0")).toBe(-1);
657
+ expect(simulatorService.compareVersions("v0.65.0", "v0.66.0")).toBe(-1);
658
+ expect(simulatorService.compareVersions("v0.64.9", "v0.65.0")).toBe(-1);
659
+ expect(simulatorService.compareVersions("v1.0.0", "v2.0.0")).toBe(-1);
660
+ expect(simulatorService.compareVersions("v1.0.0", "v1.1.0")).toBe(-1);
661
+ expect(simulatorService.compareVersions("v1.0.0", "v1.0.1")).toBe(-1);
662
+ });
663
+
664
+ test("should return 1 when first version is greater than second", () => {
665
+ expect(simulatorService.compareVersions("v0.66.0", "v0.65.0")).toBe(1);
666
+ expect(simulatorService.compareVersions("v0.65.1", "v0.65.0")).toBe(1);
667
+ expect(simulatorService.compareVersions("v2.0.0", "v1.0.0")).toBe(1);
668
+ expect(simulatorService.compareVersions("v1.1.0", "v1.0.0")).toBe(1);
669
+ expect(simulatorService.compareVersions("v1.0.1", "v1.0.0")).toBe(1);
670
+ });
671
+
672
+ test("should handle versions with different number of parts", () => {
673
+ expect(simulatorService.compareVersions("v1.0", "v1.0.0")).toBe(0);
674
+ expect(simulatorService.compareVersions("v1.0.0", "v1.0")).toBe(0);
675
+ expect(simulatorService.compareVersions("v1.0", "v1.0.1")).toBe(-1);
676
+ expect(simulatorService.compareVersions("v1.0.1", "v1.0")).toBe(1);
677
+ });
678
+
679
+ test("should handle versions without 'v' prefix", () => {
680
+ expect(simulatorService.compareVersions("1.0.0", "1.0.0")).toBe(0);
681
+ expect(simulatorService.compareVersions("1.0.0", "1.0.1")).toBe(-1);
682
+ expect(simulatorService.compareVersions("1.0.1", "1.0.0")).toBe(1);
683
+ });
684
+
685
+ test("should handle mixed prefix versions", () => {
686
+ expect(simulatorService.compareVersions("v1.0.0", "1.0.0")).toBe(0);
687
+ expect(simulatorService.compareVersions("1.0.0", "v1.0.1")).toBe(-1);
688
+ expect(simulatorService.compareVersions("v1.0.1", "1.0.0")).toBe(1);
689
+ });
690
+
691
+ test("should handle pre-release versions by comparing base version", () => {
692
+ expect(simulatorService.compareVersions("v1.0.0-alpha", "v1.0.0")).toBe(0);
693
+ expect(simulatorService.compareVersions("v1.0.0-beta", "v1.0.0-alpha")).toBe(0);
694
+ expect(simulatorService.compareVersions("v1.0.0-alpha", "v1.0.1")).toBe(-1);
695
+ expect(simulatorService.compareVersions("v1.0.1-beta", "v1.0.0")).toBe(1);
696
+ expect(simulatorService.compareVersions("v1.0.0-test000", "v1.0.0-beta")).toBe(0);
697
+ });
698
+
699
+ test("should handle mixed pre-release and regular versions", () => {
700
+ expect(simulatorService.compareVersions("1.0.0-alpha", "v1.0.0")).toBe(0);
701
+ expect(simulatorService.compareVersions("v1.0.0", "1.0.0-beta")).toBe(0);
702
+ expect(simulatorService.compareVersions("1.0.0-alpha", "v1.0.1")).toBe(-1);
703
+ expect(simulatorService.compareVersions("v1.0.1-beta", "1.0.0")).toBe(1);
704
+ });
705
+ });
706
+
707
+ describe("setupLocalhostAccess", () => {
708
+ beforeEach(() => {
709
+ vi.clearAllMocks();
710
+ vi.mocked(path.join).mockImplementation((...args) => args.join("/"));
711
+ });
712
+
713
+ test("should write docker-compose.override.yml to location", () => {
714
+ vi.mocked(fs.existsSync).mockReturnValue(true);
715
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
716
+
717
+ simulatorService.setupLocalhostAccess();
718
+
719
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
720
+ const overrideCall = writeCalls.find(c =>
721
+ (c[0] as string).includes("docker-compose.override.yml")
722
+ );
723
+ expect(overrideCall).toBeDefined();
724
+ expect(overrideCall![1]).toContain("jsonrpc");
725
+ expect(overrideCall![1]).toContain("extra_hosts");
726
+ expect(overrideCall![1]).toContain("host.docker.internal:host-gateway");
727
+
728
+ consoleWarnSpy.mockRestore();
729
+ });
730
+
731
+ test("should create config-overrides directory if it does not exist", () => {
732
+ vi.mocked(fs.existsSync).mockReturnValue(false);
733
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
734
+
735
+ simulatorService.setupLocalhostAccess();
736
+
737
+ expect(fs.mkdirSync).toHaveBeenCalledWith(
738
+ expect.stringContaining("config-overrides"),
739
+ { recursive: true }
740
+ );
741
+
742
+ consoleWarnSpy.mockRestore();
743
+ });
744
+
745
+ test("should not create config-overrides directory if it already exists", () => {
746
+ vi.mocked(fs.existsSync).mockReturnValue(true);
747
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
748
+
749
+ simulatorService.setupLocalhostAccess();
750
+
751
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
752
+
753
+ consoleWarnSpy.mockRestore();
754
+ });
755
+
756
+ test("should write genvm-module-web.yaml with localhost access hosts", () => {
757
+ vi.mocked(fs.existsSync).mockReturnValue(true);
758
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
759
+
760
+ simulatorService.setupLocalhostAccess();
761
+
762
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
763
+ const configCall = writeCalls.find(c =>
764
+ (c[0] as string).includes("genvm-module-web.yaml")
765
+ );
766
+ expect(configCall).toBeDefined();
767
+ const content = configCall![1] as string;
768
+ expect(content).toContain("always_allow_hosts");
769
+ expect(content).toContain("localhost");
770
+ expect(content).toContain("127.0.0.1");
771
+ expect(content).toContain("host.docker.internal");
772
+ expect(content).toContain("anvil-local");
773
+
774
+ consoleWarnSpy.mockRestore();
775
+ });
776
+
777
+ test("should print a warning about localhost access", () => {
778
+ vi.mocked(fs.existsSync).mockReturnValue(true);
779
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
780
+
781
+ simulatorService.setupLocalhostAccess();
782
+
783
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
784
+ expect.stringContaining("Localhost access enabled")
785
+ );
786
+
787
+ consoleWarnSpy.mockRestore();
788
+ });
789
+ });