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