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.
- package/dist/cli.js +1 -1
- package/dist/commands/fork/fund.js +3 -1
- package/dist/commands/fork/serve.js +10 -5
- package/dist/commands/test-move.js +6 -3
- package/dist/core/AccountManager.d.ts +121 -139
- package/dist/core/AccountManager.js +217 -158
- package/dist/fork/api.d.ts +1 -1
- package/dist/fork/api.js +9 -4
- package/dist/fork/manager.d.ts +2 -2
- package/dist/fork/manager.js +4 -8
- package/dist/fork/storage.js +5 -4
- package/dist/fork/test.d.ts +1 -1
- package/dist/fork/validation.d.ts +9 -0
- package/dist/fork/validation.js +88 -0
- package/dist/harness/Harness.d.ts +35 -6
- package/dist/harness/Harness.js +36 -5
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/move-tests.js +8 -5
- package/dist/helpers/setup.js +6 -3
- package/dist/helpers/setupLocalTesting.d.ts +2 -2
- package/dist/helpers/setupLocalTesting.js +55 -24
- package/dist/helpers/testFixtures.d.ts +5 -4
- package/dist/helpers/testFixtures.js +4 -5
- package/dist/helpers/version-check.js +4 -2
- package/dist/index.d.ts +3 -1
- package/dist/node/MoveliteManager.d.ts +18 -0
- package/dist/node/MoveliteManager.js +152 -0
- package/dist/node/NodeProvider.d.ts +9 -0
- package/dist/node/NodeProvider.js +1 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +8 -4
- package/dist/templates/.mocharc.json +1 -0
- package/dist/templates/tests/Counter.test.ts +12 -14
- package/dist/templates/tests/setup.ts +34 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/fork.d.ts +24 -0
- package/dist/types/runtime.d.ts +2 -0
- package/package.json +4 -1
package/dist/fork/storage.js
CHANGED
|
@@ -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
|
/**
|
package/dist/fork/test.d.ts
CHANGED
|
@@ -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<
|
|
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 {
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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?:
|
|
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;
|
package/dist/harness/Harness.js
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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) {
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/helpers/index.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
+
logger.newline();
|
|
53
|
+
logger.success("Move tests passed");
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
53
56
|
throw new Error("Move tests failed");
|
package/dist/helpers/setup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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 =
|
|
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] ||
|
|
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 =
|
|
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] ||
|
|
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
|
-
|
|
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;
|