movehat 0.2.6 → 0.2.8

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 (39) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands/fork/fund.js +3 -1
  3. package/dist/commands/fork/serve.js +10 -5
  4. package/dist/commands/test-move.js +6 -3
  5. package/dist/core/AccountManager.d.ts +121 -139
  6. package/dist/core/AccountManager.js +217 -158
  7. package/dist/fork/api.d.ts +1 -1
  8. package/dist/fork/api.js +9 -4
  9. package/dist/fork/manager.d.ts +2 -2
  10. package/dist/fork/manager.js +4 -8
  11. package/dist/fork/storage.js +5 -4
  12. package/dist/fork/test.d.ts +1 -1
  13. package/dist/fork/validation.d.ts +9 -0
  14. package/dist/fork/validation.js +88 -0
  15. package/dist/harness/Harness.d.ts +35 -6
  16. package/dist/harness/Harness.js +36 -5
  17. package/dist/helpers/index.d.ts +1 -0
  18. package/dist/helpers/index.js +1 -0
  19. package/dist/helpers/move-tests.js +8 -5
  20. package/dist/helpers/setup.js +6 -3
  21. package/dist/helpers/setupLocalTesting.d.ts +2 -2
  22. package/dist/helpers/setupLocalTesting.js +55 -24
  23. package/dist/helpers/testFixtures.d.ts +5 -4
  24. package/dist/helpers/testFixtures.js +4 -5
  25. package/dist/helpers/version-check.js +4 -2
  26. package/dist/index.d.ts +3 -1
  27. package/dist/node/MoveliteManager.d.ts +18 -0
  28. package/dist/node/MoveliteManager.js +152 -0
  29. package/dist/node/NodeProvider.d.ts +9 -0
  30. package/dist/node/NodeProvider.js +1 -0
  31. package/dist/runtime.d.ts +13 -0
  32. package/dist/runtime.js +8 -4
  33. package/dist/templates/.mocharc.json +1 -0
  34. package/dist/templates/tests/Counter.test.ts +12 -14
  35. package/dist/templates/tests/setup.ts +34 -0
  36. package/dist/types/config.d.ts +2 -0
  37. package/dist/types/fork.d.ts +24 -0
  38. package/dist/types/runtime.d.ts +2 -0
  39. package/package.json +4 -1
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { isHexAddress } from '../utils/address.js';
4
+ import { assertForkMetadata, assertAccountStateRecord } from './validation.js';
4
5
  /**
5
6
  * Sanitize address to create a safe filename. Validates the address through
6
7
  * the shared `isHexAddress` helper (length 1–64 hex chars, optional `0x`),
@@ -108,7 +109,7 @@ export class ForkStorage {
108
109
  if (!existsSync(metadataPath)) {
109
110
  throw new Error(`Fork metadata not found at ${metadataPath}`);
110
111
  }
111
- return readJsonFile(metadataPath, 'fork metadata');
112
+ return assertForkMetadata(readJsonFile(metadataPath, 'fork metadata'));
112
113
  }
113
114
  /**
114
115
  * Get account state
@@ -118,7 +119,7 @@ export class ForkStorage {
118
119
  if (!existsSync(accountsPath)) {
119
120
  return null;
120
121
  }
121
- const accounts = readJsonFile(accountsPath, 'fork accounts');
122
+ const accounts = assertAccountStateRecord(readJsonFile(accountsPath, 'fork accounts'));
122
123
  return accounts[address] || null;
123
124
  }
124
125
  /**
@@ -128,7 +129,7 @@ export class ForkStorage {
128
129
  const accountsPath = join(this.forkPath, 'accounts.json');
129
130
  let accounts = {};
130
131
  if (existsSync(accountsPath)) {
131
- accounts = readJsonFile(accountsPath, 'fork accounts');
132
+ accounts = assertAccountStateRecord(readJsonFile(accountsPath, 'fork accounts'));
132
133
  }
133
134
  accounts[address] = state;
134
135
  writePrivateFile(accountsPath, JSON.stringify(accounts, null, 2));
@@ -193,7 +194,7 @@ export class ForkStorage {
193
194
  if (!existsSync(accountsPath)) {
194
195
  return [];
195
196
  }
196
- const accounts = readJsonFile(accountsPath, 'fork accounts');
197
+ const accounts = assertAccountStateRecord(readJsonFile(accountsPath, 'fork accounts'));
197
198
  return Object.keys(accounts);
198
199
  }
199
200
  /**
@@ -59,7 +59,7 @@ export declare function getForkInfo(path: string): Promise<ForkInfo>;
59
59
  */
