@bfun-bot/cli 1.0.8 → 1.0.10
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/commands/llm.js +1 -1
- package/package.json +2 -1
- package/dist/__tests__/commands/balances.test.d.ts +0 -1
- package/dist/__tests__/commands/balances.test.js +0 -97
- package/dist/__tests__/commands/fees.test.d.ts +0 -1
- package/dist/__tests__/commands/fees.test.js +0 -220
- package/dist/__tests__/commands/quota.test.d.ts +0 -1
- package/dist/__tests__/commands/quota.test.js +0 -104
- package/dist/__tests__/lib/api.test.d.ts +0 -1
- package/dist/__tests__/lib/api.test.js +0 -77
- package/dist/__tests__/lib/display.test.d.ts +0 -1
- package/dist/__tests__/lib/display.test.js +0 -210
package/dist/commands/llm.js
CHANGED
|
@@ -49,7 +49,7 @@ export function registerLlm(program) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
printCard('BFun.bot Credits', [
|
|
52
|
-
['Balance', chalk.bold.white(`$${res.balance_usd}`)],
|
|
52
|
+
['Balance', chalk.reset.bold.white(`$${res.balance_usd}`)],
|
|
53
53
|
]);
|
|
54
54
|
// Show agent reload config
|
|
55
55
|
if (res.agent_reload) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bfun-bot/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "BFunBot CLI — deploy tokens, check balances, and manage your AI agent from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
|
+
"!dist/__tests__",
|
|
13
14
|
"bin",
|
|
14
15
|
"scripts"
|
|
15
16
|
],
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for bfunbot balances command.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
vi.mock('../../lib/api.js', () => ({
|
|
7
|
-
agent: {
|
|
8
|
-
walletBalance: vi.fn(),
|
|
9
|
-
},
|
|
10
|
-
handleApiError: vi.fn((err) => { throw err; }),
|
|
11
|
-
}));
|
|
12
|
-
import { agent } from '../../lib/api.js';
|
|
13
|
-
import { registerBalances } from '../../commands/balances.js';
|
|
14
|
-
const MOCK_BALANCE = {
|
|
15
|
-
evm_main: {
|
|
16
|
-
address: '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
17
|
-
balance_bnb: '0.412',
|
|
18
|
-
balance_usdt_bsc: '100.5',
|
|
19
|
-
bnb_error: null,
|
|
20
|
-
usdt_bsc_error: null,
|
|
21
|
-
},
|
|
22
|
-
evm_trading: {
|
|
23
|
-
address: '0xDeaD000000000000000042069420694206942069',
|
|
24
|
-
balance_bnb: '0.050',
|
|
25
|
-
balance_usdt_bsc: '0',
|
|
26
|
-
bnb_error: null,
|
|
27
|
-
usdt_bsc_error: null,
|
|
28
|
-
},
|
|
29
|
-
bnb_price_usd: 600,
|
|
30
|
-
};
|
|
31
|
-
function buildProgram() {
|
|
32
|
-
const program = new Command();
|
|
33
|
-
program.option('--json');
|
|
34
|
-
registerBalances(program);
|
|
35
|
-
return program;
|
|
36
|
-
}
|
|
37
|
-
describe('bfunbot balances', () => {
|
|
38
|
-
let output;
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
output = [];
|
|
41
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
42
|
-
output.push(args.map(String).join(' '));
|
|
43
|
-
});
|
|
44
|
-
vi.mocked(agent.walletBalance).mockResolvedValue(MOCK_BALANCE);
|
|
45
|
-
});
|
|
46
|
-
afterEach(() => vi.restoreAllMocks());
|
|
47
|
-
it('shows EVM Main wallet label', async () => {
|
|
48
|
-
const program = buildProgram();
|
|
49
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
50
|
-
expect(output.join('\n')).toContain('EVM Main');
|
|
51
|
-
});
|
|
52
|
-
it('shows EVM Trading wallet label', async () => {
|
|
53
|
-
const program = buildProgram();
|
|
54
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
55
|
-
expect(output.join('\n')).toContain('EVM Trading');
|
|
56
|
-
});
|
|
57
|
-
it('shows BNB balance', async () => {
|
|
58
|
-
const program = buildProgram();
|
|
59
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
60
|
-
expect(output.join('\n')).toContain('0.412');
|
|
61
|
-
});
|
|
62
|
-
it('shows USD equivalent alongside BNB', async () => {
|
|
63
|
-
const program = buildProgram();
|
|
64
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
65
|
-
expect(output.join('\n')).toContain('$');
|
|
66
|
-
});
|
|
67
|
-
it('shows USDT balance when non-zero', async () => {
|
|
68
|
-
const program = buildProgram();
|
|
69
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
70
|
-
expect(output.join('\n')).toContain('100.5');
|
|
71
|
-
});
|
|
72
|
-
it('shows error indicator when bnb_error is set', async () => {
|
|
73
|
-
vi.mocked(agent.walletBalance).mockResolvedValue({
|
|
74
|
-
...MOCK_BALANCE,
|
|
75
|
-
evm_main: { ...MOCK_BALANCE.evm_main, bnb_error: 'rpc_unavailable' },
|
|
76
|
-
});
|
|
77
|
-
const stderrOutput = [];
|
|
78
|
-
vi.spyOn(console, 'error').mockImplementation((...args) => stderrOutput.push(args.map(String).join(' ')));
|
|
79
|
-
const program = buildProgram();
|
|
80
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
81
|
-
expect(output.join('\n')).toContain('error');
|
|
82
|
-
});
|
|
83
|
-
it('shows truncated address', async () => {
|
|
84
|
-
const program = buildProgram();
|
|
85
|
-
await program.parseAsync(['node', 'bfunbot', 'balances']);
|
|
86
|
-
expect(output.join('\n')).toContain('...');
|
|
87
|
-
});
|
|
88
|
-
it('outputs JSON when --json flag is passed', async () => {
|
|
89
|
-
const program = buildProgram();
|
|
90
|
-
await program.parseAsync(['node', 'bfunbot', '--json', 'balances']);
|
|
91
|
-
const jsonLine = output.find(l => l.trim().startsWith('{'));
|
|
92
|
-
expect(jsonLine).toBeDefined();
|
|
93
|
-
const parsed = JSON.parse(jsonLine);
|
|
94
|
-
expect(parsed.evm_main.balance_bnb).toBe('0.412');
|
|
95
|
-
expect(parsed.bnb_price_usd).toBe(600);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for bfunbot fees command.
|
|
3
|
-
*
|
|
4
|
-
* Mocks the agent API and asserts stdout output contains expected values.
|
|
5
|
-
* Does NOT assert exact formatting strings — those are covered in display.test.ts.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
-
import { Command } from 'commander';
|
|
9
|
-
// Mock the API module before importing the command
|
|
10
|
-
vi.mock('../../lib/api.js', () => ({
|
|
11
|
-
agent: {
|
|
12
|
-
feesSummary: vi.fn(),
|
|
13
|
-
feesEarnings: vi.fn(),
|
|
14
|
-
feesToken: vi.fn(),
|
|
15
|
-
},
|
|
16
|
-
handleApiError: vi.fn((err) => { throw err; }),
|
|
17
|
-
}));
|
|
18
|
-
import { agent } from '../../lib/api.js';
|
|
19
|
-
import { registerFees } from '../../commands/fees.js';
|
|
20
|
-
const MOCK_SUMMARY = {
|
|
21
|
-
bsc: { token_count: 3, total_earned_bnb: 0.00412 },
|
|
22
|
-
bnb_price_usd: 600,
|
|
23
|
-
};
|
|
24
|
-
const MOCK_EARNINGS = {
|
|
25
|
-
chain: 'bsc',
|
|
26
|
-
flap: { total_earned_bnb: 0.00300, earning_token_count: 2 },
|
|
27
|
-
fourmeme: { total_earned_bnb: 0.00112, earning_token_count: 1 },
|
|
28
|
-
};
|
|
29
|
-
const MOCK_TOKEN_BSC = {
|
|
30
|
-
token_address: '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
31
|
-
token_name: 'TestToken',
|
|
32
|
-
token_symbol: 'TEST',
|
|
33
|
-
platform: 'flap',
|
|
34
|
-
chain: 'bsc',
|
|
35
|
-
earned_bnb: 0.00300,
|
|
36
|
-
};
|
|
37
|
-
function buildProgram() {
|
|
38
|
-
const program = new Command();
|
|
39
|
-
program.option('--json');
|
|
40
|
-
registerFees(program);
|
|
41
|
-
return program;
|
|
42
|
-
}
|
|
43
|
-
describe('bfunbot fees (summary)', () => {
|
|
44
|
-
let output;
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
output = [];
|
|
47
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
48
|
-
output.push(args.map(String).join(' '));
|
|
49
|
-
});
|
|
50
|
-
vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
|
|
51
|
-
});
|
|
52
|
-
afterEach(() => vi.restoreAllMocks());
|
|
53
|
-
it('shows total earned BNB', async () => {
|
|
54
|
-
const program = buildProgram();
|
|
55
|
-
await program.parseAsync(['node', 'bfunbot', 'fees']);
|
|
56
|
-
const all = output.join('\n');
|
|
57
|
-
expect(all).toContain('BNB');
|
|
58
|
-
expect(all).toContain('0.00412');
|
|
59
|
-
});
|
|
60
|
-
it('shows USD value when bnb_price_usd is provided', async () => {
|
|
61
|
-
const program = buildProgram();
|
|
62
|
-
await program.parseAsync(['node', 'bfunbot', 'fees']);
|
|
63
|
-
const all = output.join('\n');
|
|
64
|
-
expect(all).toContain('$');
|
|
65
|
-
});
|
|
66
|
-
it('shows hint to run fees breakdown', async () => {
|
|
67
|
-
const program = buildProgram();
|
|
68
|
-
await program.parseAsync(['node', 'bfunbot', 'fees']);
|
|
69
|
-
expect(output.join('\n')).toContain('breakdown');
|
|
70
|
-
});
|
|
71
|
-
it('outputs JSON when --json flag is passed', async () => {
|
|
72
|
-
const program = buildProgram();
|
|
73
|
-
await program.parseAsync(['node', 'bfunbot', '--json', 'fees']);
|
|
74
|
-
const jsonLine = output.find(l => l.trim().startsWith('{'));
|
|
75
|
-
expect(jsonLine).toBeDefined();
|
|
76
|
-
const parsed = JSON.parse(jsonLine);
|
|
77
|
-
expect(parsed.bsc.total_earned_bnb).toBe(0.00412);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('bfunbot fees breakdown', () => {
|
|
81
|
-
let output;
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
output = [];
|
|
84
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
85
|
-
output.push(args.map(String).join(' '));
|
|
86
|
-
});
|
|
87
|
-
vi.mocked(agent.feesEarnings).mockResolvedValue(MOCK_EARNINGS);
|
|
88
|
-
vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
|
|
89
|
-
});
|
|
90
|
-
afterEach(() => vi.restoreAllMocks());
|
|
91
|
-
it('still works when feesSummary fails (graceful fallback — no USD)', async () => {
|
|
92
|
-
vi.mocked(agent.feesSummary).mockRejectedValue(new Error('summary unavailable'));
|
|
93
|
-
const program = buildProgram();
|
|
94
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
|
|
95
|
-
const all = output.join('\n');
|
|
96
|
-
expect(all).toContain('flap');
|
|
97
|
-
expect(all).toContain('fourmeme');
|
|
98
|
-
});
|
|
99
|
-
it('does not call feesSummary when --json flag is used', async () => {
|
|
100
|
-
vi.mocked(agent.feesSummary).mockClear();
|
|
101
|
-
const program = buildProgram();
|
|
102
|
-
await program.parseAsync(['node', 'bfunbot', '--json', 'fees', 'breakdown']);
|
|
103
|
-
expect(agent.feesSummary).not.toHaveBeenCalled();
|
|
104
|
-
});
|
|
105
|
-
it('shows flap and fourmeme platforms', async () => {
|
|
106
|
-
const program = buildProgram();
|
|
107
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
|
|
108
|
-
const all = output.join('\n');
|
|
109
|
-
expect(all).toContain('flap');
|
|
110
|
-
expect(all).toContain('fourmeme');
|
|
111
|
-
});
|
|
112
|
-
it('shows BNB earned for both platforms', async () => {
|
|
113
|
-
const program = buildProgram();
|
|
114
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
|
|
115
|
-
const all = output.join('\n');
|
|
116
|
-
expect(all).toContain('0.003');
|
|
117
|
-
expect(all).toContain('0.00112');
|
|
118
|
-
});
|
|
119
|
-
it('shows USD column', async () => {
|
|
120
|
-
const program = buildProgram();
|
|
121
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
|
|
122
|
-
expect(output.join('\n')).toContain('$');
|
|
123
|
-
});
|
|
124
|
-
it('shows token counts', async () => {
|
|
125
|
-
const program = buildProgram();
|
|
126
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
|
|
127
|
-
const all = output.join('\n');
|
|
128
|
-
expect(all).toContain('2');
|
|
129
|
-
expect(all).toContain('1');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe('bfunbot fees --token --platform', () => {
|
|
133
|
-
let output;
|
|
134
|
-
beforeEach(() => {
|
|
135
|
-
output = [];
|
|
136
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
137
|
-
output.push(args.map(String).join(' '));
|
|
138
|
-
});
|
|
139
|
-
vi.mocked(agent.feesToken).mockResolvedValue(MOCK_TOKEN_BSC);
|
|
140
|
-
vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
|
|
141
|
-
});
|
|
142
|
-
afterEach(() => vi.restoreAllMocks());
|
|
143
|
-
it('shows token name and symbol', async () => {
|
|
144
|
-
const program = buildProgram();
|
|
145
|
-
await program.parseAsync([
|
|
146
|
-
'node', 'bfunbot', 'fees',
|
|
147
|
-
'--platform', 'flap',
|
|
148
|
-
'--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
149
|
-
]);
|
|
150
|
-
const all = output.join('\n');
|
|
151
|
-
expect(all).toContain('TestToken');
|
|
152
|
-
expect(all).toContain('TEST');
|
|
153
|
-
});
|
|
154
|
-
it('shows earned BNB with USD', async () => {
|
|
155
|
-
const program = buildProgram();
|
|
156
|
-
await program.parseAsync([
|
|
157
|
-
'node', 'bfunbot', 'fees',
|
|
158
|
-
'--platform', 'flap',
|
|
159
|
-
'--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
160
|
-
]);
|
|
161
|
-
const all = output.join('\n');
|
|
162
|
-
expect(all).toContain('BNB');
|
|
163
|
-
expect(all).toContain('$');
|
|
164
|
-
});
|
|
165
|
-
it('errors if --token given without --platform', async () => {
|
|
166
|
-
const stderrOutput = [];
|
|
167
|
-
vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
168
|
-
stderrOutput.push(args.map(String).join(' '));
|
|
169
|
-
});
|
|
170
|
-
vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
171
|
-
throw new Error('process.exit(1)');
|
|
172
|
-
}));
|
|
173
|
-
const program = buildProgram();
|
|
174
|
-
await expect(program.parseAsync(['node', 'bfunbot', 'fees', '--token', '0xabc123'])).rejects.toThrow('process.exit(1)');
|
|
175
|
-
expect(stderrOutput[0]).toContain('--platform');
|
|
176
|
-
});
|
|
177
|
-
it('still works when feesSummary fails for per-token (graceful fallback)', async () => {
|
|
178
|
-
vi.mocked(agent.feesSummary).mockRejectedValue(new Error('summary unavailable'));
|
|
179
|
-
const program = buildProgram();
|
|
180
|
-
await program.parseAsync([
|
|
181
|
-
'node', 'bfunbot', 'fees',
|
|
182
|
-
'--platform', 'flap',
|
|
183
|
-
'--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
184
|
-
]);
|
|
185
|
-
const all = output.join('\n');
|
|
186
|
-
expect(all).toContain('TestToken');
|
|
187
|
-
expect(all).toContain('BNB');
|
|
188
|
-
});
|
|
189
|
-
it('does not call feesSummary for --token when --json flag is used', async () => {
|
|
190
|
-
vi.mocked(agent.feesSummary).mockClear();
|
|
191
|
-
const program = buildProgram();
|
|
192
|
-
await program.parseAsync([
|
|
193
|
-
'node', 'bfunbot', '--json', 'fees',
|
|
194
|
-
'--platform', 'flap',
|
|
195
|
-
'--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
|
|
196
|
-
]);
|
|
197
|
-
expect(agent.feesSummary).not.toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
it('routes --platform alone to breakdown with deprecation hint', async () => {
|
|
200
|
-
vi.mocked(agent.feesEarnings).mockResolvedValue(MOCK_EARNINGS);
|
|
201
|
-
const program = buildProgram();
|
|
202
|
-
await program.parseAsync(['node', 'bfunbot', 'fees', '--platform', 'flap']);
|
|
203
|
-
const all = output.join('\n');
|
|
204
|
-
expect(all).toContain('flap');
|
|
205
|
-
expect(all).toContain('fourmeme');
|
|
206
|
-
expect(all).toContain('fees breakdown');
|
|
207
|
-
});
|
|
208
|
-
it('errors if invalid platform given', async () => {
|
|
209
|
-
const stderrOutput = [];
|
|
210
|
-
vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
211
|
-
stderrOutput.push(args.map(String).join(' '));
|
|
212
|
-
});
|
|
213
|
-
vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
214
|
-
throw new Error('process.exit(1)');
|
|
215
|
-
}));
|
|
216
|
-
const program = buildProgram();
|
|
217
|
-
await expect(program.parseAsync(['node', 'bfunbot', 'fees', '--platform', 'invalid', '--token', '0xabc'])).rejects.toThrow('process.exit(1)');
|
|
218
|
-
expect(stderrOutput[0]).toContain('Platform must be one of');
|
|
219
|
-
});
|
|
220
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for bfunbot quota command.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
vi.mock('../../lib/api.js', () => ({
|
|
7
|
-
agent: {
|
|
8
|
-
quota: vi.fn(),
|
|
9
|
-
},
|
|
10
|
-
handleApiError: vi.fn((err) => { throw err; }),
|
|
11
|
-
}));
|
|
12
|
-
import { agent } from '../../lib/api.js';
|
|
13
|
-
import { registerQuota } from '../../commands/quota.js';
|
|
14
|
-
const MOCK_QUOTA = {
|
|
15
|
-
chains: [
|
|
16
|
-
{
|
|
17
|
-
chain: 'bsc',
|
|
18
|
-
free_used_today: 2,
|
|
19
|
-
free_limit: 5,
|
|
20
|
-
sponsored_remaining: 3,
|
|
21
|
-
can_create_paid: true,
|
|
22
|
-
trading_wallet_balance: '0.050000 BNB',
|
|
23
|
-
trading_wallet_address: '0xDeaD000000000000000042069420694206942069',
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
function buildProgram() {
|
|
28
|
-
const program = new Command();
|
|
29
|
-
program.option('--json');
|
|
30
|
-
registerQuota(program);
|
|
31
|
-
return program;
|
|
32
|
-
}
|
|
33
|
-
describe('bfunbot quota', () => {
|
|
34
|
-
let output;
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
output = [];
|
|
37
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
38
|
-
output.push(args.map(String).join(' '));
|
|
39
|
-
});
|
|
40
|
-
vi.mocked(agent.quota).mockResolvedValue(MOCK_QUOTA);
|
|
41
|
-
});
|
|
42
|
-
afterEach(() => vi.restoreAllMocks());
|
|
43
|
-
it('shows chain name', async () => {
|
|
44
|
-
const program = buildProgram();
|
|
45
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
46
|
-
expect(output.join('\n')).toContain('Bsc');
|
|
47
|
-
});
|
|
48
|
-
it('shows free used and limit', async () => {
|
|
49
|
-
const program = buildProgram();
|
|
50
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
51
|
-
const all = output.join('\n');
|
|
52
|
-
expect(all).toContain('2');
|
|
53
|
-
expect(all).toContain('5');
|
|
54
|
-
});
|
|
55
|
-
it('shows remaining quota count', async () => {
|
|
56
|
-
const program = buildProgram();
|
|
57
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
58
|
-
expect(output.join('\n')).toContain('3');
|
|
59
|
-
});
|
|
60
|
-
it('shows Yes for can_create_paid=true', async () => {
|
|
61
|
-
const program = buildProgram();
|
|
62
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
63
|
-
expect(output.join('\n')).toContain('Yes');
|
|
64
|
-
});
|
|
65
|
-
it('shows No only when both free remaining and can_create_paid are false/zero', async () => {
|
|
66
|
-
vi.mocked(agent.quota).mockResolvedValue({
|
|
67
|
-
chains: [{ ...MOCK_QUOTA.chains[0], can_create_paid: false, sponsored_remaining: 0 }],
|
|
68
|
-
});
|
|
69
|
-
const program = buildProgram();
|
|
70
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
71
|
-
expect(output.join('\n')).toContain('No');
|
|
72
|
-
});
|
|
73
|
-
it('shows Yes when free remaining > 0 even if can_create_paid is false', async () => {
|
|
74
|
-
vi.mocked(agent.quota).mockResolvedValue({
|
|
75
|
-
chains: [{ ...MOCK_QUOTA.chains[0], can_create_paid: false, sponsored_remaining: 9 }],
|
|
76
|
-
});
|
|
77
|
-
const program = buildProgram();
|
|
78
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
79
|
-
expect(output.join('\n')).toContain('Yes');
|
|
80
|
-
});
|
|
81
|
-
it('table columns align (ANSI-aware)', async () => {
|
|
82
|
-
const program = buildProgram();
|
|
83
|
-
await program.parseAsync(['node', 'bfunbot', 'quota']);
|
|
84
|
-
const strip = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
85
|
-
// Find the header row (contains all column names)
|
|
86
|
-
const headerIdx = output.findIndex(l => strip(l).includes('Chain') && strip(l).includes('Used Today'));
|
|
87
|
-
expect(headerIdx).toBeGreaterThan(-1);
|
|
88
|
-
const headerLen = strip(output[headerIdx]).length;
|
|
89
|
-
// Data rows immediately follow the separator line — check they match header width
|
|
90
|
-
const separatorIdx = output.findIndex((l, i) => i > headerIdx && strip(l).includes('─'));
|
|
91
|
-
const dataRows = output.slice(separatorIdx + 1).filter(l => l.trim() !== '');
|
|
92
|
-
for (const row of dataRows) {
|
|
93
|
-
expect(strip(row).length).toBe(headerLen);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
it('outputs JSON when --json flag is passed', async () => {
|
|
97
|
-
const program = buildProgram();
|
|
98
|
-
await program.parseAsync(['node', 'bfunbot', '--json', 'quota']);
|
|
99
|
-
const jsonLine = output.find(l => l.trim().startsWith('{'));
|
|
100
|
-
expect(jsonLine).toBeDefined();
|
|
101
|
-
const parsed = JSON.parse(jsonLine);
|
|
102
|
-
expect(parsed.chains[0].chain).toBe('bsc');
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for src/lib/api.ts
|
|
3
|
-
*
|
|
4
|
-
* Covers: ApiError construction, handleApiError friendly messages,
|
|
5
|
-
* request() — correct headers, error mapping, network failure.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
-
import { ApiError, handleApiError } from '../../lib/api.js';
|
|
9
|
-
// ─── ApiError ────────────────────────────────────────────────────────────────
|
|
10
|
-
describe('ApiError', () => {
|
|
11
|
-
it('constructs with status code and message', () => {
|
|
12
|
-
const err = new ApiError(404, 'Not Found', { detail: 'Token not found' });
|
|
13
|
-
expect(err.statusCode).toBe(404);
|
|
14
|
-
expect(err.message).toBe('Token not found');
|
|
15
|
-
expect(err.name).toBe('ApiError');
|
|
16
|
-
});
|
|
17
|
-
it('falls back to statusText when no body detail', () => {
|
|
18
|
-
const err = new ApiError(500, 'Internal Server Error');
|
|
19
|
-
expect(err.message).toBe('Internal Server Error');
|
|
20
|
-
});
|
|
21
|
-
it('uses body.message if detail is absent', () => {
|
|
22
|
-
const err = new ApiError(400, 'Bad Request', { message: 'Bad input' });
|
|
23
|
-
expect(err.message).toBe('Bad input');
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
// ─── handleApiError friendly messages ────────────────────────────────────────
|
|
27
|
-
describe('handleApiError', () => {
|
|
28
|
-
let stderrOutput;
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
stderrOutput = [];
|
|
31
|
-
vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
32
|
-
stderrOutput.push(args.map(String).join(' '));
|
|
33
|
-
});
|
|
34
|
-
vi.spyOn(process, 'exit').mockImplementation((() => { }));
|
|
35
|
-
});
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
it('shows login hint for 401', () => {
|
|
40
|
-
handleApiError(new ApiError(401, 'Unauthorized'));
|
|
41
|
-
expect(stderrOutput[0]).toContain('bfunbot login');
|
|
42
|
-
});
|
|
43
|
-
it('shows access denied for 403', () => {
|
|
44
|
-
handleApiError(new ApiError(403, 'Forbidden'));
|
|
45
|
-
expect(stderrOutput[0]).toContain('Access denied');
|
|
46
|
-
});
|
|
47
|
-
it('shows 403 detail string if provided', () => {
|
|
48
|
-
handleApiError(new ApiError(403, 'Forbidden', { detail: 'IP not allowed' }));
|
|
49
|
-
expect(stderrOutput[0]).toContain('IP not allowed');
|
|
50
|
-
});
|
|
51
|
-
it('shows rate limit message for 429', () => {
|
|
52
|
-
handleApiError(new ApiError(429, 'Too Many Requests'));
|
|
53
|
-
expect(stderrOutput[0]).toContain('Rate limited');
|
|
54
|
-
});
|
|
55
|
-
it('shows server error for 500+', () => {
|
|
56
|
-
handleApiError(new ApiError(503, 'Service Unavailable'));
|
|
57
|
-
expect(stderrOutput[0]).toContain('Server error');
|
|
58
|
-
});
|
|
59
|
-
it('joins FastAPI 422 validation errors', () => {
|
|
60
|
-
handleApiError(new ApiError(422, 'Unprocessable Entity', {
|
|
61
|
-
detail: [
|
|
62
|
-
{ msg: 'field required', loc: ['body', 'name'], type: 'missing' },
|
|
63
|
-
{ msg: 'invalid value', loc: ['body', 'symbol'], type: 'value_error' },
|
|
64
|
-
],
|
|
65
|
-
}));
|
|
66
|
-
expect(stderrOutput[0]).toContain('field required');
|
|
67
|
-
expect(stderrOutput[0]).toContain('invalid value');
|
|
68
|
-
});
|
|
69
|
-
it('shows connection error for network TypeError', () => {
|
|
70
|
-
handleApiError(new TypeError('fetch failed', { cause: new Error('ECONNREFUSED') }));
|
|
71
|
-
expect(stderrOutput[0]).toContain('Could not reach');
|
|
72
|
-
});
|
|
73
|
-
it('calls process.exit(1)', () => {
|
|
74
|
-
handleApiError(new ApiError(401, 'Unauthorized'));
|
|
75
|
-
expect(process.exit).toHaveBeenCalledWith(1);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for src/lib/display.ts
|
|
3
|
-
*
|
|
4
|
-
* Covers: fmtBalance, fmtBnbWithUsd, fmtUsd, shortAddr, fmtDate, printTable alignment.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
import { fmtBalance, fmtBnbWithUsd, fmtUsd, shortAddr, fmtDate, printTable, } from '../../lib/display.js';
|
|
8
|
-
// Strip ANSI codes from output for readable assertions
|
|
9
|
-
const strip = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
10
|
-
// ─── fmtBalance ──────────────────────────────────────────────────────────────
|
|
11
|
-
describe('fmtBalance', () => {
|
|
12
|
-
it('formats a normal BNB value', () => {
|
|
13
|
-
expect(strip(fmtBalance('1.5', 'BNB'))).toBe('1.5 BNB');
|
|
14
|
-
});
|
|
15
|
-
it('trims trailing zeros', () => {
|
|
16
|
-
expect(strip(fmtBalance('1.500000', 'BNB'))).toBe('1.5 BNB');
|
|
17
|
-
});
|
|
18
|
-
it('formats a small value without scientific notation', () => {
|
|
19
|
-
const result = strip(fmtBalance('0.00000597', 'BNB'));
|
|
20
|
-
expect(result).not.toContain('e');
|
|
21
|
-
expect(result).toContain('0.00000597');
|
|
22
|
-
expect(result).toContain('BNB');
|
|
23
|
-
});
|
|
24
|
-
it('formats an extremely small value without scientific notation', () => {
|
|
25
|
-
const result = strip(fmtBalance('0.0000000012', 'BNB'));
|
|
26
|
-
expect(result).not.toContain('e');
|
|
27
|
-
expect(result).toContain('BNB');
|
|
28
|
-
});
|
|
29
|
-
it('returns dim zero for zero value', () => {
|
|
30
|
-
expect(strip(fmtBalance('0', 'BNB'))).toBe('0 BNB');
|
|
31
|
-
});
|
|
32
|
-
it('returns dim zero for null', () => {
|
|
33
|
-
expect(strip(fmtBalance(null, 'BNB'))).toBe('0 BNB');
|
|
34
|
-
});
|
|
35
|
-
it('returns dim zero for undefined', () => {
|
|
36
|
-
expect(strip(fmtBalance(undefined, 'BNB'))).toBe('0 BNB');
|
|
37
|
-
});
|
|
38
|
-
it('returns dim zero for "0.0"', () => {
|
|
39
|
-
expect(strip(fmtBalance('0.0', 'BNB'))).toBe('0 BNB');
|
|
40
|
-
});
|
|
41
|
-
it('formats USDT correctly', () => {
|
|
42
|
-
expect(strip(fmtBalance('100.5', 'USDT'))).toBe('100.5 USDT');
|
|
43
|
-
});
|
|
44
|
-
it('handles 6 decimal places without trailing zeros', () => {
|
|
45
|
-
expect(strip(fmtBalance('0.412000', 'BNB'))).toBe('0.412 BNB');
|
|
46
|
-
});
|
|
47
|
-
it('never shows 0 for a non-zero sub-1e-10 value', () => {
|
|
48
|
-
const result = strip(fmtBalance('0.00000000001', 'BNB'));
|
|
49
|
-
expect(result).not.toMatch(/^0 BNB/);
|
|
50
|
-
expect(result).not.toContain('e');
|
|
51
|
-
expect(result).toContain('BNB');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
// ─── fmtBnbWithUsd ───────────────────────────────────────────────────────────
|
|
55
|
-
describe('fmtBnbWithUsd', () => {
|
|
56
|
-
it('appends USD value in parentheses', () => {
|
|
57
|
-
const result = strip(fmtBnbWithUsd('1.0', 600));
|
|
58
|
-
expect(result).toContain('1 BNB');
|
|
59
|
-
expect(result).toContain('~$600.00');
|
|
60
|
-
});
|
|
61
|
-
it('calculates USD correctly for fractional BNB', () => {
|
|
62
|
-
const result = strip(fmtBnbWithUsd('0.5', 600));
|
|
63
|
-
expect(result).toContain('~$300.00');
|
|
64
|
-
});
|
|
65
|
-
it('returns plain BNB when price is 0', () => {
|
|
66
|
-
const result = strip(fmtBnbWithUsd('1.0', 0));
|
|
67
|
-
expect(result).toContain('BNB');
|
|
68
|
-
expect(result).not.toContain('$');
|
|
69
|
-
});
|
|
70
|
-
it('returns plain BNB for null value', () => {
|
|
71
|
-
const result = strip(fmtBnbWithUsd(null, 600));
|
|
72
|
-
expect(result).not.toContain('$');
|
|
73
|
-
});
|
|
74
|
-
it('returns plain BNB for zero value', () => {
|
|
75
|
-
const result = strip(fmtBnbWithUsd('0', 600));
|
|
76
|
-
expect(result).not.toContain('$');
|
|
77
|
-
});
|
|
78
|
-
it('handles very small BNB with USD', () => {
|
|
79
|
-
const result = strip(fmtBnbWithUsd('0.00001', 600));
|
|
80
|
-
expect(result).not.toContain('e');
|
|
81
|
-
expect(result).toContain('BNB');
|
|
82
|
-
expect(result).toContain('~$');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
// ─── fmtUsd ──────────────────────────────────────────────────────────────────
|
|
86
|
-
describe('fmtUsd', () => {
|
|
87
|
-
it('formats values >= $1 to 2 decimal places', () => {
|
|
88
|
-
expect(strip(fmtUsd('12.5'))).toBe('$12.50');
|
|
89
|
-
});
|
|
90
|
-
it('formats zero with em dash', () => {
|
|
91
|
-
expect(strip(fmtUsd('0'))).toBe('—');
|
|
92
|
-
});
|
|
93
|
-
it('formats null with em dash', () => {
|
|
94
|
-
expect(strip(fmtUsd(null))).toBe('—');
|
|
95
|
-
});
|
|
96
|
-
it('formats undefined with em dash', () => {
|
|
97
|
-
expect(strip(fmtUsd(undefined))).toBe('—');
|
|
98
|
-
});
|
|
99
|
-
it('formats small values with enough significant digits', () => {
|
|
100
|
-
const result = strip(fmtUsd('0.000123'));
|
|
101
|
-
expect(result).toContain('$');
|
|
102
|
-
expect(result).toContain('0.000123');
|
|
103
|
-
});
|
|
104
|
-
it('formats large USD values correctly', () => {
|
|
105
|
-
expect(strip(fmtUsd('1234.567'))).toBe('$1234.57');
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
// ─── shortAddr ───────────────────────────────────────────────────────────────
|
|
109
|
-
describe('shortAddr', () => {
|
|
110
|
-
it('truncates a long address', () => {
|
|
111
|
-
const addr = '0xB305ce8D75498E4D0c2963089C4894b3589F7777';
|
|
112
|
-
const result = strip(shortAddr(addr));
|
|
113
|
-
expect(result).toContain('0xB305ce');
|
|
114
|
-
expect(result).toContain('...');
|
|
115
|
-
expect(result).toContain('F7777');
|
|
116
|
-
expect(result.length).toBeLessThan(addr.length);
|
|
117
|
-
});
|
|
118
|
-
it('returns em dash for null', () => {
|
|
119
|
-
expect(strip(shortAddr(null))).toBe('—');
|
|
120
|
-
});
|
|
121
|
-
it('returns em dash for undefined', () => {
|
|
122
|
-
expect(strip(shortAddr(undefined))).toBe('—');
|
|
123
|
-
});
|
|
124
|
-
it('returns short address as-is', () => {
|
|
125
|
-
expect(strip(shortAddr('0x1234'))).toBe('0x1234');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
// ─── fmtDate ─────────────────────────────────────────────────────────────────
|
|
129
|
-
describe('fmtDate', () => {
|
|
130
|
-
it('formats a valid ISO date string', () => {
|
|
131
|
-
const result = strip(fmtDate('2024-01-15T10:30:00Z'));
|
|
132
|
-
expect(result).toContain('2024');
|
|
133
|
-
expect(result).toContain('Jan');
|
|
134
|
-
expect(result).toContain('15');
|
|
135
|
-
});
|
|
136
|
-
it('returns em dash for null', () => {
|
|
137
|
-
expect(strip(fmtDate(null))).toBe('—');
|
|
138
|
-
});
|
|
139
|
-
it('returns em dash for undefined', () => {
|
|
140
|
-
expect(strip(fmtDate(undefined))).toBe('—');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
// ─── printTable alignment ────────────────────────────────────────────────────
|
|
144
|
-
describe('printTable', () => {
|
|
145
|
-
let output;
|
|
146
|
-
beforeEach(() => {
|
|
147
|
-
output = [];
|
|
148
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
149
|
-
output.push(args.map(String).join(' '));
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
afterEach(() => {
|
|
153
|
-
vi.restoreAllMocks();
|
|
154
|
-
});
|
|
155
|
-
it('produces consistent column widths with plain text', () => {
|
|
156
|
-
printTable(['Name', 'Value', 'Status'], [
|
|
157
|
-
['Alice', '100', 'active'],
|
|
158
|
-
['Bob', '9999', 'inactive'],
|
|
159
|
-
]);
|
|
160
|
-
const rows = output.filter(l => l.trim() && !l.includes('─'));
|
|
161
|
-
// All rows should have the same length (same column widths)
|
|
162
|
-
const header = strip(rows[0]);
|
|
163
|
-
const row1 = strip(rows[1]);
|
|
164
|
-
const row2 = strip(rows[2]);
|
|
165
|
-
expect(row1.length).toBe(header.length);
|
|
166
|
-
expect(row2.length).toBe(header.length);
|
|
167
|
-
});
|
|
168
|
-
it('correctly aligns columns when cells contain chalk colors (ANSI codes)', async () => {
|
|
169
|
-
const { default: chalk } = await import('chalk');
|
|
170
|
-
printTable(['Chain', 'Used Today', 'Can Create'], [
|
|
171
|
-
[chalk.bold('BSC'), '5', chalk.green('Yes')],
|
|
172
|
-
[chalk.bold('ETH'), '10', chalk.dim('No')],
|
|
173
|
-
]);
|
|
174
|
-
const rows = output.filter(l => l.trim() && !l.includes('─'));
|
|
175
|
-
// Visible (stripped) lengths of all rows must be equal
|
|
176
|
-
const lengths = rows.map(r => strip(r).length);
|
|
177
|
-
expect(lengths.every(l => l === lengths[0])).toBe(true);
|
|
178
|
-
});
|
|
179
|
-
it('correctly handles CJK wide characters in column width calculation', async () => {
|
|
180
|
-
printTable(['Name', 'Symbol'], [
|
|
181
|
-
['普通Token', 'ABC'], // CJK chars — each is 2 columns wide
|
|
182
|
-
['NormalName', 'XYZ'],
|
|
183
|
-
]);
|
|
184
|
-
// Find separator line to locate data rows
|
|
185
|
-
const sepIdx = output.findIndex(l => strip(l).includes('─'));
|
|
186
|
-
const dataRows = output.slice(sepIdx + 1).filter(l => l.trim() !== '');
|
|
187
|
-
// Both data rows must have the same visible terminal width
|
|
188
|
-
const visW = (s) => {
|
|
189
|
-
let w = 0;
|
|
190
|
-
for (const ch of strip(s).trimEnd()) {
|
|
191
|
-
const cp = ch.codePointAt(0) ?? 0;
|
|
192
|
-
w += (cp >= 0x4E00 && cp <= 0x9FFF) ? 2 : 1;
|
|
193
|
-
}
|
|
194
|
-
return w;
|
|
195
|
-
};
|
|
196
|
-
const lengths = dataRows.map(r => visW(r));
|
|
197
|
-
expect(lengths).toHaveLength(2);
|
|
198
|
-
expect(lengths[0]).toBe(lengths[1]);
|
|
199
|
-
});
|
|
200
|
-
it('handles empty rows gracefully', () => {
|
|
201
|
-
expect(() => printTable(['Col A', 'Col B'], [])).not.toThrow();
|
|
202
|
-
});
|
|
203
|
-
it('renders headers and separator', () => {
|
|
204
|
-
printTable(['Symbol', 'Name'], [['BTC', 'Bitcoin']]);
|
|
205
|
-
const allOutput = output.join('\n');
|
|
206
|
-
expect(allOutput).toContain('Symbol');
|
|
207
|
-
expect(allOutput).toContain('Name');
|
|
208
|
-
expect(allOutput).toContain('─');
|
|
209
|
-
});
|
|
210
|
-
});
|