movehat 0.2.1 → 0.2.3

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 (176) hide show
  1. package/dist/__tests__/deployContract.test.js +56 -47
  2. package/dist/__tests__/deployContract.test.js.map +1 -1
  3. package/dist/__tests__/exports.test.d.ts +2 -0
  4. package/dist/__tests__/exports.test.d.ts.map +1 -0
  5. package/dist/__tests__/exports.test.js +30 -0
  6. package/dist/__tests__/exports.test.js.map +1 -0
  7. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
  8. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
  9. package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
  10. package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
  11. package/dist/__tests__/fork/api.test.js +5 -0
  12. package/dist/__tests__/fork/api.test.js.map +1 -1
  13. package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
  14. package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
  15. package/dist/__tests__/fork/api.timeout.test.js +98 -0
  16. package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
  17. package/dist/cli.js +4 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
  20. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
  21. package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
  22. package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
  23. package/dist/commands/__tests__/init.test.js +73 -11
  24. package/dist/commands/__tests__/init.test.js.map +1 -1
  25. package/dist/commands/compile.d.ts.map +1 -1
  26. package/dist/commands/compile.js +19 -10
  27. package/dist/commands/compile.js.map +1 -1
  28. package/dist/commands/init.d.ts +22 -0
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +55 -6
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/test.js +12 -19
  33. package/dist/commands/test.js.map +1 -1
  34. package/dist/core/AccountManager.d.ts.map +1 -1
  35. package/dist/core/AccountManager.js +14 -2
  36. package/dist/core/AccountManager.js.map +1 -1
  37. package/dist/core/Publisher.d.ts.map +1 -1
  38. package/dist/core/Publisher.js +72 -82
  39. package/dist/core/Publisher.js.map +1 -1
  40. package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
  41. package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
  42. package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
  43. package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
  44. package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
  45. package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
  46. package/dist/core/__tests__/movementProfile.test.js +112 -0
  47. package/dist/core/__tests__/movementProfile.test.js.map +1 -0
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +14 -10
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/deployments.d.ts.map +1 -1
  52. package/dist/core/deployments.js +4 -2
  53. package/dist/core/deployments.js.map +1 -1
  54. package/dist/core/movementProfile.d.ts +55 -22
  55. package/dist/core/movementProfile.d.ts.map +1 -1
  56. package/dist/core/movementProfile.js +77 -99
  57. package/dist/core/movementProfile.js.map +1 -1
  58. package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
  59. package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
  60. package/dist/fork/__tests__/server.cors.test.js +79 -0
  61. package/dist/fork/__tests__/server.cors.test.js.map +1 -0
  62. package/dist/fork/api.d.ts +9 -1
  63. package/dist/fork/api.d.ts.map +1 -1
  64. package/dist/fork/api.js +37 -7
  65. package/dist/fork/api.js.map +1 -1
  66. package/dist/fork/manager.js +10 -10
  67. package/dist/fork/manager.js.map +1 -1
  68. package/dist/fork/server.d.ts +20 -1
  69. package/dist/fork/server.d.ts.map +1 -1
  70. package/dist/fork/server.js +40 -24
  71. package/dist/fork/server.js.map +1 -1
  72. package/dist/fork/test.d.ts.map +1 -1
  73. package/dist/fork/test.js +3 -2
  74. package/dist/fork/test.js.map +1 -1
  75. package/dist/harness/Harness.d.ts +6 -2
  76. package/dist/harness/Harness.d.ts.map +1 -1
  77. package/dist/harness/Harness.js +8 -2
  78. package/dist/harness/Harness.js.map +1 -1
  79. package/dist/harness/codeObject.d.ts.map +1 -1
  80. package/dist/harness/codeObject.js +41 -41
  81. package/dist/harness/codeObject.js.map +1 -1
  82. package/dist/harness/script.d.ts +3 -3
  83. package/dist/harness/script.d.ts.map +1 -1
  84. package/dist/harness/script.js +42 -35
  85. package/dist/harness/script.js.map +1 -1
  86. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
  87. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
  88. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
  89. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
  90. package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
  91. package/dist/helpers/setupLocalTesting.js +31 -5
  92. package/dist/helpers/setupLocalTesting.js.map +1 -1
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/node/LocalNodeManager.d.ts +8 -0
  96. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  97. package/dist/node/LocalNodeManager.js +70 -23
  98. package/dist/node/LocalNodeManager.js.map +1 -1
  99. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
  100. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
  101. package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
  102. package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
  103. package/dist/node/__tests__/LocalNodeManager.test.js +114 -14
  104. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  105. package/dist/templates/move/Move.toml +1 -1
  106. package/dist/templates/move/sources/Counter.move +31 -4
  107. package/dist/templates/scripts/deploy-counter.ts +10 -0
  108. package/dist/types/config.d.ts +8 -1
  109. package/dist/types/config.d.ts.map +1 -1
  110. package/dist/ui/__tests__/logger.test.d.ts +2 -0
  111. package/dist/ui/__tests__/logger.test.d.ts.map +1 -0
  112. package/dist/ui/__tests__/logger.test.js +75 -0
  113. package/dist/ui/__tests__/logger.test.js.map +1 -0
  114. package/dist/ui/formatters.d.ts +0 -16
  115. package/dist/ui/formatters.d.ts.map +1 -1
  116. package/dist/ui/formatters.js +1 -1
  117. package/dist/ui/formatters.js.map +1 -1
  118. package/dist/ui/logger.d.ts +41 -0
  119. package/dist/ui/logger.d.ts.map +1 -1
  120. package/dist/ui/logger.js +49 -0
  121. package/dist/ui/logger.js.map +1 -1
  122. package/dist/ui/spinner.d.ts +25 -0
  123. package/dist/ui/spinner.d.ts.map +1 -1
  124. package/dist/ui/spinner.js +44 -0
  125. package/dist/ui/spinner.js.map +1 -1
  126. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
  127. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
  128. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
  129. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
  130. package/dist/utils/childProcessAdapter.d.ts +7 -0
  131. package/dist/utils/childProcessAdapter.d.ts.map +1 -1
  132. package/dist/utils/childProcessAdapter.js +20 -2
  133. package/dist/utils/childProcessAdapter.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/__tests__/deployContract.test.ts +59 -50
  136. package/src/__tests__/exports.test.ts +32 -0
  137. package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
  138. package/src/__tests__/fork/api.test.ts +5 -0
  139. package/src/__tests__/fork/api.timeout.test.ts +150 -0
  140. package/src/cli.ts +4 -0
  141. package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
  142. package/src/commands/__tests__/init.test.ts +96 -11
  143. package/src/commands/compile.ts +24 -15
  144. package/src/commands/init.ts +77 -6
  145. package/src/commands/test.ts +12 -19
  146. package/src/core/AccountManager.ts +18 -1
  147. package/src/core/Publisher.ts +103 -107
  148. package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
  149. package/src/core/__tests__/movementProfile.test.ts +131 -0
  150. package/src/core/config.ts +18 -11
  151. package/src/core/deployments.ts +5 -4
  152. package/src/core/movementProfile.ts +75 -127
  153. package/src/fork/__tests__/server.cors.test.ts +101 -0
  154. package/src/fork/api.ts +69 -10
  155. package/src/fork/manager.ts +10 -10
  156. package/src/fork/server.ts +59 -24
  157. package/src/fork/test.ts +3 -2
  158. package/src/harness/Harness.ts +11 -2
  159. package/src/harness/codeObject.ts +45 -48
  160. package/src/harness/script.ts +47 -43
  161. package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
  162. package/src/helpers/setupLocalTesting.ts +39 -5
  163. package/src/index.ts +9 -1
  164. package/src/node/LocalNodeManager.ts +87 -26
  165. package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
  166. package/src/node/__tests__/LocalNodeManager.test.ts +144 -17
  167. package/src/templates/move/Move.toml +1 -1
  168. package/src/templates/move/sources/Counter.move +31 -4
  169. package/src/templates/scripts/deploy-counter.ts +10 -0
  170. package/src/types/config.ts +8 -1
  171. package/src/ui/__tests__/logger.test.ts +89 -0
  172. package/src/ui/formatters.ts +1 -1
  173. package/src/ui/logger.ts +62 -0
  174. package/src/ui/spinner.ts +47 -0
  175. package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
  176. package/src/utils/childProcessAdapter.ts +32 -2
