naracli 1.0.32 → 1.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/nara-cli-bundle.cjs +92393 -88258
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/package.json +8 -4
- package/src/cli/commands/agent.ts +46 -8
- package/src/cli/commands/config.ts +31 -13
- package/src/cli/commands/quest.ts +151 -15
- package/src/cli/commands/zkid.ts +5 -5
- package/src/cli/utils/agent-config.ts +127 -22
- package/src/cli/utils/wallet.ts +9 -7
- package/src/tests/agent.test.ts +193 -0
- package/src/tests/config.test.ts +199 -0
- package/src/tests/helpers.ts +20 -0
- package/src/tests/quest-referral.test.ts +15 -22
- package/src/tests/quest.test.ts +1 -1
- package/src/tests/validation.test.ts +80 -0
- package/src/tests/zkid.test.ts +6 -6
|
@@ -1,50 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent config utilities
|
|
2
|
+
* Agent config utilities
|
|
3
|
+
*
|
|
4
|
+
* Global config: ~/.config/nara/config.json — rpc_url, wallet
|
|
5
|
+
* Network config: ~/.config/nara/agent-{network}.json — agent_ids, zk_ids
|
|
6
|
+
*
|
|
7
|
+
* {network} is derived from the effective RPC URL:
|
|
8
|
+
* https://mainnet-api.nara.build/ → mainnet-api-nara-build
|
|
9
|
+
* https://devnet-api.nara.build/ → devnet-api-nara-build
|
|
10
|
+
* http://127.0.0.1:8899/ → 127-0-0-1-8899
|
|
3
11
|
*/
|
|
4
12
|
|
|
5
|
-
import { join
|
|
13
|
+
import { join } from "node:path";
|
|
6
14
|
import { homedir } from "node:os";
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { DEFAULT_RPC_URL } from "nara-sdk";
|
|
8
17
|
|
|
9
|
-
const
|
|
18
|
+
const CONFIG_DIR = join(homedir(), ".config", "nara");
|
|
19
|
+
const GLOBAL_CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
// ─── URL → filename ─────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export function rpcUrlToNetworkName(url: string): string {
|
|
24
|
+
let name = url.replace(/^https?:\/\//, "");
|
|
25
|
+
name = name.replace(/\/+$/, "");
|
|
26
|
+
name = name.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
27
|
+
name = name.replace(/-+/g, "-");
|
|
28
|
+
name = name.replace(/^-|-$/g, "");
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function networkConfigPath(rpcUrl: string): string {
|
|
33
|
+
return join(CONFIG_DIR, `agent-${rpcUrlToNetworkName(rpcUrl)}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Global config (rpc_url, wallet) ─────────────────────────────
|
|
37
|
+
|
|
38
|
+
export interface GlobalConfig {
|
|
14
39
|
rpc_url?: string;
|
|
15
40
|
wallet?: string;
|
|
16
41
|
}
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
export function loadGlobalConfig(): GlobalConfig {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(GLOBAL_CONFIG_PATH, "utf-8");
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
return {
|
|
48
|
+
rpc_url: parsed.rpc_url ?? undefined,
|
|
49
|
+
wallet: parsed.wallet ?? undefined,
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function saveGlobalConfig(config: GlobalConfig): void {
|
|
57
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
58
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the effective RPC URL (without CLI flag context).
|
|
63
|
+
* For use when no CLI flag is available.
|
|
64
|
+
*/
|
|
65
|
+
export function getConfiguredRpcUrl(): string {
|
|
66
|
+
const global = loadGlobalConfig();
|
|
67
|
+
return global.rpc_url || DEFAULT_RPC_URL;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Network config (agent_ids, zk_ids) ──────────────────────────
|
|
71
|
+
|
|
72
|
+
export interface NetworkConfig {
|
|
73
|
+
agent_ids: string[];
|
|
74
|
+
zk_ids: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DEFAULT_NETWORK_CONFIG: NetworkConfig = { agent_ids: [], zk_ids: [] };
|
|
19
78
|
|
|
20
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Load network-specific config.
|
|
81
|
+
* @param rpcUrl - effective RPC URL (determines which file to load)
|
|
82
|
+
*/
|
|
83
|
+
export function loadNetworkConfig(rpcUrl?: string): NetworkConfig {
|
|
84
|
+
const url = rpcUrl || getConfiguredRpcUrl();
|
|
85
|
+
const path = networkConfigPath(url);
|
|
21
86
|
try {
|
|
22
|
-
const raw = readFileSync(
|
|
87
|
+
const raw = readFileSync(path, "utf-8");
|
|
23
88
|
const parsed = JSON.parse(raw);
|
|
24
89
|
return {
|
|
25
90
|
agent_ids: Array.isArray(parsed.agent_ids) ? parsed.agent_ids : [],
|
|
26
91
|
zk_ids: Array.isArray(parsed.zk_ids) ? parsed.zk_ids : [],
|
|
27
|
-
rpc_url: parsed.rpc_url ?? undefined,
|
|
28
|
-
wallet: parsed.wallet ?? undefined,
|
|
29
92
|
};
|
|
30
93
|
} catch {
|
|
31
|
-
return { ...
|
|
94
|
+
return { ...DEFAULT_NETWORK_CONFIG };
|
|
32
95
|
}
|
|
33
96
|
}
|
|
34
97
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Save network-specific config.
|
|
100
|
+
*/
|
|
101
|
+
export function saveNetworkConfig(config: NetworkConfig, rpcUrl?: string): void {
|
|
102
|
+
const url = rpcUrl || getConfiguredRpcUrl();
|
|
103
|
+
const path = networkConfigPath(url);
|
|
104
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
105
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
|
|
38
106
|
}
|
|
39
107
|
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
// ─── Convenience helpers ─────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function addAgentId(id: string, rpcUrl?: string): void {
|
|
111
|
+
const config = loadNetworkConfig(rpcUrl);
|
|
42
112
|
config.agent_ids = [id, ...config.agent_ids.filter((x) => x !== id)];
|
|
43
|
-
|
|
113
|
+
saveNetworkConfig(config, rpcUrl);
|
|
44
114
|
}
|
|
45
115
|
|
|
46
|
-
export function addZkId(name: string): void {
|
|
47
|
-
const config =
|
|
116
|
+
export function addZkId(name: string, rpcUrl?: string): void {
|
|
117
|
+
const config = loadNetworkConfig(rpcUrl);
|
|
48
118
|
config.zk_ids = [name, ...config.zk_ids.filter((x) => x !== name)];
|
|
49
|
-
|
|
119
|
+
saveNetworkConfig(config, rpcUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Migration: import old agent.json if network config doesn't exist ──
|
|
123
|
+
|
|
124
|
+
const LEGACY_CONFIG_PATH = join(CONFIG_DIR, "agent.json");
|
|
125
|
+
|
|
126
|
+
export function migrateIfNeeded(rpcUrl?: string): void {
|
|
127
|
+
const url = rpcUrl || getConfiguredRpcUrl();
|
|
128
|
+
const path = networkConfigPath(url);
|
|
129
|
+
if (existsSync(path)) return;
|
|
130
|
+
if (!existsSync(LEGACY_CONFIG_PATH)) return;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const raw = readFileSync(LEGACY_CONFIG_PATH, "utf-8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
|
|
136
|
+
// Migrate network-specific fields
|
|
137
|
+
const networkConfig: NetworkConfig = {
|
|
138
|
+
agent_ids: Array.isArray(parsed.agent_ids) ? parsed.agent_ids : [],
|
|
139
|
+
zk_ids: Array.isArray(parsed.zk_ids) ? parsed.zk_ids : [],
|
|
140
|
+
};
|
|
141
|
+
saveNetworkConfig(networkConfig, url);
|
|
142
|
+
|
|
143
|
+
// Migrate global fields if config.json doesn't exist
|
|
144
|
+
if (!existsSync(GLOBAL_CONFIG_PATH)) {
|
|
145
|
+
const globalConfig: GlobalConfig = {};
|
|
146
|
+
if (parsed.rpc_url) globalConfig.rpc_url = parsed.rpc_url;
|
|
147
|
+
if (parsed.wallet) globalConfig.wallet = parsed.wallet;
|
|
148
|
+
if (Object.keys(globalConfig).length > 0) {
|
|
149
|
+
saveGlobalConfig(globalConfig);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore migration errors
|
|
154
|
+
}
|
|
50
155
|
}
|
package/src/cli/utils/wallet.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Keypair } from "@solana/web3.js";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { DEFAULT_RPC_URL } from "nara-sdk";
|
|
9
|
-
import {
|
|
9
|
+
import { loadGlobalConfig, migrateIfNeeded } from "./agent-config";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_WALLET_PATH = join(homedir(), ".config", "nara", "id.json");
|
|
12
12
|
|
|
@@ -22,13 +22,13 @@ function resolvePath(p: string): string {
|
|
|
22
22
|
*
|
|
23
23
|
* Priority:
|
|
24
24
|
* 1. CLI flag (walletPath parameter)
|
|
25
|
-
* 2.
|
|
25
|
+
* 2. Global config (~/.config/nara/config.json wallet field)
|
|
26
26
|
* 3. Default path (~/.config/nara/id.json)
|
|
27
27
|
*/
|
|
28
28
|
export async function loadWallet(walletPath?: string): Promise<Keypair> {
|
|
29
29
|
let path = walletPath;
|
|
30
30
|
if (!path) {
|
|
31
|
-
const config =
|
|
31
|
+
const config = loadGlobalConfig();
|
|
32
32
|
path = config.wallet ? resolvePath(config.wallet) : DEFAULT_WALLET_PATH;
|
|
33
33
|
} else {
|
|
34
34
|
path = resolvePath(path);
|
|
@@ -64,11 +64,13 @@ export async function loadWallet(walletPath?: string): Promise<Keypair> {
|
|
|
64
64
|
*
|
|
65
65
|
* Priority:
|
|
66
66
|
* 1. CLI flag (rpcUrl parameter)
|
|
67
|
-
* 2.
|
|
67
|
+
* 2. Global config (~/.config/nara/config.json rpc_url field)
|
|
68
68
|
* 3. Default (from SDK constants)
|
|
69
|
+
*
|
|
70
|
+
* Also triggers migration from legacy agent.json if needed.
|
|
69
71
|
*/
|
|
70
72
|
export function getRpcUrl(rpcUrl?: string): string {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
73
|
+
const effective = rpcUrl || loadGlobalConfig().rpc_url || DEFAULT_RPC_URL;
|
|
74
|
+
migrateIfNeeded(effective);
|
|
75
|
+
return effective;
|
|
74
76
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `agent` CLI commands
|
|
3
|
+
*
|
|
4
|
+
* - Help / validation tests run without wallet or chain
|
|
5
|
+
* - On-chain tests require wallet + NARA balance
|
|
6
|
+
*
|
|
7
|
+
* Run: npm run test:agent-cli
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { runCli, hasWallet } from "./helpers.js";
|
|
13
|
+
|
|
14
|
+
// ─── Help output ──────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("agent --help", () => {
|
|
17
|
+
it("shows all subcommands", async () => {
|
|
18
|
+
const { stdout, exitCode } = await runCli(["agent", "--help"]);
|
|
19
|
+
assert.equal(exitCode, 0);
|
|
20
|
+
for (const cmd of [
|
|
21
|
+
"register", "get", "set-bio", "set-metadata",
|
|
22
|
+
"upload-memory", "memory", "transfer", "close-buffer",
|
|
23
|
+
"delete", "set-referral", "log",
|
|
24
|
+
]) {
|
|
25
|
+
assert.ok(stdout.includes(cmd), `missing subcommand: ${cmd}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("agent register --help shows --referral option", async () => {
|
|
30
|
+
const { stdout, exitCode } = await runCli(["agent", "register", "--help"]);
|
|
31
|
+
assert.equal(exitCode, 0);
|
|
32
|
+
assert.ok(stdout.includes("--referral"));
|
|
33
|
+
assert.ok(stdout.includes("<agent-id>"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("agent set-referral --help shows both args", async () => {
|
|
37
|
+
const { stdout, exitCode } = await runCli(["agent", "set-referral", "--help"]);
|
|
38
|
+
assert.equal(exitCode, 0);
|
|
39
|
+
assert.ok(stdout.includes("<agent-id>"));
|
|
40
|
+
assert.ok(stdout.includes("<referral-agent-id>"));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("agent log --help shows --model and --referral", async () => {
|
|
44
|
+
const { stdout, exitCode } = await runCli(["agent", "log", "--help"]);
|
|
45
|
+
assert.equal(exitCode, 0);
|
|
46
|
+
assert.ok(stdout.includes("--model"));
|
|
47
|
+
assert.ok(stdout.includes("--referral"));
|
|
48
|
+
assert.ok(stdout.includes("<agent-id>"));
|
|
49
|
+
assert.ok(stdout.includes("<activity>"));
|
|
50
|
+
assert.ok(stdout.includes("<log>"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("agent get --help shows <agent-id>", async () => {
|
|
54
|
+
const { stdout, exitCode } = await runCli(["agent", "get", "--help"]);
|
|
55
|
+
assert.equal(exitCode, 0);
|
|
56
|
+
assert.ok(stdout.includes("<agent-id>"));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("agent set-bio --help shows <bio>", async () => {
|
|
60
|
+
const { stdout, exitCode } = await runCli(["agent", "set-bio", "--help"]);
|
|
61
|
+
assert.equal(exitCode, 0);
|
|
62
|
+
assert.ok(stdout.includes("<bio>"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("agent set-metadata --help shows <json>", async () => {
|
|
66
|
+
const { stdout, exitCode } = await runCli(["agent", "set-metadata", "--help"]);
|
|
67
|
+
assert.equal(exitCode, 0);
|
|
68
|
+
assert.ok(stdout.includes("<json>"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("agent upload-memory --help shows <file>", async () => {
|
|
72
|
+
const { stdout, exitCode } = await runCli(["agent", "upload-memory", "--help"]);
|
|
73
|
+
assert.equal(exitCode, 0);
|
|
74
|
+
assert.ok(stdout.includes("<file>"));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("agent transfer --help shows <new-authority>", async () => {
|
|
78
|
+
const { stdout, exitCode } = await runCli(["agent", "transfer", "--help"]);
|
|
79
|
+
assert.equal(exitCode, 0);
|
|
80
|
+
assert.ok(stdout.includes("<new-authority>"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("agent delete --help shows <agent-id>", async () => {
|
|
84
|
+
const { stdout, exitCode } = await runCli(["agent", "delete", "--help"]);
|
|
85
|
+
assert.equal(exitCode, 0);
|
|
86
|
+
assert.ok(stdout.includes("<agent-id>"));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── Argument validation (no chain needed) ────────────────────────
|
|
91
|
+
|
|
92
|
+
describe("agent argument errors", () => {
|
|
93
|
+
it("agent get with no args exits non-zero", async () => {
|
|
94
|
+
const { exitCode } = await runCli(["agent", "get"]);
|
|
95
|
+
assert.notEqual(exitCode, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("agent register with no args exits non-zero", async () => {
|
|
99
|
+
const { exitCode } = await runCli(["agent", "register"]);
|
|
100
|
+
assert.notEqual(exitCode, 0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("agent set-referral with no args exits non-zero", async () => {
|
|
104
|
+
const { exitCode } = await runCli(["agent", "set-referral"]);
|
|
105
|
+
assert.notEqual(exitCode, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("agent set-referral with only one arg exits non-zero", async () => {
|
|
109
|
+
const { exitCode } = await runCli(["agent", "set-referral", "my-agent"]);
|
|
110
|
+
assert.notEqual(exitCode, 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("agent log with missing args exits non-zero", async () => {
|
|
114
|
+
const { exitCode } = await runCli(["agent", "log"]);
|
|
115
|
+
assert.notEqual(exitCode, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("agent log with only one arg exits non-zero", async () => {
|
|
119
|
+
const { exitCode } = await runCli(["agent", "log", "my-agent"]);
|
|
120
|
+
assert.notEqual(exitCode, 0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─── Name validation (lowercase) ─────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe("agent name validation", () => {
|
|
127
|
+
it("rejects uppercase agent ID", async () => {
|
|
128
|
+
if (!hasWallet) return;
|
|
129
|
+
const { exitCode, stderr } = await runCli(["agent", "register", "MyAgent"]);
|
|
130
|
+
assert.equal(exitCode, 1);
|
|
131
|
+
assert.ok(stderr.includes("lowercase"), `stderr: ${stderr}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("rejects agent ID starting with number", async () => {
|
|
135
|
+
if (!hasWallet) return;
|
|
136
|
+
const { exitCode, stderr } = await runCli(["agent", "register", "123agent"]);
|
|
137
|
+
assert.equal(exitCode, 1);
|
|
138
|
+
assert.ok(stderr.includes("lowercase"), `stderr: ${stderr}`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("rejects agent ID starting with hyphen", async () => {
|
|
142
|
+
if (!hasWallet) return;
|
|
143
|
+
const { exitCode, stderr } = await runCli(["agent", "register", "-my-agent"]);
|
|
144
|
+
// Commander may interpret -m as a flag, so check for either parse error or validation error
|
|
145
|
+
assert.notEqual(exitCode, 0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("rejects agent ID with special characters", async () => {
|
|
149
|
+
if (!hasWallet) return;
|
|
150
|
+
const { exitCode, stderr } = await runCli(["agent", "register", "my_agent"]);
|
|
151
|
+
assert.equal(exitCode, 1);
|
|
152
|
+
assert.ok(stderr.includes("lowercase"), `stderr: ${stderr}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("rejects agent ID with spaces", async () => {
|
|
156
|
+
if (!hasWallet) return;
|
|
157
|
+
const { exitCode, stderr } = await runCli(["agent", "register", "my agent"]);
|
|
158
|
+
// Commander may treat "agent" as a separate arg
|
|
159
|
+
assert.notEqual(exitCode, 0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─── Read-only chain queries ──────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe("agent get (read-only)", () => {
|
|
166
|
+
it("returns error for non-existent agent", async () => {
|
|
167
|
+
const { exitCode, stderr } = await runCli(["agent", "get", "definitely-does-not-exist-xyz999"]);
|
|
168
|
+
assert.equal(exitCode, 1);
|
|
169
|
+
assert.ok(stderr.length > 0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Metadata validation ─────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
describe("agent set-metadata validation", () => {
|
|
176
|
+
it("rejects invalid JSON", async () => {
|
|
177
|
+
if (!hasWallet) return;
|
|
178
|
+
const { exitCode, stderr } = await runCli(["agent", "set-metadata", "any-agent", "not-valid-json"]);
|
|
179
|
+
assert.equal(exitCode, 1);
|
|
180
|
+
assert.ok(stderr.includes("Invalid JSON"), `stderr: ${stderr}`);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ─── Upload validation ───────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe("agent upload-memory validation", () => {
|
|
187
|
+
it("rejects non-existent file", async () => {
|
|
188
|
+
if (!hasWallet) return;
|
|
189
|
+
const { exitCode, stderr } = await runCli(["agent", "upload-memory", "any-agent", "/tmp/__no_such_file__.bin"]);
|
|
190
|
+
assert.equal(exitCode, 1);
|
|
191
|
+
assert.ok(stderr.includes("Failed to read file"), `stderr: ${stderr}`);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `config` CLI commands and agent-config utilities
|
|
3
|
+
*
|
|
4
|
+
* - Config get/set/reset via CLI
|
|
5
|
+
* - rpcUrlToNetworkName conversion
|
|
6
|
+
* - Global and network config load/save
|
|
7
|
+
*
|
|
8
|
+
* Run: npm run test:config
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, before, after } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { runCli } from "./helpers.js";
|
|
17
|
+
import { rpcUrlToNetworkName } from "../cli/utils/agent-config.js";
|
|
18
|
+
|
|
19
|
+
const CONFIG_DIR = join(homedir(), ".config", "nara");
|
|
20
|
+
const GLOBAL_CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
21
|
+
|
|
22
|
+
// Backup and restore global config around tests
|
|
23
|
+
let originalConfig: string | null = null;
|
|
24
|
+
|
|
25
|
+
before(() => {
|
|
26
|
+
try {
|
|
27
|
+
originalConfig = readFileSync(GLOBAL_CONFIG_PATH, "utf-8");
|
|
28
|
+
} catch {
|
|
29
|
+
originalConfig = null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(() => {
|
|
34
|
+
if (originalConfig !== null) {
|
|
35
|
+
writeFileSync(GLOBAL_CONFIG_PATH, originalConfig);
|
|
36
|
+
} else if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
37
|
+
// Restore to no config (delete if it didn't exist before)
|
|
38
|
+
unlinkSync(GLOBAL_CONFIG_PATH);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── rpcUrlToNetworkName ──────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("rpcUrlToNetworkName", () => {
|
|
45
|
+
it("converts mainnet URL", () => {
|
|
46
|
+
assert.equal(rpcUrlToNetworkName("https://mainnet-api.nara.build/"), "mainnet-api-nara-build");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("converts devnet URL", () => {
|
|
50
|
+
assert.equal(rpcUrlToNetworkName("https://devnet-api.nara.build/"), "devnet-api-nara-build");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("converts localhost URL", () => {
|
|
54
|
+
assert.equal(rpcUrlToNetworkName("http://127.0.0.1:8899/"), "127-0-0-1-8899");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles URL without trailing slash", () => {
|
|
58
|
+
assert.equal(rpcUrlToNetworkName("https://mainnet-api.nara.build"), "mainnet-api-nara-build");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("handles URL with multiple slashes", () => {
|
|
62
|
+
const result = rpcUrlToNetworkName("https://example.com///");
|
|
63
|
+
assert.ok(!result.includes("/"));
|
|
64
|
+
assert.ok(!result.startsWith("-"));
|
|
65
|
+
assert.ok(!result.endsWith("-"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("replaces dots and special chars with hyphens", () => {
|
|
69
|
+
assert.equal(rpcUrlToNetworkName("https://my.custom.rpc:9090/"), "my-custom-rpc-9090");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("collapses multiple hyphens", () => {
|
|
73
|
+
const result = rpcUrlToNetworkName("http://a...b///c/");
|
|
74
|
+
assert.ok(!result.includes("--"));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── config get ───────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("config get", () => {
|
|
81
|
+
it("shows current config in text mode", async () => {
|
|
82
|
+
const { stdout, exitCode } = await runCli(["config", "get"]);
|
|
83
|
+
assert.equal(exitCode, 0);
|
|
84
|
+
assert.ok(stdout.includes("RPC URL:"));
|
|
85
|
+
assert.ok(stdout.includes("Wallet:"));
|
|
86
|
+
assert.ok(stdout.includes("Network:"));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("--json returns structured config", async () => {
|
|
90
|
+
const { stdout, exitCode } = await runCli(["config", "get", "--json"]);
|
|
91
|
+
assert.equal(exitCode, 0);
|
|
92
|
+
const data = JSON.parse(stdout);
|
|
93
|
+
assert.ok(typeof data.rpc_url === "string");
|
|
94
|
+
assert.ok(typeof data.wallet === "string");
|
|
95
|
+
assert.ok(typeof data.network === "string");
|
|
96
|
+
assert.ok(Array.isArray(data.agent_ids));
|
|
97
|
+
assert.ok(Array.isArray(data.zk_ids));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── config set ───────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("config set", () => {
|
|
104
|
+
it("sets rpc-url", async () => {
|
|
105
|
+
const testUrl = "https://test-rpc.example.com/";
|
|
106
|
+
const { exitCode, stdout } = await runCli(["config", "set", "rpc-url", testUrl]);
|
|
107
|
+
assert.equal(exitCode, 0);
|
|
108
|
+
|
|
109
|
+
// Verify it was saved
|
|
110
|
+
const { stdout: getOut } = await runCli(["config", "get", "--json"]);
|
|
111
|
+
const data = JSON.parse(getOut);
|
|
112
|
+
assert.equal(data.rpc_url, testUrl);
|
|
113
|
+
assert.equal(data.rpc_url_custom, true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("sets wallet path", async () => {
|
|
117
|
+
const testPath = "/tmp/test-wallet.json";
|
|
118
|
+
const { exitCode } = await runCli(["config", "set", "wallet", testPath]);
|
|
119
|
+
assert.equal(exitCode, 0);
|
|
120
|
+
|
|
121
|
+
const { stdout } = await runCli(["config", "get", "--json"]);
|
|
122
|
+
const data = JSON.parse(stdout);
|
|
123
|
+
assert.equal(data.wallet, testPath);
|
|
124
|
+
assert.equal(data.wallet_custom, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects unknown config key", async () => {
|
|
128
|
+
const { exitCode, stderr } = await runCli(["config", "set", "unknown-key", "value"]);
|
|
129
|
+
assert.equal(exitCode, 1);
|
|
130
|
+
assert.ok(stderr.includes("Unknown config key"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("--json returns set confirmation", async () => {
|
|
134
|
+
const { stdout, exitCode } = await runCli(["config", "set", "rpc-url", "https://example.com/", "--json"]);
|
|
135
|
+
assert.equal(exitCode, 0);
|
|
136
|
+
const data = JSON.parse(stdout);
|
|
137
|
+
assert.equal(data.key, "rpc-url");
|
|
138
|
+
assert.equal(data.value, "https://example.com/");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ─── config reset ─────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe("config reset", () => {
|
|
145
|
+
before(async () => {
|
|
146
|
+
// Set both values first
|
|
147
|
+
await runCli(["config", "set", "rpc-url", "https://test.example.com/"]);
|
|
148
|
+
await runCli(["config", "set", "wallet", "/tmp/test.json"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("resets a single key", async () => {
|
|
152
|
+
const { exitCode } = await runCli(["config", "reset", "rpc-url"]);
|
|
153
|
+
assert.equal(exitCode, 0);
|
|
154
|
+
|
|
155
|
+
const { stdout } = await runCli(["config", "get", "--json"]);
|
|
156
|
+
const data = JSON.parse(stdout);
|
|
157
|
+
assert.equal(data.rpc_url_custom, false);
|
|
158
|
+
// wallet should still be custom
|
|
159
|
+
assert.equal(data.wallet_custom, true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("resets all keys", async () => {
|
|
163
|
+
// Set again to ensure both are custom
|
|
164
|
+
await runCli(["config", "set", "rpc-url", "https://test.example.com/"]);
|
|
165
|
+
const { exitCode } = await runCli(["config", "reset"]);
|
|
166
|
+
assert.equal(exitCode, 0);
|
|
167
|
+
|
|
168
|
+
const { stdout } = await runCli(["config", "get", "--json"]);
|
|
169
|
+
const data = JSON.parse(stdout);
|
|
170
|
+
assert.equal(data.rpc_url_custom, false);
|
|
171
|
+
assert.equal(data.wallet_custom, false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects unknown reset key", async () => {
|
|
175
|
+
const { exitCode, stderr } = await runCli(["config", "reset", "bad-key"]);
|
|
176
|
+
assert.equal(exitCode, 1);
|
|
177
|
+
assert.ok(stderr.includes("Unknown config key"));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("--json returns reset confirmation", async () => {
|
|
181
|
+
const { stdout, exitCode } = await runCli(["config", "reset", "rpc-url", "--json"]);
|
|
182
|
+
assert.equal(exitCode, 0);
|
|
183
|
+
const data = JSON.parse(stdout);
|
|
184
|
+
assert.equal(data.key, "rpc-url");
|
|
185
|
+
assert.equal(data.reset, true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ─── config --help ────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe("config --help", () => {
|
|
192
|
+
it("shows subcommands", async () => {
|
|
193
|
+
const { stdout, exitCode } = await runCli(["config", "--help"]);
|
|
194
|
+
assert.equal(exitCode, 0);
|
|
195
|
+
assert.ok(stdout.includes("get"));
|
|
196
|
+
assert.ok(stdout.includes("set"));
|
|
197
|
+
assert.ok(stdout.includes("reset"));
|
|
198
|
+
});
|
|
199
|
+
});
|
package/src/tests/helpers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { join, dirname } from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
+
import type { Connection } from "@solana/web3.js";
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const CLI = join(__dirname, "../../bin/nara-cli.ts");
|
|
@@ -57,3 +58,22 @@ export const hasWallet = existsSync(join(homedir(), ".config", "nara", "id.json"
|
|
|
57
58
|
export function uniqueName(prefix: string): string {
|
|
58
59
|
return `${prefix}-${Date.now().toString(36)}`;
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Poll for transaction confirmation (avoids WebSocket-based confirmTransaction
|
|
64
|
+
* which fails with TLS errors on some networks).
|
|
65
|
+
*/
|
|
66
|
+
export async function pollConfirmation(
|
|
67
|
+
connection: Connection,
|
|
68
|
+
signature: string,
|
|
69
|
+
maxRetries = 30,
|
|
70
|
+
intervalMs = 1000
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
73
|
+
const { value } = await connection.getSignatureStatuses([signature]);
|
|
74
|
+
const status = value[0]?.confirmationStatus;
|
|
75
|
+
if (status === "confirmed" || status === "finalized") return;
|
|
76
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Transaction ${signature} not confirmed after ${maxRetries}s`);
|
|
79
|
+
}
|