movehat 0.2.0 → 0.2.2

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 (184) hide show
  1. package/README.md +132 -279
  2. package/dist/__tests__/deployContract.test.js +56 -47
  3. package/dist/__tests__/deployContract.test.js.map +1 -1
  4. package/dist/__tests__/exports.test.d.ts +2 -0
  5. package/dist/__tests__/exports.test.d.ts.map +1 -0
  6. package/dist/__tests__/exports.test.js +30 -0
  7. package/dist/__tests__/exports.test.js.map +1 -0
  8. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
  9. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
  10. package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
  11. package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
  12. package/dist/__tests__/fork/api.test.js +7 -2
  13. package/dist/__tests__/fork/api.test.js.map +1 -1
  14. package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
  15. package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
  16. package/dist/__tests__/fork/api.timeout.test.js +98 -0
  17. package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
  18. package/dist/__tests__/harness/Harness.proxy.test.js +7 -11
  19. package/dist/__tests__/harness/Harness.proxy.test.js.map +1 -1
  20. package/dist/__tests__/harness/codeObject.deploy.test.js +1 -1
  21. package/dist/__tests__/harness/codeObject.deploy.test.js.map +1 -1
  22. package/dist/__tests__/harness/view.test.js +3 -3
  23. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
  24. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
  25. package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
  26. package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
  27. package/dist/commands/__tests__/init.test.js +73 -11
  28. package/dist/commands/__tests__/init.test.js.map +1 -1
  29. package/dist/commands/__tests__/run.test.js +3 -3
  30. package/dist/commands/__tests__/run.test.js.map +1 -1
  31. package/dist/commands/init.d.ts +22 -0
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +55 -6
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/core/AccountManager.d.ts +0 -3
  36. package/dist/core/AccountManager.d.ts.map +1 -1
  37. package/dist/core/AccountManager.js +14 -7
  38. package/dist/core/AccountManager.js.map +1 -1
  39. package/dist/core/Publisher.d.ts +0 -5
  40. package/dist/core/Publisher.d.ts.map +1 -1
  41. package/dist/core/Publisher.js +52 -76
  42. package/dist/core/Publisher.js.map +1 -1
  43. package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
  44. package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
  45. package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
  46. package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
  47. package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
  48. package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
  49. package/dist/core/__tests__/movementProfile.test.js +112 -0
  50. package/dist/core/__tests__/movementProfile.test.js.map +1 -0
  51. package/dist/core/config.js +6 -5
  52. package/dist/core/config.js.map +1 -1
  53. package/dist/core/contract.d.ts +0 -3
  54. package/dist/core/contract.d.ts.map +1 -1
  55. package/dist/core/contract.js +0 -3
  56. package/dist/core/contract.js.map +1 -1
  57. package/dist/core/deployments.d.ts +0 -6
  58. package/dist/core/deployments.d.ts.map +1 -1
  59. package/dist/core/deployments.js +0 -12
  60. package/dist/core/deployments.js.map +1 -1
  61. package/dist/core/movementProfile.d.ts +55 -22
  62. package/dist/core/movementProfile.d.ts.map +1 -1
  63. package/dist/core/movementProfile.js +77 -99
  64. package/dist/core/movementProfile.js.map +1 -1
  65. package/dist/fork/__tests__/manager.test.js +1 -1
  66. package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
  67. package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
  68. package/dist/fork/__tests__/server.cors.test.js +79 -0
  69. package/dist/fork/__tests__/server.cors.test.js.map +1 -0
  70. package/dist/fork/api.d.ts +9 -1
  71. package/dist/fork/api.d.ts.map +1 -1
  72. package/dist/fork/api.js +37 -7
  73. package/dist/fork/api.js.map +1 -1
  74. package/dist/fork/manager.d.ts +1 -21
  75. package/dist/fork/manager.d.ts.map +1 -1
  76. package/dist/fork/manager.js +1 -41
  77. package/dist/fork/manager.js.map +1 -1
  78. package/dist/fork/server.d.ts +20 -1
  79. package/dist/fork/server.d.ts.map +1 -1
  80. package/dist/fork/server.js +19 -9
  81. package/dist/fork/server.js.map +1 -1
  82. package/dist/fork/test.d.ts +0 -1
  83. package/dist/fork/test.d.ts.map +1 -1
  84. package/dist/fork/test.js.map +1 -1
  85. package/dist/harness/Harness.d.ts +11 -13
  86. package/dist/harness/Harness.d.ts.map +1 -1
  87. package/dist/harness/Harness.js +13 -13
  88. package/dist/harness/Harness.js.map +1 -1
  89. package/dist/harness/codeObject.d.ts.map +1 -1
  90. package/dist/harness/codeObject.js +31 -38
  91. package/dist/harness/codeObject.js.map +1 -1
  92. package/dist/harness/script.d.ts +3 -3
  93. package/dist/harness/script.d.ts.map +1 -1
  94. package/dist/harness/script.js +33 -29
  95. package/dist/harness/script.js.map +1 -1
  96. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
  97. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
  98. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
  99. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
  100. package/dist/helpers/setupLocalTesting.d.ts +1 -2
  101. package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
  102. package/dist/helpers/setupLocalTesting.js +28 -2
  103. package/dist/helpers/setupLocalTesting.js.map +1 -1
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +0 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/node/LocalNodeManager.d.ts +8 -0
  109. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  110. package/dist/node/LocalNodeManager.js +10 -1
  111. package/dist/node/LocalNodeManager.js.map +1 -1
  112. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
  113. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
  114. package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
  115. package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
  116. package/dist/node/__tests__/LocalNodeManager.test.js +4 -3
  117. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  118. package/dist/runtime.d.ts.map +1 -1
  119. package/dist/runtime.js +1 -3
  120. package/dist/runtime.js.map +1 -1
  121. package/dist/templates/move/Move.toml +1 -1
  122. package/dist/templates/move/sources/Counter.move +31 -4
  123. package/dist/templates/scripts/deploy-counter.ts +11 -1
  124. package/dist/templates/tests/Counter.test.ts +2 -2
  125. package/dist/types/config.d.ts +8 -1
  126. package/dist/types/config.d.ts.map +1 -1
  127. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
  128. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
  129. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
  130. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
  131. package/dist/utils/address.d.ts +0 -4
  132. package/dist/utils/address.d.ts.map +1 -1
  133. package/dist/utils/address.js +0 -4
  134. package/dist/utils/address.js.map +1 -1
  135. package/dist/utils/childProcessAdapter.d.ts +7 -0
  136. package/dist/utils/childProcessAdapter.d.ts.map +1 -1
  137. package/dist/utils/childProcessAdapter.js +23 -6
  138. package/dist/utils/childProcessAdapter.js.map +1 -1
  139. package/package.json +2 -1
  140. package/src/__tests__/deployContract.test.ts +59 -50
  141. package/src/__tests__/exports.test.ts +32 -0
  142. package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
  143. package/src/__tests__/fork/api.test.ts +7 -2
  144. package/src/__tests__/fork/api.timeout.test.ts +150 -0
  145. package/src/__tests__/harness/Harness.proxy.test.ts +7 -11
  146. package/src/__tests__/harness/codeObject.deploy.test.ts +1 -1
  147. package/src/__tests__/harness/view.test.ts +3 -3
  148. package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
  149. package/src/commands/__tests__/init.test.ts +96 -11
  150. package/src/commands/__tests__/run.test.ts +3 -3
  151. package/src/commands/init.ts +77 -6
  152. package/src/core/AccountManager.ts +18 -13
  153. package/src/core/Publisher.ts +58 -85
  154. package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
  155. package/src/core/__tests__/movementProfile.test.ts +131 -0
  156. package/src/core/config.ts +9 -5
  157. package/src/core/contract.ts +0 -3
  158. package/src/core/deployments.ts +0 -12
  159. package/src/core/movementProfile.ts +75 -127
  160. package/src/fork/__tests__/manager.test.ts +1 -1
  161. package/src/fork/__tests__/server.cors.test.ts +101 -0
  162. package/src/fork/api.ts +69 -10
  163. package/src/fork/manager.ts +1 -41
  164. package/src/fork/server.ts +38 -9
  165. package/src/fork/test.ts +0 -1
  166. package/src/harness/Harness.ts +16 -13
  167. package/src/harness/codeObject.ts +38 -48
  168. package/src/harness/script.ts +40 -39
  169. package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
  170. package/src/helpers/setupLocalTesting.ts +37 -4
  171. package/src/index.ts +9 -2
  172. package/src/node/LocalNodeManager.ts +24 -2
  173. package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
  174. package/src/node/__tests__/LocalNodeManager.test.ts +5 -4
  175. package/src/runtime.ts +1 -3
  176. package/src/templates/move/Move.toml +1 -1
  177. package/src/templates/move/sources/Counter.move +31 -4
  178. package/src/templates/scripts/deploy-counter.ts +11 -1
  179. package/src/templates/tests/Counter.test.ts +2 -2
  180. package/src/types/config.ts +8 -1
  181. package/src/types/runtime.ts +2 -2
  182. package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
  183. package/src/utils/address.ts +0 -4
  184. package/src/utils/childProcessAdapter.ts +35 -6