@@ -0,0 +1,212 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * F1 — Harness.createFork(network) must honor the requested network.
8
+ *
9
+ * Mocks ForkManager so we can capture which RPC URL the fresh-fork
10
+ * code path picks. Strategy mirrors the pattern in
11
+ * src/fork/__tests__/manager.test.ts: replace the manager module with
12
+ * a stub that records every call to `initialize`.
13
+ *
14
+ * The mock also implements `load()` + `getMetadata()` so the
15
+ * "fork-exists, wrong network" case (audit-f1 follow-up) can exercise
16
+ * the metadata-mismatch guard in the `else` branch of `setupWithFork`.
17
+ */
18
+
19
+ interface InitCall {
20
+ nodeUrl: string;
21
+ networkName?: string;
22
+ apiKey?: string;
23
+ }
24
+ const initializeCalls: InitCall[] = [];
25
+
26
+ vi.mock("../../fork/manager.js", async () => {
27
+ const fs = await import("node:fs");
28
+ return {
29
+ ForkManager: class {
30
+ forkPath: string;
31
+ metadata: { network: string; nodeUrl: string } | null = null;
32
+ constructor(forkPath: string) {
33
+ this.forkPath = forkPath;
34
+ }
35
+ async initialize(nodeUrl: string, networkName?: string, apiKey?: string) {
36
+ const entry: InitCall = { nodeUrl };
37
+ if (networkName !== undefined) entry.networkName = networkName;
38
+ if (apiKey !== undefined) entry.apiKey = apiKey;
39
+ initializeCalls.push(entry);
40
+ this.metadata = { network: networkName ?? "custom", nodeUrl };
41
+ }
42
+ load() {
43
+ const raw = fs.readFileSync(`${this.forkPath}/metadata.json`, "utf-8");
44
+ this.metadata = JSON.parse(raw);
45
+ }
46
+ getMetadata() {
47
+ if (!this.metadata) {
48
+ throw new Error("Fork not initialized");
49
+ }
50
+ return this.metadata;
51
+ }
52
+ setApiKey() {}
53
+ async resetState() {}
54
+ async fundAccount() {}
55
+ async fundMultipleAccounts() {}
56
+ },
57
+ };
58
+ });
59
+
60
+ vi.mock("../../fork/server.js", () => {
61
+ return {
62
+ ForkServer: class {
63
+ constructor(_p: string, _port: number) {}
64
+ async start() {}
65
+ async stop() {}
66
+ },
67
+ };
68
+ });
69
+
70
+ vi.mock("../../runtime.js", () => ({
71
+ initRuntime: vi.fn(async () => ({})),
72
+ }));
73
+
74
+ vi.mock("../../core/AccountManager.js", () => {
75
+ let _seq = 0;
76
+ return {
77
+ AccountManager: {
78
+ createBatch(labels: readonly string[]) {
79
+ const out: Record<string, { accountAddress: { toString(): string } }> = {};
80
+ for (const l of labels) {
81
+ _seq++;
82
+ const addr = "0x" + _seq.toString(16).padStart(64, "0");
83
+ out[l] = { accountAddress: { toString: () => addr } };
84
+ }
85
+ return out;
86
+ },
87
+ exportPrivateKeys(_labels: readonly string[]) {
88
+ return { deployer: "0x" + "1".repeat(64) };
89
+ },
90
+ },
91
+ };
92
+ });
93
+
94
+ // Imported after mocks so vi.hoisted ordering applies.
95
+ import { setupLocalTesting } from "../setupLocalTesting.js";
96
+ import { logger } from "../../ui/index.js";
97
+
98
+ describe("F1 — setupLocalTesting honors forkNetwork", () => {
99
+ let cwdBackup: string;
100
+ let tmpRoot: string;
101
+
102
+ beforeEach(() => {
103
+ initializeCalls.length = 0;
104
+ cwdBackup = process.cwd();
105
+ tmpRoot = mkdtempSync(join(tmpdir(), "movehat-f1-"));
106
+ process.chdir(tmpRoot);
107
+ vi.spyOn(logger, "step").mockImplementation(() => undefined);
108
+ vi.spyOn(logger, "success").mockImplementation(() => undefined);
109
+ vi.spyOn(logger, "plain").mockImplementation(() => undefined);
110
+ vi.spyOn(logger, "newline").mockImplementation(() => undefined);
111
+ vi.spyOn(logger, "warning").mockImplementation(() => undefined);
112
+ vi.spyOn(logger, "error").mockImplementation(() => undefined);
113
+ });
114
+
115
+ afterEach(() => {
116
+ process.chdir(cwdBackup);
117
+ rmSync(tmpRoot, { recursive: true, force: true });
118
+ vi.restoreAllMocks();
119
+ });
120
+
121
+ it("uses the mainnet RPC when forkNetwork = 'mainnet'", async () => {
122
+ await setupLocalTesting({
123
+ mode: "fork",
124
+ forkNetwork: "mainnet",
125
+ accountLabels: ["deployer"],
126
+ autoFund: false,
127
+ });
128
+
129
+ expect(initializeCalls).toHaveLength(1);
130
+ const call = initializeCalls[0]!;
131
+ expect(call.networkName).toBe("mainnet");
132
+ expect(call.nodeUrl).not.toMatch(/testnet/i);
133
+ expect(call.nodeUrl).toMatch(/mainnet/i);
134
+ });
135
+
136
+ it("uses the testnet RPC when forkNetwork = 'testnet'", async () => {
137
+ await setupLocalTesting({
138
+ mode: "fork",
139
+ forkNetwork: "testnet",
140
+ accountLabels: ["deployer"],
141
+ autoFund: false,
142
+ });
143
+
144
+ expect(initializeCalls).toHaveLength(1);
145
+ const call = initializeCalls[0]!;
146
+ expect(call.networkName).toBe("testnet");
147
+ expect(call.nodeUrl).toMatch(/testnet/i);
148
+ });
149
+
150
+ it("uses forkRpcUrl override when supplied for a custom network", async () => {
151
+ await setupLocalTesting({
152
+ mode: "fork",
153
+ forkNetwork: "custom",
154
+ forkRpcUrl: "https://my-custom-node.example/v1",
155
+ accountLabels: ["deployer"],
156
+ autoFund: false,
157
+ });
158
+
159
+ expect(initializeCalls).toHaveLength(1);
160
+ const call = initializeCalls[0]!;
161
+ expect(call.networkName).toBe("custom");
162
+ expect(call.nodeUrl).toBe("https://my-custom-node.example/v1");
163
+ });
164
+
165
+ it("rejects a non-built-in forkNetwork when no forkRpcUrl is provided", async () => {
166
+ await expect(
167
+ setupLocalTesting({
168
+ mode: "fork",
169
+ forkNetwork: "some-unknown-network",
170
+ accountLabels: ["deployer"],
171
+ autoFund: false,
172
+ })
173
+ ).rejects.toThrow(/forkRpcUrl/i);
174
+ expect(initializeCalls).toHaveLength(0);
175
+ });
176
+
177
+ it("rejects when an existing fork's saved network does not match the requested one (audit-f1 follow-up)", async () => {
178
+ // Pre-seed `.movehat/forks/test-local/metadata.json` with the
179
+ // wrong network so the `forkExists` branch fires and loads stale
180
+ // metadata. Without the metadata-mismatch guard, setupLocalTesting
181
+ // would silently serve a testnet snapshot while the caller thinks
182
+ // it's reading mainnet.
183
+ const forkDir = join(tmpRoot, ".movehat", "forks", "test-local");
184
+ mkdirSync(forkDir, { recursive: true });
185
+ writeFileSync(
186
+ join(forkDir, "metadata.json"),
187
+ JSON.stringify({
188
+ network: "testnet",
189
+ nodeUrl: "https://testnet.movementnetwork.xyz/v1",
190
+ chainId: 250,
191
+ ledgerVersion: "0",
192
+ timestamp: "0",
193
+ epoch: "0",
194
+ blockHeight: "0",
195
+ createdAt: new Date().toISOString(),
196
+ }),
197
+ );
198
+
199
+ await expect(
200
+ setupLocalTesting({
201
+ mode: "fork",
202
+ forkNetwork: "mainnet",
203
+ accountLabels: ["deployer"],
204
+ autoFund: false,
205
+ })
206
+ ).rejects.toThrow(/network mismatch|created for|requested/i);
207
+ // Must not have re-initialized — the existing dir is what poisons
208
+ // the load path, and silently reinitializing would clobber the
209
+ // user's saved snapshot.
210
+ expect(initializeCalls).toHaveLength(0);
211
+ });
212
+ });
@@ -9,6 +9,25 @@ import { AccountManager } from "../core/AccountManager.js";
9
9
  import { logger } from "../ui/index.js";
