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.
- package/README.md +132 -279
- package/dist/__tests__/deployContract.test.js +56 -47
- package/dist/__tests__/deployContract.test.js.map +1 -1
- package/dist/__tests__/exports.test.d.ts +2 -0
- package/dist/__tests__/exports.test.d.ts.map +1 -0
- package/dist/__tests__/exports.test.js +30 -0
- package/dist/__tests__/exports.test.js.map +1 -0
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
- package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
- package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
- package/dist/__tests__/fork/api.test.js +7 -2
- package/dist/__tests__/fork/api.test.js.map +1 -1
- package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
- package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
- package/dist/__tests__/fork/api.timeout.test.js +98 -0
- package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
- package/dist/__tests__/harness/Harness.proxy.test.js +7 -11
- package/dist/__tests__/harness/Harness.proxy.test.js.map +1 -1
- package/dist/__tests__/harness/codeObject.deploy.test.js +1 -1
- package/dist/__tests__/harness/codeObject.deploy.test.js.map +1 -1
- package/dist/__tests__/harness/view.test.js +3 -3
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.js +73 -11
- package/dist/commands/__tests__/init.test.js.map +1 -1
- package/dist/commands/__tests__/run.test.js +3 -3
- package/dist/commands/__tests__/run.test.js.map +1 -1
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +55 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/core/AccountManager.d.ts +0 -3
- package/dist/core/AccountManager.d.ts.map +1 -1
- package/dist/core/AccountManager.js +14 -7
- package/dist/core/AccountManager.js.map +1 -1
- package/dist/core/Publisher.d.ts +0 -5
- package/dist/core/Publisher.d.ts.map +1 -1
- package/dist/core/Publisher.js +52 -76
- package/dist/core/Publisher.js.map +1 -1
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
- package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
- package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
- package/dist/core/__tests__/movementProfile.test.js +112 -0
- package/dist/core/__tests__/movementProfile.test.js.map +1 -0
- package/dist/core/config.js +6 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/contract.d.ts +0 -3
- package/dist/core/contract.d.ts.map +1 -1
- package/dist/core/contract.js +0 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/core/deployments.d.ts +0 -6
- package/dist/core/deployments.d.ts.map +1 -1
- package/dist/core/deployments.js +0 -12
- package/dist/core/deployments.js.map +1 -1
- package/dist/core/movementProfile.d.ts +55 -22
- package/dist/core/movementProfile.d.ts.map +1 -1
- package/dist/core/movementProfile.js +77 -99
- package/dist/core/movementProfile.js.map +1 -1
- package/dist/fork/__tests__/manager.test.js +1 -1
- package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
- package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
- package/dist/fork/__tests__/server.cors.test.js +79 -0
- package/dist/fork/__tests__/server.cors.test.js.map +1 -0
- package/dist/fork/api.d.ts +9 -1
- package/dist/fork/api.d.ts.map +1 -1
- package/dist/fork/api.js +37 -7
- package/dist/fork/api.js.map +1 -1
- package/dist/fork/manager.d.ts +1 -21
- package/dist/fork/manager.d.ts.map +1 -1
- package/dist/fork/manager.js +1 -41
- package/dist/fork/manager.js.map +1 -1
- package/dist/fork/server.d.ts +20 -1
- package/dist/fork/server.d.ts.map +1 -1
- package/dist/fork/server.js +19 -9
- package/dist/fork/server.js.map +1 -1
- package/dist/fork/test.d.ts +0 -1
- package/dist/fork/test.d.ts.map +1 -1
- package/dist/fork/test.js.map +1 -1
- package/dist/harness/Harness.d.ts +11 -13
- package/dist/harness/Harness.d.ts.map +1 -1
- package/dist/harness/Harness.js +13 -13
- package/dist/harness/Harness.js.map +1 -1
- package/dist/harness/codeObject.d.ts.map +1 -1
- package/dist/harness/codeObject.js +31 -38
- package/dist/harness/codeObject.js.map +1 -1
- package/dist/harness/script.d.ts +3 -3
- package/dist/harness/script.d.ts.map +1 -1
- package/dist/harness/script.js +33 -29
- package/dist/harness/script.js.map +1 -1
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
- package/dist/helpers/setupLocalTesting.d.ts +1 -2
- package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
- package/dist/helpers/setupLocalTesting.js +28 -2
- package/dist/helpers/setupLocalTesting.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/node/LocalNodeManager.d.ts +8 -0
- package/dist/node/LocalNodeManager.d.ts.map +1 -1
- package/dist/node/LocalNodeManager.js +10 -1
- package/dist/node/LocalNodeManager.js.map +1 -1
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.test.js +4 -3
- package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -3
- package/dist/runtime.js.map +1 -1
- package/dist/templates/move/Move.toml +1 -1
- package/dist/templates/move/sources/Counter.move +31 -4
- package/dist/templates/scripts/deploy-counter.ts +11 -1
- package/dist/templates/tests/Counter.test.ts +2 -2
- package/dist/types/config.d.ts +8 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
- package/dist/utils/address.d.ts +0 -4
- package/dist/utils/address.d.ts.map +1 -1
- package/dist/utils/address.js +0 -4
- package/dist/utils/address.js.map +1 -1
- package/dist/utils/childProcessAdapter.d.ts +7 -0
- package/dist/utils/childProcessAdapter.d.ts.map +1 -1
- package/dist/utils/childProcessAdapter.js +23 -6
- package/dist/utils/childProcessAdapter.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/deployContract.test.ts +59 -50
- package/src/__tests__/exports.test.ts +32 -0
- package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
- package/src/__tests__/fork/api.test.ts +7 -2
- package/src/__tests__/fork/api.timeout.test.ts +150 -0
- package/src/__tests__/harness/Harness.proxy.test.ts +7 -11
- package/src/__tests__/harness/codeObject.deploy.test.ts +1 -1
- package/src/__tests__/harness/view.test.ts +3 -3
- package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
- package/src/commands/__tests__/init.test.ts +96 -11
- package/src/commands/__tests__/run.test.ts +3 -3
- package/src/commands/init.ts +77 -6
- package/src/core/AccountManager.ts +18 -13
- package/src/core/Publisher.ts +58 -85
- package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
- package/src/core/__tests__/movementProfile.test.ts +131 -0
- package/src/core/config.ts +9 -5
- package/src/core/contract.ts +0 -3
- package/src/core/deployments.ts +0 -12
- package/src/core/movementProfile.ts +75 -127
- package/src/fork/__tests__/manager.test.ts +1 -1
- package/src/fork/__tests__/server.cors.test.ts +101 -0
- package/src/fork/api.ts +69 -10
- package/src/fork/manager.ts +1 -41
- package/src/fork/server.ts +38 -9
- package/src/fork/test.ts +0 -1
- package/src/harness/Harness.ts +16 -13
- package/src/harness/codeObject.ts +38 -48
- package/src/harness/script.ts +40 -39
- package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
- package/src/helpers/setupLocalTesting.ts +37 -4
- package/src/index.ts +9 -2
- package/src/node/LocalNodeManager.ts +24 -2
- package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
- package/src/node/__tests__/LocalNodeManager.test.ts +5 -4
- package/src/runtime.ts +1 -3
- package/src/templates/move/Move.toml +1 -1
- package/src/templates/move/sources/Counter.move +31 -4
- package/src/templates/scripts/deploy-counter.ts +11 -1
- package/src/templates/tests/Counter.test.ts +2 -2
- package/src/types/config.ts +8 -1
- package/src/types/runtime.ts +2 -2
- package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
- package/src/utils/address.ts +0 -4
- 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
|
|
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
|
|
269
|
-
await forkManager.initialize(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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()],
|
package/src/types/config.ts
CHANGED
|
@@ -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)
|
package/src/types/runtime.ts
CHANGED
|
@@ -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.
|
|
55
|
-
//
|
|
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
|
+
});
|
package/src/utils/address.ts
CHANGED
|
@@ -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
|
|
103
|
-
// SIGTERM'ing them silently
|
|
104
|
-
//
|
|
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', (
|
|
121
|
-
child.stderr?.on('data', (
|
|
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 = () => {
|