60
60
  export declare function viewForkResource(sessionPath: string, account: string, resourceType: string, options?: {
61
61
  adapter?: ChildProcessAdapter;
62
- }): Promise<any>;
62
+ }): Promise<unknown>;
63
63
  /**
64
64
  * Compare a resource between current network state and a fork
65
65
  * Useful for verifying state changes after tests
@@ -0,0 +1,9 @@
1
+ import type { LedgerInfo, AccountData, AccountResource, ForkMetadata, AccountState, CoinStore } from "../types/fork.js";
2
+ export declare function assertLedgerInfo(v: unknown): LedgerInfo;
3
+ export declare function assertAccountData(v: unknown): AccountData;
4
+ export declare function assertAccountResource(v: unknown): AccountResource;
5
+ export declare function assertAccountResourceArray(v: unknown): AccountResource[];
6
+ export declare function assertForkMetadata(v: unknown): ForkMetadata;
7
+ export declare function assertAccountState(v: unknown): AccountState;
8
+ export declare function assertAccountStateRecord(v: unknown): Record<string, AccountState>;
9
+ export declare function assertCoinStore(v: unknown): CoinStore;
@@ -0,0 +1,88 @@
1
+ function isObject(v) {
2
+ return v !== null && typeof v === "object" && !Array.isArray(v);
3
+ }
4
+ function hasString(o, key) {
5
+ return typeof o[key] === "string";
6
+ }
7
+ export function assertLedgerInfo(v) {
8
+ if (!isObject(v) ||
9
+ typeof v.chain_id !== "number" ||
10
+ !hasString(v, "epoch") ||
11
+ !hasString(v, "ledger_version") ||
12
+ !hasString(v, "oldest_ledger_version") ||
13
+ !hasString(v, "ledger_timestamp") ||
14
+ !hasString(v, "node_role") ||
15
+ !hasString(v, "oldest_block_height") ||
16
+ !hasString(v, "block_height")) {
17
+ throw new Error("Invalid LedgerInfo: missing or incorrectly typed fields");
18
+ }
19
+ return v;
20
+ }
21
+ export function assertAccountData(v) {
22
+ if (!isObject(v) ||
23
+ !hasString(v, "sequence_number") ||
24
+ !hasString(v, "authentication_key")) {
25
+ throw new Error("Invalid AccountData: expected object with 'sequence_number' and 'authentication_key' strings");
26
+ }
27
+ return v;
28
+ }
29
+ export function assertAccountResource(v) {
30
+ if (!isObject(v) || !hasString(v, "type") || !("data" in v)) {
31
+ throw new Error("Invalid AccountResource: expected object with 'type' string and 'data' field");
32
+ }
33
+ return v;
34
+ }
35
+ export function assertAccountResourceArray(v) {
36
+ if (!Array.isArray(v)) {
37
+ throw new Error("Invalid resources response: expected array");
38
+ }
39
+ for (let i = 0; i < v.length; i++) {
40
+ if (!isObject(v[i]) || !hasString(v[i], "type") || !("data" in v[i])) {
41
+ throw new Error(`Invalid AccountResource at index ${i}: expected object with 'type' and 'data'`);
42
+ }
43
+ }
44
+ return v;
45
+ }
46
+ export function assertForkMetadata(v) {
47
+ if (!isObject(v) ||
48
+ !hasString(v, "network") ||
49
+ !hasString(v, "nodeUrl") ||
50
+ typeof v.chainId !== "number" ||
51
+ !hasString(v, "ledgerVersion") ||
52
+ !hasString(v, "timestamp") ||
53
+ !hasString(v, "epoch") ||
54
+ !hasString(v, "blockHeight") ||
55
+ !hasString(v, "createdAt")) {
56
+ throw new Error("Invalid ForkMetadata: missing or incorrectly typed fields");
57
+ }
58
+ return v;
59
+ }
60
+ export function assertAccountState(v) {
61
+ if (!isObject(v) ||
62
+ !hasString(v, "sequenceNumber") ||
63
+ !hasString(v, "authenticationKey")) {
64
+ throw new Error("Invalid AccountState: expected object with 'sequenceNumber' and 'authenticationKey' strings");
65
+ }
66
+ return v;
67
+ }
68
+ export function assertAccountStateRecord(v) {
69
+ if (!isObject(v)) {
70
+ throw new Error("Invalid account state record: expected object");
71
+ }
72
+ for (const [key, val] of Object.entries(v)) {
73
+ if (!isObject(val) ||
74
+ !hasString(val, "sequenceNumber") ||
75
+ !hasString(val, "authenticationKey")) {
76
+ throw new Error(`Invalid AccountState for key "${key}": missing 'sequenceNumber' or 'authenticationKey'`);
77
+ }
78
+ }
79
+ return v;
80
+ }
81
+ export function assertCoinStore(v) {
82
+ if (!isObject(v) ||
83
+ !isObject(v.coin) ||
84
+ !hasString(v.coin, "value")) {
85
+ throw new Error("Invalid CoinStore: expected object with 'coin.value' string");
86
+ }
87
+ return v;
88
+ }
@@ -1,5 +1,6 @@
1
+ import type { Account } from "@aptos-labs/ts-sdk";
1
2
  import type { MovehatRuntime } from "../types/runtime.js";
2
- import type { LocalNodeManager } from "../node/LocalNodeManager.js";
3
+ import type { NodeProvider } from "../node/NodeProvider.js";
3
4
  import type { ForkServer } from "../fork/server.js";
4
5
  import type { ForkManager } from "../fork/manager.js";
5
6
  import type { LocalTestOptions } from "../types/config.js";
@@ -13,21 +14,49 @@ export type HarnessMode = "local" | "fork" | "live";
13
14
  * a Proxy that synchronously throws {@link HarnessDisposedError} on any
14
15
  * post-`cleanup()` call to one of the deployment / script / view methods.
15
16
  *
16
- * AccountManager note: the underlying account pool is a process-wide
17
- * static (see `core/AccountManager.ts`). Two Harness instances in the
18
- * same process share account labels; this is the same constraint that
19
- * already governs `setupTestFixture`.
17
+ * Account isolation: each Harness owns a per-instance `AccountManager`
18
+ * (see `core/AccountManager.ts`) reachable at `harness.runtime.accountManager`.
19
+ * Two Harness instances in the same process have independent account
20
+ * pools and label maps — `Harness.createLocal({ accountLabels: ["alice"] })`
21
+ * twice produces two DIFFERENT alice accounts.
22
+ *
23
+ * Labeled accounts created during construction (via `accountLabels` in
24
+ * `createLocal`) are snapshotted onto `harness.accounts` at construction
25
+ * time. Late additions via `harness.runtime.accountManager.createAccount(...)`
26
+ * are NOT reflected in the `accounts` Record — that's a snapshot, not a
27
+ * live view. For live mode (`Harness.createLive`), `accounts` is `{}`
28
+ * because the live factory does not create labeled accounts; reach into
29
+ * `harness.runtime.accountManager.*` for advanced operations.
30
+ *
31
+ * The `AccountManager` class-static methods remain available in 0.2.x
32
+ * for backward compatibility (deprecation warning lands in 0.2.7,
33
+ * removal in 0.3.0 — see #270). New code should prefer
34
+ * `harness.accounts.<label>` for the common read path.
20
35
  */