10
10
  import type { LocalTestOptions } from "../types/config.js";
11
11
 
12
+ const BUILTIN_FORK_RPCS: Record<string, string> = {
13
+ testnet: "https://testnet.movementnetwork.xyz/v1",
14
+ mainnet: "https://mainnet.movementnetwork.xyz/v1",
15
+ };
16
+
17
+ function resolveForkRpcUrl(
18
+ network: string,
19
+ override: string | undefined
20
+ ): string {
21
+ if (override !== undefined) return override;
22
+ const builtin = BUILTIN_FORK_RPCS[network];
23
+ if (builtin !== undefined) return builtin;
24
+ throw new Error(
25
+ `Cannot fork unknown network "${network}" without a forkRpcUrl. ` +
26
+ `Either pass forkRpcUrl in LocalTestOptions or use one of: ` +
27
+ `${Object.keys(BUILTIN_FORK_RPCS).join(", ")}.`
28
+ );
29
+ }
30
+
12
31
  /**
13
32
  * Context returned by {@link setupLocalTesting}.
14
33
  *
@@ -76,9 +95,9 @@ export async function setupLocalTesting(
76
95
  const accountLabels = options.accountLabels || ["deployer", "alice", "bob"];
77
96
 
78
97
  logger.newline();
79
- logger.step("Setting up local testing environment...");
80
- logger.plain(` Mode: ${mode}`);
81
- logger.plain(` Accounts: ${accountLabels.join(", ")}`);
98
+ logger.phase("Setting up local testing environment");
99
+ logger.kv("Mode", mode, 2);
100
+ logger.kv("Accounts", accountLabels.join(", "), 2);
82
101
  logger.newline();
83
102
 
84
103
  if (mode === 'local-node') {
@@ -264,8 +283,8 @@ async function setupWithFork(
264
283
 
265
284
  if (!forkExists) {
266
285
  logger.step(`Creating fork from ${forkNetwork}...`);
267
- const testnetRpc = "https://testnet.movementnetwork.xyz/v1";
268
- await forkManager.initialize(testnetRpc, forkNetwork, options.forkApiKey);
286
+ const rpcUrl = resolveForkRpcUrl(forkNetwork, options.forkRpcUrl);
287
+ await forkManager.initialize(rpcUrl, forkNetwork, options.forkApiKey);
269
288
  logger.success(`Fork created at ${forkPath}`);
270
289
  logger.newline();
271
290
  } else {
@@ -278,6 +297,21 @@ async function setupWithFork(
278
297
  }
279
298
  forkManager.load();
280
299
 
300
+ // Guard against the audit-f1 follow-up case: the default forkName
301
+ // ("test-local") doesn't encode the network, so a fork created for
302
+ // testnet would silently serve mainnet requests. Refuse to load
303
+ // when the saved metadata's network doesn't match what the caller
304
+ // asked for — the user must either pass a network-specific
305
+ // `forkName` or delete the stale directory.
306
+ const savedNetwork = forkManager.getMetadata().network;
307
+ if (savedNetwork !== forkNetwork) {
308
+ throw new Error(
309
+ `Fork at ${forkPath} was created for network "${savedNetwork}" but ` +
310
+ `you requested "${forkNetwork}". Use a different forkName ` +
311
+ `(e.g. "${forkNetwork}-local") or delete ${forkPath} to recreate.`
312
+ );
313
+ }
314
+
281
315
  if (forkResetState) {
282
316
  logger.step("Resetting fork state...");
283
317
  await forkManager.resetState();
package/src/index.ts CHANGED
@@ -19,4 +19,12 @@ export type { ForkMetadata, AccountState, LedgerInfo, AccountData, AccountResour
19
19
  export { ModuleAlreadyDeployedError, PostPublishError } from "./errors.js";
20
20
 
21
21
  export { Harness, HarnessDisposedError } from "./harness/index.js";
22
- export type { HarnessMode } from "./harness/index.js";
22
+ export type { HarnessMode } from "./harness/index.js";
23
+ export type {
24
+ DeployCodeObjectOptions,
25
+ UpgradeCodeObjectOptions,
26
+ CodeObjectInfo,
27
+ RunViewFunctionOptions,
28
+ RunMoveScriptOptions,
29
+ MoveScriptResult,
30
+ } from "./types/harness.js";
@@ -6,13 +6,30 @@ import {
6
6
  type ChildProcessAdapter,
7
7
  type SpawnedProcess,
8
8
  } from "../utils/childProcessAdapter.js";
9
- import { logger } from "../ui/index.js";
9
+ import { logger, isVerbose, colors, symbols } from "../ui/index.js";
10
+ import { withTimedSpinner, withSpinner } from "../ui/spinner.js";
11
+
12
+ /**
13
+ * Substrings that always surface from the movement subprocess regardless
14
+ * of verbosity. These are signals the user must see to debug a stuck
15
+ * startup (panic, fatal, address-in-use). Tested in
16
+ * __tests__/LocalNodeManager.test.ts to guard against silent regressions.
17
+ */
18
+ const CRITICAL_NODE_OUTPUT = /panic|fatal|address already in use|EADDRINUSE/i;
10
19
 