@@ -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
  *
@@ -20,8 +39,7 @@ import type { LocalTestOptions } from "../types/config.js";
20
39
  * @public The `runtime` and `teardown` fields are the supported surface.
21
40
  * `localNode`, `forkServer`, and `forkManager` are exposed for
22
41
  * escape hatches (e.g. mid-test `forkManager.resetState()`) but
23
- * their concrete shapes are considered `@internal` until the M5
24
- * TypeDoc pass formalizes the public API.
42
+ * their concrete shapes are `@internal`.
25
43
  */
26
44
  export interface LocalTestingContext {
27
45
  runtime: MovehatRuntime;
@@ -265,8 +283,8 @@ async function setupWithFork(
265
283
 
266
284
  if (!forkExists) {
267
285
  logger.step(`Creating fork from ${forkNetwork}...`);
268
- const testnetRpc = "https://testnet.movementnetwork.xyz/v1";
269
- await forkManager.initialize(testnetRpc, forkNetwork, options.forkApiKey);
286
+ const rpcUrl = resolveForkRpcUrl(forkNetwork, options.forkRpcUrl);
287
+ await forkManager.initialize(rpcUrl, forkNetwork, options.forkApiKey);
270
288
  logger.success(`Fork created at ${forkPath}`);
271
289
  logger.newline();
272
290
  } else {
@@ -279,6 +297,21 @@ async function setupWithFork(
279
297
  }
280
298
  forkManager.load();
281
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
+
282
315
  if (forkResetState) {
283
316
  logger.step("Resetting fork state...");
284
317
  await forkManager.resetState();
package/src/index.ts CHANGED
@@ -18,6 +18,13 @@ export type { ForkMetadata, AccountState, LedgerInfo, AccountData, AccountResour
18
18
  // Export custom errors
19
19
  export { ModuleAlreadyDeployedError, PostPublishError } from "./errors.js";
20
20
 
21
- // Export Harness (Hardhat-style API — primary public surface from M2 onward)
22
21
  export { Harness, HarnessDisposedError } from "./harness/index.js";
23
- 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";
@@ -12,7 +12,15 @@ export interface LocalNodeOptions {
12
12
  testDir?: string; // Directory for node data (default: .movehat/local-node)
13
13
  forceRestart?: boolean; // Clean state and start fresh
14
14
  faucetPort?: number; // Faucet port (default: 8081)
15
- apiPort?: number; // API/RPC port (default: 8080)
15
+ /**
16
+ * REST API port. Movement CLI (`movement node run-localnet`) does
17
+ * not accept a flag to change this — the node always binds 8080.
18
+ * Passing any other value triggers a warning at construction time
19
+ * and is replaced with 8080. Field is kept for source compatibility.
20
+ *
21
+ * @deprecated Movement CLI does not honor this. Omit it.
22
+ */
23
+ apiPort?: number;
16
24
  readyPort?: number; // Ready server port (default: 8070)
17
25
  silent?: boolean; // Suppress node output
18
26
  /**
@@ -22,6 +30,8 @@ export interface LocalNodeOptions {
22
30
  adapter?: ChildProcessAdapter;
23
31
  }
24
32
 
33
+ const MOVEMENT_API_PORT = 8080;
34
+
25
35
  export interface LocalNodeInfo {
26
36
  rpcUrl: string;
27
37
  faucetUrl: string;
@@ -47,11 +57,23 @@ export class LocalNodeManager {
47
57
 
48
58
  constructor(options: LocalNodeOptions = {}) {
49
59
  this.adapter = options.adapter ?? defaultChildProcessAdapter;
60
+ if (
61
+ options.apiPort !== undefined &&
62
+ options.apiPort !== MOVEMENT_API_PORT
63
+ ) {
64
+ // Movement CLI hardcodes the REST API port to 8080. Surfacing
65
+ // the requested port via getNodeInfo() would lie about where
66
+ // the node actually listens.
67
+ logger.warning(
68
+ `LocalNodeManager: apiPort=${options.apiPort} is not supported by ` +
69
+ `movement node run-localnet; forcing REST API port to 8080.`
70
+ );
71
+ }
50
72
  this.options = {
51
73
  testDir: options.testDir || join(process.cwd(), ".movehat", "local-node"),
52
74
  forceRestart: options.forceRestart ?? false,
53
75
  faucetPort: options.faucetPort || 8081,
54
- apiPort: options.apiPort || 8080,
76
+ apiPort: MOVEMENT_API_PORT,
55
77
  readyPort: options.readyPort || 8070,
56
78
  silent: options.silent ?? false,
57
79
  };
@@ -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
+ });
@@ -13,7 +13,7 @@ import type {
13
13
  } from "../../utils/childProcessAdapter.js";
14
14
 
15
15
  /**
16
- * Tests for `LocalNodeManager` (M3.3).
16
+ * Tests for `LocalNodeManager`.
17
17
  *
18
18
  * Strategy:
19
19
  * - Inject a fake `ChildProcessAdapter` so we control the spawn
@@ -126,17 +126,18 @@ describe("LocalNodeManager — start / stop / lifecycle", () => {
126
126
  expect(info.testDir).toBe(tmpDir);
127
127
  });
128
128
 
129
- it("honors custom ports from constructor options", () => {
129
+ it("honors custom faucet/ready ports; apiPort is pinned to 8080 (see F9)", () => {
130
130
  const { adapter } = buildFakeAdapter();
131
131
  const mgr = new LocalNodeManager({
132
132
  adapter,
133
133
  testDir: tmpDir,
134
- apiPort: 9000,
135
134
  faucetPort: 9001,
136
135
  readyPort: 9002,
137
136
  });
138
137
  const info = mgr.getNodeInfo();
139
- expect(info.rpcUrl).toContain(":9000");
138
+ // Movement CLI does not accept a flag for the REST API port; see
139
+ // LocalNodeManager.api-port.test.ts for the F9 contract.
140
+ expect(info.rpcUrl).toBe("http://127.0.0.1:8080");
140
141
  expect(info.faucetUrl).toContain(":9001");
141
142
  expect(info.readyUrl).toContain(":9002");
142
143
  });
package/src/runtime.ts CHANGED
@@ -93,9 +93,7 @@ export async function initRuntime(
93
93
  adapter?: ChildProcessAdapter;
94
94
  }
95
95
  ): Promise<DeploymentInfo> => {
96
- // Thin orchestrator over Publisher (M1.4 / #79). The 250-line closure
97
- // body lives in core/Publisher.ts and carries the bug fixes for
98
- // #36 / #37 / #38.
96
+ // Thin orchestrator; the actual logic lives in core/Publisher.ts.
99
97
  return new Publisher({ adapter: options?.adapter }).deploy({
100
98
  moduleName,
101
99
  config,
@@ -1,5 +1,5 @@
1
1
  [package]
2
- name = "{{projectName}}"
2
+ name = "{{movePackageName}}"
3
3
  version = "1.0.0"
4
4
  authors = []
5
5
 
@@ -33,7 +33,16 @@ module counter::counter {
33
33
 
34
34
  public entry fun increment(account: &signer) acquires Counter {
35
35
  let account_addr = signer::address_of(account);
36
- assert!(exists<Counter>(account_addr), E_NOT_INITIALIZED);
36
+
37
+ // Auto-init: create Counter if it doesn't exist yet. Defense in
38
+ // depth so the module stays usable even if a caller skips the
39
+ // dedicated `init` entry function.
40
+ if (!exists<Counter>(account_addr)) {
41
+ move_to(account, Counter {
42
+ value: 0,
43
+ increment_events: account::new_event_handle<IncrementEvent>(account),
44
+ });
45
+ };
37
46
 
38
47
  let counter = borrow_global_mut<Counter>(account_addr);
39
48
  let old_value = counter.value;
@@ -56,14 +65,32 @@ module counter::counter {
56
65
  public fun test_increment(account: &signer) acquires Counter {
57
66
  let addr = signer::address_of(account);
58
67
  aptos_framework::account::create_account_for_test(addr);
59
-
68
+
60
69
  init(account);
61
70
  assert!(get(addr) == 0, 0);
62
-
71
+
63
72
  increment(account);
64
73
  assert!(get(addr) == 1, 1);
65
-
74
+
66
75
  increment(account);
67
76
  assert!(get(addr) == 2, 2);
68
77
  }
78
+
79
+ // Regression guard: increment must auto-create the Counter resource
80
+ // when called against a never-initialized account. Locks the
81
+ // defense-in-depth behavior so a future refactor can't accidentally
82
+ // remove it.
83
+ #[test(account = @0x2)]
84
+ public fun test_increment_auto_inits(account: &signer) acquires Counter {
85
+ let addr = signer::address_of(account);
86
+ aptos_framework::account::create_account_for_test(addr);
87
+
88
+ // Skip init entirely — increment must create the resource.
89
+ increment(account);
90
+ assert!(get(addr) == 1, 0);
91
+
92
+ // Idempotent: a second increment uses the now-existing resource.
93
+ increment(account);
94
+ assert!(get(addr) == 2, 1);
95
+ }
69
96
  }
@@ -3,7 +3,7 @@ import { Harness } from "movehat";
3
3
  async function main() {
4
4
  console.log("🚀 Deploying Counter contract...\n");
5
5
 
6
- // Hardhat-style Harness — primary public API since M2.
6
+ // Hardhat-style Harness — the primary public API.
7
7
  // createLive binds to a real running network (testnet / mainnet / a
8
8
  // custom one defined in movehat.config.ts). No local process spawned.
9
9
  const network = process.env.MOVEHAT_NETWORK ?? "testnet";
@@ -31,6 +31,16 @@ async function main() {
31
31
  // Interact with the freshly deployed module via the runtime helper.
32
32
  const counter = harness.runtime.getContract(deployment.address, "counter");
33
33
 
34
+ // Counter is a Move resource — it must be created explicitly per
35
+ // account before any method that reads or mutates it. The dedicated
36
+ // `init` entry function does this once per signer. (The module also
37
+ // auto-inits inside `increment` as defense in depth, so this call is
38
+ // technically optional today, but kept for pedagogy: real-world Move
39
+ // modules usually require an explicit init step.)
40
+ console.log("\n🔧 Initializing counter resource for this account...");
41
+ const initTx = await counter.call(harness.runtime.account, "init", []);
42
+ console.log(` Init tx: ${initTx.hash}`);
43
+
34
44
  console.log("\n📝 Incrementing counter...");
35
45
  const txResult = await counter.call(harness.runtime.account, "increment", []);
36
46
  console.log(`✅ Transaction hash: ${txResult.hash}`);
@@ -11,7 +11,7 @@ describe("Counter Contract", () => {
11
11
  before(async function () {
12
12
  this.timeout(60000); // Allow time for local node startup + deployment
13
13
 
14
- // Hardhat-style Harness — primary public API since M2.
14
+ // Hardhat-style Harness — the primary public API.
15
15
  // createLocal spins up a Movement local node, funds the labeled
16
16
  // accounts from the local faucet, and (via autoDeploy) builds +
17
17
  // publishes the named modules so they're ready to use.
@@ -43,7 +43,7 @@ describe("Counter Contract", () => {
43
43
  const tx = await counter.call(deployer, "init", []);
44
44
  console.log(` Init transaction: ${tx.hash}`);
45
45
 
46
- // Read counter value via harness.runViewFunction (new in M2)
46
+ // Read counter value via harness.runViewFunction
47
47
  const [value] = await harness.runViewFunction({
48
48
  function: `${counter.moduleAddress}::counter::get`,
49
49
  functionArguments: [deployer.accountAddress.toString()],
@@ -55,7 +55,14 @@ export interface LocalTestOptions {
55
55
  nodeSilent?: boolean; // Suppress node output (default: false)
56
56
 
57
57
  // Fork options (when mode='fork')
58
- forkNetwork?: 'testnet' | string; // Network to fork from (default: 'testnet')
58
+ forkNetwork?: 'testnet' | 'mainnet' | string; // Network to fork from (default: 'testnet')
59
+ /**
60
+ * RPC URL override used when forking a non-built-in network.
61
+ * Required when `forkNetwork` is not one of the built-in names
62
+ * (`'testnet'`, `'mainnet'`). Ignored when a fork already exists
63
+ * on disk (the saved metadata's nodeUrl is reused).
64
+ */
65
+ forkRpcUrl?: string;
59
66
  forkName?: string; // Name for the fork (default: 'test-local')
60
67
  forkPort?: number; // Fork server port (default: 8080)
61
68
  forkResetState?: boolean; // Clear fork state before tests (default: true)
@@ -51,7 +51,7 @@ export interface MovehatRuntime {
51
51
  getAccountByIndex: (index: number) => Account;
52
52
 
53
53
  // Network switching — returns a new runtime bound to the requested
54
- // network. As of M1.5 there is no module-cached runtime, so callers
55
- // must capture and use the returned instance.
54
+ // network. There is no module-cached runtime, so callers must capture
55
+ // and use the returned instance.
56
56
  switchNetwork: (networkName: string) => Promise<MovehatRuntime>;
57
57
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defaultChildProcessAdapter } from "../childProcessAdapter.js";
3
+
4
+ /**
5
+ * F4 — `run()` must reject when child output exceeds `maxBuffer`.
6
+ *
7
+ * Without this cap, the stdout/stderr Buffer arrays in
8
+ * DefaultChildProcessAdapter grow without limit. A buggy or hostile
9
+ * subprocess can OOM the parent process. F4 adds an opt-in byte cap
10
+ * with kill-on-overflow semantics.
11
+ */
12
+
13
+ const NODE = process.execPath;
14
+
15
+ describe("F4 — ChildProcessAdapter.run maxBuffer", () => {
16
+ it("rejects with a maxBuffer error when stdout exceeds the cap", async () => {
17
+ // 8KiB of output, cap at 1KiB → must abort.
18
+ const script = `process.stdout.write('x'.repeat(8 * 1024)); setTimeout(() => {}, 30000);`;
19
+ await expect(
20
+ defaultChildProcessAdapter.run({
21
+ command: NODE,
22
+ args: ["-e", script],
23
+ maxBuffer: 1024,
24
+ timeoutMs: 10_000,
25
+ })
26
+ ).rejects.toThrow(/maxBuffer|exceeded/i);
27
+ });
28
+
29
+ it("rejects with a maxBuffer error when stderr exceeds the cap", async () => {
30
+ const script = `process.stderr.write('y'.repeat(8 * 1024)); setTimeout(() => {}, 30000);`;
31
+ await expect(
32
+ defaultChildProcessAdapter.run({
33
+ command: NODE,
34
+ args: ["-e", script],
35
+ maxBuffer: 1024,
36
+ timeoutMs: 10_000,
37
+ })
38
+ ).rejects.toThrow(/maxBuffer|exceeded/i);
39
+ });
40
+
41
+ it("does NOT throw when output stays under the cap", async () => {
42
+ const script = `process.stdout.write('ok'); process.exit(0);`;
43
+ const result = await defaultChildProcessAdapter.run({
44
+ command: NODE,
45
+ args: ["-e", script],
46
+ maxBuffer: 1024,
47
+ });
48
+ expect(result.exitCode).toBe(0);
49
+ expect(result.stdout).toBe("ok");
50
+ });
51
+ });
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * Address normalization helpers shared across the fork code.
3
3
  *
4
- * Two normalization variants live in production today and are preserved here
5
- * with identical semantics so the in-place migration in M1.2 is behavior-
6
- * preserving:
7
- *
8
4
  * - `normalizeAddress` → lowercase + `0x` + left-pad to 64 hex chars.
9
5
  * Used by `fork/manager.ts` for storage keys.
10
6
  * - `normalizeAddressShort` → lowercase + `0x`, no padding.
@@ -38,8 +38,17 @@ export interface RunInput {
38
38
  * Default: `false`.
39
39
  */
40
40
  inheritStdio?: boolean;
41
+ /**
42
+ * Maximum combined bytes (stdout + stderr) the captured Buffers may
43
+ * grow to before the child is killed and the promise rejects. Defaults
44
+ * to 64 MiB. Set to `Infinity` to disable. Ignored when
45
+ * `inheritStdio` is `true` (no buffering happens).
46
+ */
47
+ maxBuffer?: number;
41
48
  }
42
49
 
50
+ const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
51
+
43
52
  export interface RunResult {
44
53
  /**
45
54
  * Numeric exit code from the child. `-1` when the child was terminated by
@@ -99,10 +108,9 @@ class DefaultChildProcessAdapter implements ChildProcessAdapter {
99
108
  run(input: RunInput): Promise<RunResult> {
100
109
  // Skip the default 5-minute timeout when the caller wires stdio
101
110
  // through to the terminal — interactive sessions (mocha, tsx scripts,
102
- // `movement move test`, `pnpm install`) routinely exceed 5 minutes and
103
- // SIGTERM'ing them silently is a regression vs the direct-spawn code
104
- // these callers used before M1.3a. An explicitly-passed `timeoutMs`
105
- // is always honored, regardless of `inheritStdio`.
111
+ // `movement move test`, `pnpm install`) routinely exceed 5 minutes
112
+ // and SIGTERM'ing them silently would be a regression. An explicit
113
+ // `timeoutMs` is always honored, regardless of `inheritStdio`.
106
114
  const timeoutMs =
107
115
  input.timeoutMs ?? (input.inheritStdio ? undefined : DEFAULT_TIMEOUT_MS);
108
116
 
@@ -115,10 +123,31 @@ class DefaultChildProcessAdapter implements ChildProcessAdapter {
115
123
 
116
124
  const stdoutChunks: Buffer[] = [];
117
125
  const stderrChunks: Buffer[] = [];
126
+ let totalBytes = 0;
127
+ let overflowed = false;
128
+ const maxBuffer = input.maxBuffer ?? DEFAULT_MAX_BUFFER;
129
+
130
+ const onChunk = (chunks: Buffer[]) => (chunk: Buffer) => {
131
+ if (overflowed) return;
132
+ totalBytes += chunk.length;
133
+ if (totalBytes > maxBuffer) {
134
+ overflowed = true;
135
+ clearTimer();
136
+ input.signal?.removeEventListener('abort', onAbort);
137
+ child.kill('SIGTERM');
138
+ reject(
139
+ new Error(
140
+ `Command output exceeded maxBuffer (${maxBuffer} bytes): ${input.command}`
141
+ )
142
+ );
143
+ return;
144
+ }
145
+ chunks.push(chunk);
146
+ };
118
147
 
119
148
  // Streams are null when stdio is 'inherit'; the `?.` covers that.
120
- child.stdout?.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
121
- child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
149
+ child.stdout?.on('data', onChunk(stdoutChunks));
150
+ child.stderr?.on('data', onChunk(stderrChunks));
122
151
 
123
152
  let timeoutHandle: NodeJS.Timeout | undefined;
124
153
  const clearTimer = () => {