21
36
  export declare class Harness {
22
37
  readonly mode: HarnessMode;
23
38
  readonly runtime: MovehatRuntime;
39
+ /**
40
+ * Labeled accounts snapshotted from `runtime.accountManager` at
41
+ * construction time. For `createLocal`, this is populated from the
42
+ * `accountLabels` option. For `createFork`, populated the same way.
43
+ * For `createLive`, empty (live mode does not create labeled accounts;
44
+ * use `harness.runtime.accountManager.loadAccountFromPrivateKey(...)`
45
+ * for direct key loading).
46
+ *
47
+ * Typed `Readonly` so the snapshot can't be mutated via
48
+ * `harness.accounts.alice = X`. Reach into
49
+ * `harness.runtime.accountManager.*` when you need mutable access.
50
+ */
51
+ readonly accounts: Readonly<Record<string, Account>>;
24
52
  /** @internal */
25
- readonly localNode?: LocalNodeManager;
53
+ readonly localNode?: NodeProvider;
26
54
  /** @internal */
27
55
  readonly forkServer?: ForkServer;
28
56
  /** @internal */
29
57
  readonly forkManager?: ForkManager;
30
58
  private _poisoned;
59
+ private readonly ownsLocalNode;
31
60
  private constructor();
32
61
  /** True once `cleanup()` has been awaited at least once. */
33
62
  get poisoned(): boolean;
@@ -12,14 +12,41 @@ import { runMoveScript } from "./script.js";
12
12
  * a Proxy that synchronously throws {@link HarnessDisposedError} on any
13
13
  * post-`cleanup()` call to one of the deployment / script / view methods.
14
14
  *
15
- * AccountManager note: the underlying account pool is a process-wide
16
- * static (see `core/AccountManager.ts`). Two Harness instances in the
17
- * same process share account labels; this is the same constraint that
18
- * already governs `setupTestFixture`.
15
+ * Account isolation: each Harness owns a per-instance `AccountManager`
16
+ * (see `core/AccountManager.ts`) reachable at `harness.runtime.accountManager`.
17
+ * Two Harness instances in the same process have independent account
18
+ * pools and label maps — `Harness.createLocal({ accountLabels: ["alice"] })`
19
+ * twice produces two DIFFERENT alice accounts.
20
+ *
21
+ * Labeled accounts created during construction (via `accountLabels` in
22
+ * `createLocal`) are snapshotted onto `harness.accounts` at construction
23
+ * time. Late additions via `harness.runtime.accountManager.createAccount(...)`
24
+ * are NOT reflected in the `accounts` Record — that's a snapshot, not a
25
+ * live view. For live mode (`Harness.createLive`), `accounts` is `{}`
26
+ * because the live factory does not create labeled accounts; reach into
27
+ * `harness.runtime.accountManager.*` for advanced operations.
28
+ *
29
+ * The `AccountManager` class-static methods remain available in 0.2.x
30
+ * for backward compatibility (deprecation warning lands in 0.2.7,
31
+ * removal in 0.3.0 — see #270). New code should prefer
32
+ * `harness.accounts.<label>` for the common read path.
19
33
  */
20
34
  export class Harness {
21
35
  mode;
22
36
  runtime;
37
+ /**
38
+ * Labeled accounts snapshotted from `runtime.accountManager` at
39
+ * construction time. For `createLocal`, this is populated from the
40
+ * `accountLabels` option. For `createFork`, populated the same way.
41
+ * For `createLive`, empty (live mode does not create labeled accounts;
42
+ * use `harness.runtime.accountManager.loadAccountFromPrivateKey(...)`
43
+ * for direct key loading).
44
+ *
45
+ * Typed `Readonly` so the snapshot can't be mutated via
46
+ * `harness.accounts.alice = X`. Reach into
47
+ * `harness.runtime.accountManager.*` when you need mutable access.
48
+ */
49
+ accounts;
23
50
  /** @internal */
24
51
  localNode;
25
52
  /** @internal */
@@ -27,9 +54,12 @@ export class Harness {
27
54
  /** @internal */
28
55
  forkManager;
29
56
  _poisoned = false;
57
+ ownsLocalNode;
30
58
  constructor(init) {
31
59
  this.mode = init.mode;
32
60
  this.runtime = init.runtime;
61
+ this.accounts = init.runtime.accountManager.getLabeledAccounts();
62
+ this.ownsLocalNode = init.ownsLocalNode ?? true;
33
63
  if (init.localNode)
34
64
  this.localNode = init.localNode;
35
65
  if (init.forkServer)
@@ -53,6 +83,7 @@ export class Harness {
53
83
  const init = {
54
84
  mode: "local",
55
85
  runtime: ctx.runtime,
86
+ ownsLocalNode: !options.localNode,
56
87
  };
57
88
  if (ctx.localNode)
58
89
  init.localNode = ctx.localNode;
@@ -128,7 +159,7 @@ export class Harness {
128
159
  if (this._poisoned)
129
160
  return;
130
161
  this._poisoned = true;
131
- if (this.localNode) {
162
+ if (this.localNode && this.ownsLocalNode) {
132
163
  await this.localNode.stop().catch(() => { });
133
164
  }
134
165
  if (this.forkServer) {
@@ -11,6 +11,7 @@ export { AccountManager } from "../core/AccountManager.js";
11
11
  export type { StoredAccount } from "../core/AccountManager.js";
12
12
  export { LocalNodeManager } from "../node/LocalNodeManager.js";
13
13
  export type { LocalNodeOptions, LocalNodeInfo } from "../node/LocalNodeManager.js";
14
+ export { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
14
15
  export { setupLocalTesting } from "./setupLocalTesting.js";
15
16
  export type { LocalTestingContext } from "./setupLocalTesting.js";
16
17
  export { setupTestFixture, setupMinimalFixture, } from "./testFixtures.js";
@@ -6,5 +6,6 @@ export { saveDeployment, loadDeployment, getAllDeployments, getDeployedAddress,
6
6
  export { snapshot, getForkInfo, viewForkResource, compareForkState, listSnapshots, } from "../fork/test.js";
7
7
  export { AccountManager } from "../core/AccountManager.js";
8
8
  export { LocalNodeManager } from "../node/LocalNodeManager.js";
9
+ export { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
9
10
  export { setupLocalTesting } from "./setupLocalTesting.js";
10
11
  export { setupTestFixture, setupMinimalFixture, } from "./testFixtures.js";
@@ -2,6 +2,7 @@ import { existsSync } from "fs";
2
2
  import { resolve } from "path";
3
3
  import { loadUserConfig } from "../core/config.js";
4
4
  import { runCli } from "../utils/runCli.js";
5
+ import { logger } from "../ui/index.js";
5
6
  /**
6
7
  * Run Move unit tests using Movement CLI
7
8
  * @param options Test options including filter, warnings, and skip behavior
@@ -12,8 +13,9 @@ export async function runMoveTests(options = {}) {
12
13
  const moveDir = resolve(process.cwd(), userConfig.moveDir || "./move");
13
14
  if (!existsSync(moveDir)) {
14
15
  if (options.skipIfMissing) {
15
- console.log("No Move directory found (./move not found)");
16
- console.log(" Skipping Move tests...\n");
16
+ logger.info("No Move directory found (./move not found)");
17
+ logger.plain(" Skipping Move tests...");
18
+ logger.newline();
17
19
  return;
18
20
  }
19
21
  else {
@@ -42,12 +44,13 @@ export async function runMoveTests(options = {}) {
42
44
  catch (error) {
43
45
  // Spawn-time failure (ENOENT, etc.). The original code logged a
44
46
  // Movement-CLI-install hint here; keep that.
45
- console.error(`Failed to run Move tests: ${error.message}`);
46
- console.error(" Make sure Movement CLI is installed");
47
+ logger.error(`Failed to run Move tests: ${error.message}`);
48
+ logger.error(" Make sure Movement CLI is installed");
47
49
  throw error;
48
50
  }
49
51
  if (result.exitCode === 0) {
50
- console.log("\n✓ Move tests passed");
52
+ logger.newline();
53
+ logger.success("Move tests passed");
51
54
  return;
52
55
  }
53
56
  throw new Error("Move tests failed");
@@ -14,8 +14,10 @@ export async function setupTestEnvironment(networkName) {
14
14
  fullnode: config.rpc,
15
15
  });
16
16
  const aptos = new Aptos(aptosConfig);
17
- // Load account using AccountManager
18
- const account = AccountManager.loadAccountFromPrivateKey(config.privateKey);
17
+ // Load account using a throwaway AccountManager instance. This helper
18
+ // returns Aptos+Account directly (no runtime exposure), so a local
19
+ // instance suffices — no shared pool needed.
20
+ const account = new AccountManager().loadAccountFromPrivateKey(config.privateKey);
19
21
  logger.success("Test environment ready");
20
22
  logger.plain(` Account: ${account.accountAddress.toString()}`);
21
23
  logger.plain(` Network: ${config.network}`);
@@ -28,5 +30,6 @@ export async function setupTestEnvironment(networkName) {
28
30
  };
29
31
  }
30
32
  export function createTestAccount() {
31
- return AccountManager.createAccount();
33
+ // Throwaway instance — see setupTestEnvironment for rationale.
34
+ return new AccountManager().createAccount();
32
35
  }
@@ -1,7 +1,7 @@
1
1
  import type { MovehatRuntime } from "../types/runtime.js";
2
2
  import { ForkManager } from "../fork/manager.js";
3
3
  import { ForkServer } from "../fork/server.js";
4
- import { LocalNodeManager } from "../node/LocalNodeManager.js";
4
+ import type { NodeProvider } from "../node/NodeProvider.js";
5
5
  import type { LocalTestOptions } from "../types/config.js";
6
6
  /**
7
7
  * Context returned by {@link setupLocalTesting}.
@@ -17,7 +17,7 @@ import type { LocalTestOptions } from "../types/config.js";
17
17
  export interface LocalTestingContext {
18
18
  runtime: MovehatRuntime;
19
19
  /** @internal */
20
- localNode?: LocalNodeManager;
20
+ localNode?: NodeProvider;
21
21
  /** @internal */
22
22
  forkServer?: ForkServer;
23
23
  /** @internal */
@@ -4,6 +4,7 @@ import { initRuntime } from "../runtime.js";
4
4
  import { ForkManager } from "../fork/manager.js";
5
5
  import { ForkServer } from "../fork/server.js";
6
6
  import { LocalNodeManager } from "../node/LocalNodeManager.js";
7
+ import { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
7
8
  import { AccountManager } from "../core/AccountManager.js";
8
9
  import { logger } from "../ui/index.js";
9
10
  const BUILTIN_FORK_RPCS = {
@@ -64,11 +65,13 @@ export async function setupLocalTesting(options = {}) {
64
65
  logger.kv("Accounts", accountLabels.join(", "), 2);
65
66
  logger.newline();
66
67
  if (mode === 'local-node') {
67
- const { runtime, localNode } = await setupWithLocalNode(options, accountLabels, autoFund, defaultBalance);
68
+ const { runtime, localNode, ownsNode } = await setupWithLocalNode(options, accountLabels, autoFund, defaultBalance);
68
69
  return {
69
70
  runtime,
70
71
  localNode,
71
72
  teardown: async () => {
73
+ if (!ownsNode)
74
+ return;
72
75
  logger.newline();
73
76
  logger.step("Stopping local testing environment...");
74
77
  await localNode.stop();
@@ -97,29 +100,53 @@ export async function setupLocalTesting(options = {}) {
97
100
  * Setup using local Movement node (full blockchain)
98
101
  */
99
102
  async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalance) {
100
- const nodeTestDir = options.nodeTestDir || join(process.cwd(), ".movehat", "local-node");
101
- const nodeForceRestart = options.nodeForceRestart !== false;
102
- const nodeFaucetPort = options.nodeFaucetPort || 8081;
103
- const nodeApiPort = options.nodeApiPort || 8080;
104
- const nodeReadyPort = options.nodeReadyPort || 8070;
105
- const nodeSilent = options.nodeSilent ?? false;
106
- const localNode = new LocalNodeManager({
107
- testDir: nodeTestDir,
108
- forceRestart: nodeForceRestart,
109
- faucetPort: nodeFaucetPort,
110
- apiPort: nodeApiPort,
111
- readyPort: nodeReadyPort,
112
- silent: nodeSilent,
113
- });
114
- const nodeInfo = await localNode.start();
103
+ let localNode;
104
+ let ownsNode;
105
+ let nodeInfo;
106
+ if (options.localNode) {
107
+ localNode = options.localNode;
108
+ ownsNode = false;
109
+ if (!localNode.isRunning()) {
110
+ throw new Error("localNode was provided but isRunning() is false. " +
111
+ "Start the node before passing it to setupLocalTesting.");
112
+ }
113
+ nodeInfo = localNode.getNodeInfo();
114
+ }
115
+ else if (options.useMovelite !== false && findMoveliteBinary()) {
116
+ localNode = new MoveliteManager(findMoveliteBinary());
117
+ nodeInfo = await localNode.start();
118
+ ownsNode = true;
119
+ }
120
+ else {
121
+ const nodeTestDir = options.nodeTestDir || join(process.cwd(), ".movehat", "local-node");
122
+ const nodeForceRestart = options.nodeForceRestart !== false;
123
+ const nodeFaucetPort = options.nodeFaucetPort || 8081;
124
+ const nodeApiPort = options.nodeApiPort || 8080;
125
+ const nodeReadyPort = options.nodeReadyPort || 8070;
126
+ const nodeSilent = options.nodeSilent ?? false;
127
+ localNode = new LocalNodeManager({
128
+ testDir: nodeTestDir,
129
+ forceRestart: nodeForceRestart,
130
+ faucetPort: nodeFaucetPort,
131
+ apiPort: nodeApiPort,
132
+ readyPort: nodeReadyPort,
133
+ silent: nodeSilent,
134
+ });
135
+ ownsNode = true;
136
+ nodeInfo = await localNode.start();
137
+ }
115
138
  // Once the node is up, every later step (account creation, funding,
116
139
  // runtime init, autoDeploy) is fallible. If any of them throws we
117
140
  // must stop the node we just started — otherwise the child process
118
141
  // leaks and port 8080 stays bound until the OS reaps it (manifests as
119
142
  // "Movement command failed" on the next test:example run).
120
143
  try {
144
+ // Per-context AccountManager. Threaded into initRuntime below so
145
+ // the runtime exposes the SAME instance — the labels created by
146
+ // createBatch here are visible on `runtime.accountManager`.
147
+ const accountManager = new AccountManager();
121
148
  logger.step(`Generating ${accountLabels.length} test accounts...`);
122
- const accounts = AccountManager.createBatch(accountLabels);
149
+ const accounts = accountManager.createBatch(accountLabels);
123
150
  for (const [label, account] of Object.entries(accounts)) {
124
151
  logger.plain(` ${label}: ${account.accountAddress.toString()}`);
125
152
  }
@@ -129,12 +156,13 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
129
156
  await localNode.fundAccounts(accountsList, defaultBalance);
130
157
  }
131
158
  logger.step("Initializing runtime for local network...");
132
- const deployerPrivateKey = AccountManager.exportPrivateKeys(["deployer"]).deployer;
159
+ const deployerPrivateKey = accountManager.exportPrivateKeys(["deployer"]).deployer;
133
160
  if (!deployerPrivateKey) {
134
161
  throw new Error("Failed to get deployer private key");
135
162
  }
136
163
  const runtime = await initRuntime({
137
164
  network: "local",
165
+ accountManager,
138
166
  configOverride: {
139
167
  networks: {
140
168
  local: {
@@ -183,12 +211,12 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
183
211
  logger.plain(` Accounts: ${Array.from(accountLabels).join(", ")}`);
184
212
  logger.plain(` Balance per account: ${defaultBalance / 100_000_000} MOVE`);
185
213
  logger.newline();
186
- return { runtime, localNode };
214
+ return { runtime, localNode, ownsNode };
187
215
  }
188
216
  catch (error) {
189
- // Best-effort cleanup. Swallow the stop() error so the original
190
- // setup failure surfaces unchanged.
191
- await localNode.stop().catch(() => { });
217
+ if (ownsNode) {
218
+ await localNode.stop().catch(() => { });
219
+ }
192
220
  throw error;
193
221
  }
194
222
  }
@@ -256,8 +284,10 @@ async function setupWithFork(options, accountLabels, autoFund, defaultBalance) {
256
284
  // we leak the listener.
257
285
  try {
258
286
  await new Promise((resolve) => setTimeout(resolve, 500));
287
+ // Per-context AccountManager (mirror of setupWithLocalNode pattern).
288
+ const accountManager = new AccountManager();
259
289
  logger.step(`Generating ${accountLabels.length} test accounts...`);
260
- const accounts = AccountManager.createBatch(accountLabels);
290
+ const accounts = accountManager.createBatch(accountLabels);
261
291
  for (const [label, account] of Object.entries(accounts)) {
262
292
  logger.plain(` ${label}: ${account.accountAddress.toString()}`);
263
293
  }
@@ -267,12 +297,13 @@ async function setupWithFork(options, accountLabels, autoFund, defaultBalance) {
267
297
  await forkManager.fundMultipleAccounts(addresses, defaultBalance);
268
298
  }
269
299
  logger.step("Initializing runtime for local network...");
270
- const deployerPrivateKey = AccountManager.exportPrivateKeys(["deployer"]).deployer;
300
+ const deployerPrivateKey = accountManager.exportPrivateKeys(["deployer"]).deployer;
271
301
  if (!deployerPrivateKey) {
272
302
  throw new Error("Failed to get deployer private key");
273
303
  }
274
304
  const runtime = await initRuntime({
275
305
  network: "local",
306
+ accountManager,
276
307
  configOverride: {
277
308
  networks: {
278
309
  local: {
@@ -22,10 +22,11 @@ export interface TestFixture<TModules extends string = string> {
22
22
  /**
23
23
  * Stop the local node / fork server this fixture started.
24
24
  *
25
- * Does **not** clear the shared `AccountManager` pool — clearing it
26
- * would break parallel `setupTestFixture` invocations that share the
27
- * pool. The pool grows for the lifetime of the process; the process
28
- * exit reclaims it.
25
+ * Each fixture owns its own `AccountManager` instance (via
26
+ * `setupLocalTesting` `initRuntime`), so there is no shared pool
27
+ * to clear accounts are garbage-collected when the fixture goes
28
+ * out of scope. Parallel `setupTestFixture` invocations have fully
29
+ * isolated label maps and pools (M9.2, #270).
29
30
  */
30
31
  teardown: () => Promise<void>;
31
32
  }
@@ -1,4 +1,3 @@
1
- import { AccountManager } from "../core/AccountManager.js";
2
1
  import { setupLocalTesting } from "./setupLocalTesting.js";
3
2
  import { logger } from "../ui/index.js";
4
3
  /**
@@ -59,7 +58,7 @@ export async function setupTestFixture(modules, accountLabels = ["alice", "bob"]
59
58
  // the original assembly error unchanged.
60
59
  try {
61
60
  const mh = ctx.runtime;
62
- const labeledAccounts = AccountManager.getLabeledAccounts();
61
+ const labeledAccounts = mh.accountManager.getLabeledAccounts();
63
62
  // any: TestFixture.accounts has a structural shape with required
64
63
  // `deployer/alice/bob` plus a `[key: string]: Account` index. The
65
64
  // builder fills the index dynamically; typing this as the exact
@@ -72,7 +71,7 @@ export async function setupTestFixture(modules, accountLabels = ["alice", "bob"]
72
71
  deployer: labeledAccounts.deployer,
73
72
  };
74
73
  for (const label of accountLabels) {
75
- accounts[label] = labeledAccounts[label] || AccountManager.getOrCreateLabeled(label);
74
+ accounts[label] = labeledAccounts[label] || mh.accountManager.getOrCreateLabeled(label);
76
75
  }
77
76
  const contracts = {};
78
77
  for (const moduleName of modules) {
@@ -119,14 +118,14 @@ export async function setupMinimalFixture(accountLabels = ["alice", "bob"], opti
119
118
  // Same teardown-on-assembly-failure pattern as setupTestFixture.
120
119
  try {
121
120
  const mh = ctx.runtime;
122
- const labeledAccounts = AccountManager.getLabeledAccounts();
121
+ const labeledAccounts = mh.accountManager.getLabeledAccounts();
123
122
  // any: see setupTestFixture above — same dynamic-key builder pattern.
124
123
  const accounts = {
125
124
  // non-null: deployer is unconditionally added to allLabels at L156 above.
126
125
  deployer: labeledAccounts.deployer,
127
126
  };
128
127
  for (const label of accountLabels) {
129
- accounts[label] = labeledAccounts[label] || AccountManager.getOrCreateLabeled(label);
128
+ accounts[label] = labeledAccounts[label] || mh.accountManager.getOrCreateLabeled(label);
130
129
  }
131
130
  logger.newline();
132
131
  logger.success("Minimal fixture ready!");
@@ -3,7 +3,7 @@ import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import { isNewerVersion } from "./semver-utils.js";
5
5
  import { fetchLatestVersion } from "./npm-registry.js";
6
- import { box, colors, formatCommand } from "../ui/index.js";
6
+ import { box, colors, formatCommand, logger } from "../ui/index.js";
7
7
  const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
8
8
  const CACHE_DIR = join(homedir(), ".movehat");
9
9
  const CACHE_FILE = join(CACHE_DIR, "version-cache.json");
@@ -72,7 +72,9 @@ export function checkForUpdates(currentVersion, packageName) {
72
72
  const updateMessage = box(`${colors.brandBright('Update Available!')}\n\n` +
73
73
  `${currentVersion} ${colors.dim('→')} ${colors.success(cache.latestVersion)}\n\n` +
74
74
  `${formatCommand('movehat update')}`, { borderColor: 'warning', padding: 1 });
75
- console.error('\n' + updateMessage + '\n');
75
+ logger.newline();
76
+ logger.warning(updateMessage);
77
+ logger.newline();
76
78
  }
77
79
  // Update cache in background if needed (doesn't block)
78
80
  if (!cache || Date.now() - cache.lastChecked > CACHE_DURATION) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export * from "./helpers/index.js";
2
- export type { MovehatConfig } from "./types/config.js";
2
+ export type { MovehatConfig, NetworkConfig, LocalTestingMode, } from "./types/config.js";
3
3
  export { initRuntime } from "./runtime.js";
4
+ export type { InitRuntimeOptions } from "./runtime.js";
4
5
  export type { MovehatRuntime, NetworkInfo } from "./types/runtime.js";
6
+ export type { ChildProcessAdapter } from "./utils/childProcessAdapter.js";
5
7
  export { ForkManager } from "./fork/manager.js";
6
8
  export { MovementApiClient } from "./fork/api.js";
7
9
  export { ForkStorage } from "./fork/storage.js";
@@ -0,0 +1,18 @@
1
+ import type { Account } from "@aptos-labs/ts-sdk";
2
+ import type { LocalNodeInfo } from "./LocalNodeManager.js";
3
+ export declare class MoveliteManager {
4
+ private process;
5
+ private port;
6
+ private killed;
7
+ private readonly binaryPath;
8
+ constructor(binaryPath: string, port?: number);
9
+ start(): Promise<LocalNodeInfo>;
10
+ private waitForReady;
11
+ fundAccount(address: string, amount: number): Promise<void>;
12
+ fundAccounts(accounts: Account[], balance: number): Promise<void>;
13
+ stop(): Promise<void>;
14
+ private isPortInUse;
15
+ isRunning(): boolean;
16
+ getNodeInfo(): LocalNodeInfo;
17
+ }
18
+ export declare function findMoveliteBinary(): string | null;