11
20
  export interface LocalNodeOptions {
12
21
  testDir?: string; // Directory for node data (default: .movehat/local-node)
13
22
  forceRestart?: boolean; // Clean state and start fresh
14
23
  faucetPort?: number; // Faucet port (default: 8081)
15
- apiPort?: number; // API/RPC port (default: 8080)
24
+ /**
25
+ * REST API port. Movement CLI (`movement node run-localnet`) does
26
+ * not accept a flag to change this — the node always binds 8080.
27
+ * Passing any other value triggers a warning at construction time
28
+ * and is replaced with 8080. Field is kept for source compatibility.
29
+ *
30
+ * @deprecated Movement CLI does not honor this. Omit it.
31
+ */
32
+ apiPort?: number;
16
33
  readyPort?: number; // Ready server port (default: 8070)
17
34
  silent?: boolean; // Suppress node output
18
35
  /**
@@ -22,6 +39,8 @@ export interface LocalNodeOptions {
22
39
  adapter?: ChildProcessAdapter;
23
40
  }
24
41
 
42
+ const MOVEMENT_API_PORT = 8080;
43
+
25
44
  export interface LocalNodeInfo {
26
45
  rpcUrl: string;
27
46
  faucetUrl: string;
@@ -47,11 +66,23 @@ export class LocalNodeManager {
47
66
 
48
67
  constructor(options: LocalNodeOptions = {}) {
49
68
  this.adapter = options.adapter ?? defaultChildProcessAdapter;
69
+ if (
70
+ options.apiPort !== undefined &&
71
+ options.apiPort !== MOVEMENT_API_PORT
72
+ ) {
73
+ // Movement CLI hardcodes the REST API port to 8080. Surfacing
74
+ // the requested port via getNodeInfo() would lie about where
75
+ // the node actually listens.
76
+ logger.warning(
77
+ `LocalNodeManager: apiPort=${options.apiPort} is not supported by ` +
78
+ `movement node run-localnet; forcing REST API port to 8080.`
79
+ );
80
+ }
50
81
  this.options = {
51
82
  testDir: options.testDir || join(process.cwd(), ".movehat", "local-node"),
52
83
  forceRestart: options.forceRestart ?? false,
53
84
  faucetPort: options.faucetPort || 8081,
54
- apiPort: options.apiPort || 8080,
85
+ apiPort: MOVEMENT_API_PORT,
55
86
  readyPort: options.readyPort || 8070,
56
87
  silent: options.silent ?? false,
57
88
  };
@@ -64,7 +95,7 @@ export class LocalNodeManager {
64
95
  */
65
96
  async start(): Promise<LocalNodeInfo> {
66
97
  if (this.spawned) {
67
- console.log("Local node already running");
98
+ logger.info("Local node already running");
68
99
  return this.getNodeInfo();
69
100
  }
70
101
 
@@ -76,11 +107,11 @@ export class LocalNodeManager {
76
107
 
77
108
  try {
78
109
  logger.newline();
79
- logger.step("Starting local Movement node...");
80
- logger.plain(` Test directory: ${this.options.testDir}`);
81
- logger.plain(` RPC port: ${this.options.apiPort}`);
82
- logger.plain(` Faucet port: ${this.options.faucetPort}`);
83
- logger.plain(` Ready port: ${this.options.readyPort}`);
110
+ logger.step("Starting local Movement node");
111
+ logger.kv("Test directory", this.options.testDir, 2);
112
+ logger.kv("RPC port", String(this.options.apiPort), 2);
113
+ logger.kv("Faucet port", String(this.options.faucetPort), 2);
114
+ logger.kv("Ready port", String(this.options.readyPort), 2);
84
115
  logger.newline();
85
116
 
86
117
  // Clean state if force restart
@@ -111,19 +142,43 @@ export class LocalNodeManager {
111
142
  stdio: this.options.silent ? "ignore" : "pipe",
112
143
  });
113
144
 
114
- // Handle process output
145
+ // Subprocess output handling (see §9 Console UX in CLAUDE.md):
146
+ // - stdout chatter is hidden by default; gated by isVerbose()
147
+ // - lines matching CRITICAL_NODE_OUTPUT always surface as warnings
148
+ // so the user is never silenced through a real failure
149
+ // - stderr is always surfaced (real signal), modulo benign WARN
150
+ // lines that the movement binary emits during normal startup
115
151
  if (!this.options.silent && this.spawned.stdout && this.spawned.stderr) {
116
152
  this.spawned.stdout.on("data", (data: Buffer) => {
117
153
  const output = data.toString().trim();
118
- if (output) {
119
- console.log(`[Node] ${output}`);
154
+ if (!output) return;
155
+ if (CRITICAL_NODE_OUTPUT.test(output)) {
156
+ logger.warning(output);
157
+ return;
158
+ }
159
+ if (isVerbose()) {
160
+ for (const line of output.split("\n")) {
161
+ if (line) console.log(` ${colors.muted(symbols.pointer + " " + line)}`);
162
+ }
120
163
  }
121
164
  });
122
165
 
123
166
  this.spawned.stderr.on("data", (data: Buffer) => {
124
167
  const output = data.toString().trim();
125
- if (output && !output.includes("WARN")) {
126
- console.error(`[Node Error] ${output}`);
168
+ if (!output) return;
169
+ // Movement CLI uses stderr for both progress messages
170
+ // ("Applying post startup steps...", "Compiling...") and
171
+ // real errors. Stream channel alone isn't a reliable
172
+ // signal — gate routine lines behind verbosity, escalate
173
+ // anything matching CRITICAL_NODE_OUTPUT regardless.
174
+ if (CRITICAL_NODE_OUTPUT.test(output)) {
175
+ logger.error(output);
176
+ return;
177
+ }
178
+ if (isVerbose()) {
179
+ for (const line of output.split("\n")) {
180
+ if (line) console.error(` ${colors.muted(symbols.pointer + " " + line)}`);
181
+ }
127
182
  }
128
183
  });
129
184
  }
@@ -139,11 +194,13 @@ export class LocalNodeManager {
139
194
  this.spawned = null;
140
195
  });
141
196
 
142
- // Wait for node to be ready
143
- logger.step("Waiting for node to be ready...");
144
- await this.waitForReady(60000); // 60 second timeout
197
+ // Wait for node to be ready — wrapped in withTimedSpinner so the
198
+ // user sees live elapsed-time feedback while subprocess chatter is
199
+ // hidden in non-verbose mode.
200
+ await withTimedSpinner("Waiting for node to be ready", () =>
201
+ this.waitForReady(60000)
202
+ );
145
203
 
146
- logger.success("Local Movement node is ready!");
147
204
  logger.newline();
148
205
 
149
206
  this.starting = false;
@@ -281,7 +338,9 @@ export class LocalNodeManager {
281
338
  }
282
339
 
283
340
  const result = await response.json();
284
- logger.success(`Funded ${address} with ${amount} octas`, 2);
341
+ if (isVerbose()) {
342
+ logger.success(`Funded ${address} with ${amount} octas`, 2);
343
+ }
285
344
 
286
345
  return result;
287
346
  } catch (error) {
@@ -295,13 +354,15 @@ export class LocalNodeManager {
295
354
  */
296
355
  async fundAccounts(accounts: (Account | string)[], amount: number = 100_000_000): Promise<void> {
297
356
  logger.newline();
298
- logger.step(`Funding ${accounts.length} accounts from local faucet...`);
299
-
300
- for (const account of accounts) {
301
- await this.fundAccount(account, amount);
302
- }
303
-
304
- logger.success("All accounts funded successfully");
357
+ await withSpinner(
358
+ `Funding ${accounts.length} accounts from local faucet`,
359
+ async () => {
360
+ for (const account of accounts) {
361
+ await this.fundAccount(account, amount);
362
+ }
363
+ },
364
+ `Funded ${accounts.length} accounts (${(amount / 1e8).toFixed(0)} APT each)`,
365
+ );
305
366
  logger.newline();
306
367
  }
307
368
 
@@ -0,0 +1,62 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { LocalNodeManager } from "../LocalNodeManager.js";
7
+ import { logger } from "../../ui/index.js";
8
+
9
+ /**
10
+ * F9 — `apiPort` must not lie about where the node listens.
11
+ *
12
+ * `movement node run-localnet` (Movement CLI 7.4.0) does NOT accept a
13
+ * flag to change the REST API port. It always binds 8080. Earlier
14
+ * versions of LocalNodeManager accepted `apiPort: 9000` from the
15
+ * caller, stored it, and surfaced `http://127.0.0.1:9000` from
16
+ * `getNodeInfo()` — but the actual node was still on 8080. That
17
+ * mismatch would silently surface as "Movement command failed" with
18
+ * no useful signal. F9 closes the gap by refusing to lie: the
19
+ * effective port is 8080 regardless of what the caller passes, with a
20
+ * warning when they pass anything else.
21
+ */
22
+
23
+ describe("F9 — LocalNodeManager apiPort is constrained to 8080", () => {
24
+ let tmpDir: string;
25
+ let warnSpy: ReturnType<typeof vi.spyOn>;
26
+
27
+ beforeEach(() => {
28
+ tmpDir = mkdtempSync(join(tmpdir(), "movehat-f9-"));
29
+ warnSpy = vi.spyOn(logger, "warning").mockImplementation(() => undefined);
30
+ vi.spyOn(logger, "step").mockImplementation(() => undefined);
31
+ vi.spyOn(logger, "plain").mockImplementation(() => undefined);
32
+ vi.spyOn(logger, "newline").mockImplementation(() => undefined);
33
+ vi.spyOn(logger, "success").mockImplementation(() => undefined);
34
+ vi.spyOn(logger, "error").mockImplementation(() => undefined);
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ rmSync(tmpDir, { recursive: true, force: true });
40
+ });
41
+
42
+ it("ignores non-default apiPort, forces 8080, and warns", () => {
43
+ const mgr = new LocalNodeManager({ testDir: tmpDir, apiPort: 9000 });
44
+ expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
45
+ expect(warnSpy).toHaveBeenCalledTimes(1);
46
+ const msg = warnSpy.mock.calls[0]?.[0] as string;
47
+ expect(msg).toMatch(/8080/);
48
+ expect(msg).toMatch(/apiPort|REST API port/i);
49
+ });
50
+
51
+ it("accepts apiPort: 8080 without warning", () => {
52
+ const mgr = new LocalNodeManager({ testDir: tmpDir, apiPort: 8080 });
53
+ expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
54
+ expect(warnSpy).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it("accepts omitted apiPort without warning (default path)", () => {
58
+ const mgr = new LocalNodeManager({ testDir: tmpDir });
59
+ expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
60
+ expect(warnSpy).not.toHaveBeenCalled();
61
+ });
62
+ });