agentbnb 8.4.2 → 8.4.4
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/{chunk-Z5726VPY.js → chunk-EKLVNIIY.js} +1 -1
- package/dist/{chunk-6XCN62YU.js → chunk-IMLFBU3H.js} +5 -3
- package/dist/{chunk-RVBW2QXU.js → chunk-J46N2TCC.js} +5 -3
- package/dist/{chunk-XGOA5J2K.js → chunk-RJNKX347.js} +100 -3
- package/dist/{chunk-UF6R2RVN.js → chunk-SME5LJTE.js} +1 -1
- package/dist/cli/index.js +12 -12
- package/dist/{conduct-FZPUMPCI.js → conduct-2RD45QKB.js} +3 -3
- package/dist/{conduct-J2NXU6RM.js → conduct-TE4YAXKR.js} +3 -3
- package/dist/{conductor-mode-OPGQJFLA.js → conductor-mode-PXTMYGK5.js} +1 -1
- package/dist/{conductor-mode-HNNMWZIH.js → conductor-mode-TLIQMU4A.js} +2 -2
- package/dist/index.js +5 -3
- package/dist/{openclaw-setup-KA72IIEW.js → openclaw-setup-LVSGMXDF.js} +88 -14
- package/dist/{openclaw-skills-CT673RBL.js → openclaw-skills-6ZWQJ5V6.js} +130 -95
- package/dist/{request-IM3ZLAOA.js → request-XWEOIVB3.js} +1 -1
- package/dist/scanner-GP4AOCW6.js +16 -0
- package/dist/{server-O77TDLDU.js → server-ZUUJT5QC.js} +7 -6
- package/dist/{service-coordinator-R5LZVM6A.js → service-coordinator-2HDVHDFD.js} +1 -1
- package/dist/skills/agentbnb/bootstrap.js +6 -5
- package/package.json +23 -21
- package/skills/agentbnb/install.sh +0 -0
- package/dist/scanner-F5VNV5FP.js +0 -8
- package/skills/agentbnb/bootstrap.test.ts +0 -323
- package/skills/agentbnb/openclaw-tools.test.ts +0 -328
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for bootstrap.ts thin OpenClaw adapter.
|
|
3
|
-
*
|
|
4
|
-
* bootstrap.ts is a thin adapter — it delegates all lifecycle logic to
|
|
5
|
-
* ServiceCoordinator via AgentBnBService. Tests verify the adapter's own
|
|
6
|
-
* responsibilities:
|
|
7
|
-
* - CONFIG_NOT_FOUND when no config exists
|
|
8
|
-
* - BootstrapContext shape (service, status, startDisposition)
|
|
9
|
-
* - Signal handler registration and removal
|
|
10
|
-
* - deactivate() only stops node when startDisposition === 'started'
|
|
11
|
-
* - deactivate() is idempotent
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Mocks — must be hoisted before imports
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const mockEnsureRunning = vi.fn<() => Promise<'started' | 'already_running'>>();
|
|
23
|
-
const mockGetNodeStatus = vi.fn();
|
|
24
|
-
const mockStop = vi.fn<() => Promise<void>>();
|
|
25
|
-
|
|
26
|
-
vi.mock('../../src/app/agentbnb-service.js', () => ({
|
|
27
|
-
AgentBnBService: vi.fn().mockImplementation(() => ({
|
|
28
|
-
ensureRunning: mockEnsureRunning,
|
|
29
|
-
getNodeStatus: mockGetNodeStatus,
|
|
30
|
-
stop: mockStop,
|
|
31
|
-
})),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
vi.mock('../../src/runtime/service-coordinator.js', () => ({
|
|
35
|
-
ServiceCoordinator: vi.fn().mockImplementation(() => ({})),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
vi.mock('../../src/runtime/process-guard.js', () => ({
|
|
39
|
-
ProcessGuard: vi.fn().mockImplementation(() => ({})),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
vi.mock('../../src/cli/config.js', () => ({
|
|
43
|
-
loadConfig: vi.fn(),
|
|
44
|
-
getConfigDir: vi.fn(() => join(homedir(), '.agentbnb')),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
vi.mock('../../src/registry/store.js', () => ({
|
|
48
|
-
openDatabase: vi.fn(() => ({
|
|
49
|
-
prepare: vi.fn(() => ({
|
|
50
|
-
get: vi.fn(() => ({ id: 'existing-card' })),
|
|
51
|
-
run: vi.fn(),
|
|
52
|
-
})),
|
|
53
|
-
})),
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
import { loadConfig } from '../../src/cli/config.js';
|
|
57
|
-
import { activate, deactivate } from './bootstrap.js';
|
|
58
|
-
import bootstrapDefault from './bootstrap.js';
|
|
59
|
-
import type { BootstrapContext, OnboardDeps } from './bootstrap.js';
|
|
60
|
-
|
|
61
|
-
const mockLoadConfig = vi.mocked(loadConfig);
|
|
62
|
-
|
|
63
|
-
const MINIMAL_CONFIG = {
|
|
64
|
-
owner: 'test-agent',
|
|
65
|
-
gateway_url: 'http://localhost:7700',
|
|
66
|
-
gateway_port: 7700,
|
|
67
|
-
db_path: ':memory:',
|
|
68
|
-
credit_db_path: ':memory:',
|
|
69
|
-
token: 'test-token',
|
|
70
|
-
api_key: 'test-api-key',
|
|
71
|
-
registry: 'https://agentbnb.fly.dev',
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const MOCK_STATUS = {
|
|
75
|
-
state: 'running' as const,
|
|
76
|
-
pid: 1234,
|
|
77
|
-
port: 7700,
|
|
78
|
-
owner: 'test-agent',
|
|
79
|
-
relayConnected: false,
|
|
80
|
-
uptime_ms: 100,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Test suite
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
describe('bootstrap activate/deactivate lifecycle', () => {
|
|
88
|
-
let ctx: BootstrapContext | undefined;
|
|
89
|
-
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
vi.clearAllMocks();
|
|
92
|
-
mockEnsureRunning.mockResolvedValue('started');
|
|
93
|
-
mockGetNodeStatus.mockResolvedValue(MOCK_STATUS);
|
|
94
|
-
mockStop.mockResolvedValue(undefined);
|
|
95
|
-
mockLoadConfig.mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
afterEach(async () => {
|
|
99
|
-
if (ctx) {
|
|
100
|
-
await deactivate(ctx).catch(() => undefined);
|
|
101
|
-
ctx = undefined;
|
|
102
|
-
}
|
|
103
|
-
process.removeAllListeners('SIGTERM');
|
|
104
|
-
process.removeAllListeners('SIGINT');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Test 1: INIT_FAILED when CLI not found and config missing
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
it('activate() throws INIT_FAILED when CLI not found and config missing', async () => {
|
|
111
|
-
mockLoadConfig.mockReturnValue(null);
|
|
112
|
-
const deps: OnboardDeps = {
|
|
113
|
-
resolveSelfCli: () => {
|
|
114
|
-
throw new Error('not found');
|
|
115
|
-
},
|
|
116
|
-
runCommand: vi.fn(),
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
await expect(activate({}, deps)).rejects.toMatchObject({
|
|
120
|
-
code: 'INIT_FAILED',
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Test 1b: Auto-onboard runs init + openclaw sync when config missing
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
it('activate() auto-onboards when config missing and CLI available', async () => {
|
|
128
|
-
mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
|
|
129
|
-
const mockRun = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
|
|
130
|
-
const deps: OnboardDeps = {
|
|
131
|
-
resolveSelfCli: () => '/usr/local/bin/agentbnb',
|
|
132
|
-
runCommand: mockRun,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
ctx = await activate({}, deps);
|
|
136
|
-
|
|
137
|
-
expect(mockRun).toHaveBeenCalledTimes(2);
|
|
138
|
-
expect(mockRun.mock.calls[0][0]).toMatch(/^'\/usr\/local\/bin\/agentbnb' init --owner .* --yes --no-detect$/);
|
|
139
|
-
expect(mockRun.mock.calls[1][0]).toBe('\'/usr/local/bin/agentbnb\' openclaw sync');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Test 1c: Auto-onboard continues if openclaw sync fails
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
it('activate() continues if openclaw sync fails during auto-onboard', async () => {
|
|
146
|
-
mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
|
|
147
|
-
const mockRun = vi.fn()
|
|
148
|
-
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
149
|
-
.mockRejectedValueOnce(new Error('SOUL.md not found'));
|
|
150
|
-
const deps: OnboardDeps = {
|
|
151
|
-
resolveSelfCli: () => '/usr/local/bin/agentbnb',
|
|
152
|
-
runCommand: mockRun,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
ctx = await activate({}, deps);
|
|
156
|
-
|
|
157
|
-
expect(ctx.startDisposition).toBe('started');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// Test 1d: Skips auto-onboard when config already exists
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
it('activate() skips auto-onboard when config already exists', async () => {
|
|
164
|
-
const mockRun = vi.fn();
|
|
165
|
-
const deps: OnboardDeps = {
|
|
166
|
-
resolveSelfCli: () => '/usr/local/bin/agentbnb',
|
|
167
|
-
runCommand: mockRun,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
ctx = await activate({}, deps);
|
|
171
|
-
|
|
172
|
-
expect(mockRun).not.toHaveBeenCalled();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// Test 2: BootstrapContext shape
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
it('activate() returns BootstrapContext with correct shape', async () => {
|
|
179
|
-
ctx = await activate();
|
|
180
|
-
|
|
181
|
-
expect(ctx).toHaveProperty('service');
|
|
182
|
-
expect(ctx).toHaveProperty('status');
|
|
183
|
-
expect(ctx).toHaveProperty('startDisposition');
|
|
184
|
-
expect(ctx).toHaveProperty('_removeSignalHandlers');
|
|
185
|
-
expect(typeof ctx._removeSignalHandlers).toBe('function');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Test 3: startDisposition reflects ensureRunning result
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
it('startDisposition is "started" when ensureRunning returns "started"', async () => {
|
|
192
|
-
mockEnsureRunning.mockResolvedValue('started');
|
|
193
|
-
ctx = await activate();
|
|
194
|
-
expect(ctx.startDisposition).toBe('started');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('startDisposition is "already_running" when node was already up', async () => {
|
|
198
|
-
mockEnsureRunning.mockResolvedValue('already_running');
|
|
199
|
-
ctx = await activate();
|
|
200
|
-
expect(ctx.startDisposition).toBe('already_running');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// Test 4: status snapshot from getNodeStatus
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
it('activate() status reflects getNodeStatus() snapshot', async () => {
|
|
207
|
-
ctx = await activate();
|
|
208
|
-
expect(ctx.status).toEqual(MOCK_STATUS);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
// Test 5: signal handlers registered
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
it('activate() registers SIGTERM and SIGINT handlers', async () => {
|
|
215
|
-
const sigtermBefore = process.listenerCount('SIGTERM');
|
|
216
|
-
const sigintBefore = process.listenerCount('SIGINT');
|
|
217
|
-
|
|
218
|
-
ctx = await activate();
|
|
219
|
-
|
|
220
|
-
expect(process.listenerCount('SIGTERM')).toBe(sigtermBefore + 1);
|
|
221
|
-
expect(process.listenerCount('SIGINT')).toBe(sigintBefore + 1);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
// Test 6: _removeSignalHandlers removes them
|
|
226
|
-
// ---------------------------------------------------------------------------
|
|
227
|
-
it('_removeSignalHandlers() removes SIGTERM and SIGINT handlers', async () => {
|
|
228
|
-
ctx = await activate();
|
|
229
|
-
const sigtermAfterActivate = process.listenerCount('SIGTERM');
|
|
230
|
-
const sigintAfterActivate = process.listenerCount('SIGINT');
|
|
231
|
-
|
|
232
|
-
ctx._removeSignalHandlers();
|
|
233
|
-
|
|
234
|
-
expect(process.listenerCount('SIGTERM')).toBe(sigtermAfterActivate - 1);
|
|
235
|
-
expect(process.listenerCount('SIGINT')).toBe(sigintAfterActivate - 1);
|
|
236
|
-
|
|
237
|
-
ctx = undefined;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
// Test 7: deactivate() stops node when startDisposition === 'started'
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
it('deactivate() calls service.stop() when startDisposition is "started"', async () => {
|
|
244
|
-
mockEnsureRunning.mockResolvedValue('started');
|
|
245
|
-
ctx = await activate();
|
|
246
|
-
|
|
247
|
-
await deactivate(ctx);
|
|
248
|
-
ctx = undefined;
|
|
249
|
-
|
|
250
|
-
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
// Test 8: deactivate() does NOT stop node when already_running
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
it('deactivate() does NOT call service.stop() when startDisposition is "already_running"', async () => {
|
|
257
|
-
mockEnsureRunning.mockResolvedValue('already_running');
|
|
258
|
-
ctx = await activate();
|
|
259
|
-
|
|
260
|
-
await deactivate(ctx);
|
|
261
|
-
ctx = undefined;
|
|
262
|
-
|
|
263
|
-
expect(mockStop).not.toHaveBeenCalled();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
// Test 9: deactivate() is idempotent
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
it('deactivate() is idempotent — second call does not throw', async () => {
|
|
270
|
-
ctx = await activate();
|
|
271
|
-
|
|
272
|
-
await deactivate(ctx);
|
|
273
|
-
await expect(deactivate(ctx)).resolves.not.toThrow();
|
|
274
|
-
|
|
275
|
-
ctx = undefined;
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
279
|
-
// Test 10: deactivate() removes signal handlers
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
it('deactivate() removes signal handlers', async () => {
|
|
282
|
-
ctx = await activate();
|
|
283
|
-
const sigtermAfterActivate = process.listenerCount('SIGTERM');
|
|
284
|
-
|
|
285
|
-
await deactivate(ctx);
|
|
286
|
-
ctx = undefined;
|
|
287
|
-
|
|
288
|
-
expect(process.listenerCount('SIGTERM')).toBeLessThan(sigtermAfterActivate);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
// Default export (OpenClaw plugin definition)
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
|
|
296
|
-
describe('bootstrap default export (OpenClaw plugin definition)', () => {
|
|
297
|
-
it('has id, name, description, and register', () => {
|
|
298
|
-
expect(bootstrapDefault).toHaveProperty('id', 'agentbnb');
|
|
299
|
-
expect(bootstrapDefault).toHaveProperty('name', 'AgentBnB');
|
|
300
|
-
expect(bootstrapDefault).toHaveProperty('description');
|
|
301
|
-
expect(typeof bootstrapDefault.description).toBe('string');
|
|
302
|
-
expect(bootstrapDefault).toHaveProperty('register');
|
|
303
|
-
expect(typeof bootstrapDefault.register).toBe('function');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('register() calls api.registerTool with a factory', () => {
|
|
307
|
-
const mockRegisterTool = vi.fn();
|
|
308
|
-
bootstrapDefault.register({ registerTool: mockRegisterTool });
|
|
309
|
-
expect(mockRegisterTool).toHaveBeenCalledTimes(1);
|
|
310
|
-
expect(typeof mockRegisterTool.mock.calls[0][0]).toBe('function');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('factory returns 5 tools', () => {
|
|
314
|
-
let factory: ((ctx: { workspaceDir?: string; agentDir?: string }) => unknown[]) | undefined;
|
|
315
|
-
const mockRegisterTool = vi.fn((fn: typeof factory) => { factory = fn; });
|
|
316
|
-
bootstrapDefault.register({ registerTool: mockRegisterTool });
|
|
317
|
-
|
|
318
|
-
// Call the factory with a mock context — it will fail on config read,
|
|
319
|
-
// so we mock createAllTools indirectly by checking the factory is callable
|
|
320
|
-
expect(factory).toBeDefined();
|
|
321
|
-
expect(typeof factory).toBe('function');
|
|
322
|
-
});
|
|
323
|
-
});
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for openclaw-tools.ts — OpenClaw tool factory.
|
|
3
|
-
*
|
|
4
|
-
* Tests verify:
|
|
5
|
-
* - Tool factory returns 5 tools with correct shape
|
|
6
|
-
* - Tool names are kebab-case with agentbnb- prefix
|
|
7
|
-
* - Parameter schemas are valid JSON Schema
|
|
8
|
-
* - Result conversion from MCP format to AgentToolResult
|
|
9
|
-
* - Context caching and multi-agent isolation
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
-
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { tmpdir } from 'node:os';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
createAllTools,
|
|
19
|
-
createDiscoverTool,
|
|
20
|
-
createRequestTool,
|
|
21
|
-
createConductTool,
|
|
22
|
-
createStatusTool,
|
|
23
|
-
createPublishTool,
|
|
24
|
-
toAgentToolResult,
|
|
25
|
-
buildMcpContext,
|
|
26
|
-
resolveConfigDir,
|
|
27
|
-
resetContextCache,
|
|
28
|
-
} from './openclaw-tools.js';
|
|
29
|
-
import type { AgentTool, OpenClawToolContext } from './openclaw-tools.js';
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Mocks
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
vi.mock('../../src/identity/identity.js', () => ({
|
|
36
|
-
ensureIdentity: vi.fn((_dir: string, owner: string) => ({
|
|
37
|
-
agent_id: 'mock-agent-id',
|
|
38
|
-
owner,
|
|
39
|
-
public_key: 'deadbeef'.repeat(8),
|
|
40
|
-
created_at: '2026-01-01T00:00:00.000Z',
|
|
41
|
-
})),
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
vi.mock('../../src/mcp/tools/discover.js', () => ({
|
|
45
|
-
handleDiscover: vi.fn(async () => ({
|
|
46
|
-
content: [{ type: 'text', text: JSON.stringify({ results: [], count: 0 }) }],
|
|
47
|
-
})),
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
vi.mock('../../src/mcp/tools/request.js', () => ({
|
|
51
|
-
handleRequest: vi.fn(async () => ({
|
|
52
|
-
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
53
|
-
})),
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
vi.mock('../../src/mcp/tools/conduct.js', () => ({
|
|
57
|
-
handleConduct: vi.fn(async () => ({
|
|
58
|
-
content: [{ type: 'text', text: JSON.stringify({ success: true, plan: [] }) }],
|
|
59
|
-
})),
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
vi.mock('../../src/mcp/tools/status.js', () => ({
|
|
63
|
-
handleStatus: vi.fn(async () => ({
|
|
64
|
-
content: [{ type: 'text', text: JSON.stringify({ agent_id: 'mock', balance: 50 }) }],
|
|
65
|
-
})),
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
|
-
vi.mock('../../src/mcp/tools/publish.js', () => ({
|
|
69
|
-
handlePublish: vi.fn(async () => ({
|
|
70
|
-
content: [{ type: 'text', text: JSON.stringify({ success: true, card_id: 'abc' }) }],
|
|
71
|
-
})),
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Helpers
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Creates a temp dir ending with `.agentbnb` containing config.json.
|
|
80
|
-
* resolveConfigDir() uses the dir as-is when it ends with `.agentbnb`.
|
|
81
|
-
*/
|
|
82
|
-
function createTempConfigDir(): string {
|
|
83
|
-
const parent = mkdtempSync(join(tmpdir(), 'agentbnb-test-'));
|
|
84
|
-
const configDir = join(parent, '.agentbnb');
|
|
85
|
-
mkdirSync(configDir, { recursive: true });
|
|
86
|
-
writeFileSync(
|
|
87
|
-
join(configDir, 'config.json'),
|
|
88
|
-
JSON.stringify({
|
|
89
|
-
owner: 'test-agent',
|
|
90
|
-
gateway_url: 'http://localhost:7700',
|
|
91
|
-
gateway_port: 7700,
|
|
92
|
-
db_path: ':memory:',
|
|
93
|
-
credit_db_path: ':memory:',
|
|
94
|
-
token: 'test-token',
|
|
95
|
-
registry: 'https://hub.agentbnb.dev',
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
return configDir;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Tests
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
describe('openclaw-tools', () => {
|
|
106
|
-
beforeEach(() => {
|
|
107
|
-
resetContextCache();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
afterEach(() => {
|
|
111
|
-
resetContextCache();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// -------------------------------------------------------------------------
|
|
115
|
-
// 1. Tool factory shape
|
|
116
|
-
// -------------------------------------------------------------------------
|
|
117
|
-
describe('createAllTools()', () => {
|
|
118
|
-
it('returns array of 5 tools', () => {
|
|
119
|
-
const configDir = createTempConfigDir();
|
|
120
|
-
const tools = createAllTools({ agentDir: configDir });
|
|
121
|
-
expect(tools).toHaveLength(5);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('each tool has required properties', () => {
|
|
125
|
-
const configDir = createTempConfigDir();
|
|
126
|
-
const tools = createAllTools({ agentDir: configDir });
|
|
127
|
-
for (const tool of tools) {
|
|
128
|
-
expect(tool).toHaveProperty('name');
|
|
129
|
-
expect(tool).toHaveProperty('label');
|
|
130
|
-
expect(tool).toHaveProperty('description');
|
|
131
|
-
expect(tool).toHaveProperty('parameters');
|
|
132
|
-
expect(tool).toHaveProperty('execute');
|
|
133
|
-
expect(typeof tool.name).toBe('string');
|
|
134
|
-
expect(typeof tool.label).toBe('string');
|
|
135
|
-
expect(typeof tool.description).toBe('string');
|
|
136
|
-
expect(typeof tool.parameters).toBe('object');
|
|
137
|
-
expect(typeof tool.execute).toBe('function');
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// -------------------------------------------------------------------------
|
|
143
|
-
// 2. Tool names — kebab-case with agentbnb- prefix
|
|
144
|
-
// -------------------------------------------------------------------------
|
|
145
|
-
describe('tool names', () => {
|
|
146
|
-
it('all names follow agentbnb-<action> pattern', () => {
|
|
147
|
-
const configDir = createTempConfigDir();
|
|
148
|
-
const tools = createAllTools({ agentDir: configDir });
|
|
149
|
-
const names = tools.map((t) => t.name);
|
|
150
|
-
|
|
151
|
-
expect(names).toEqual([
|
|
152
|
-
'agentbnb-discover',
|
|
153
|
-
'agentbnb-request',
|
|
154
|
-
'agentbnb-conduct',
|
|
155
|
-
'agentbnb-status',
|
|
156
|
-
'agentbnb-publish',
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
for (const name of names) {
|
|
160
|
-
expect(name).toMatch(/^agentbnb-[a-z]+$/);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// -------------------------------------------------------------------------
|
|
166
|
-
// 3. Parameter schemas
|
|
167
|
-
// -------------------------------------------------------------------------
|
|
168
|
-
describe('parameter schemas', () => {
|
|
169
|
-
it('discover requires query', () => {
|
|
170
|
-
const configDir = createTempConfigDir();
|
|
171
|
-
const tool = createDiscoverTool({ agentDir: configDir });
|
|
172
|
-
const schema = tool.parameters as { required: string[]; properties: Record<string, unknown> };
|
|
173
|
-
expect(schema.required).toContain('query');
|
|
174
|
-
expect(schema.properties).toHaveProperty('query');
|
|
175
|
-
expect(schema.properties).toHaveProperty('level');
|
|
176
|
-
expect(schema.properties).toHaveProperty('online_only');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('request has no required fields', () => {
|
|
180
|
-
const configDir = createTempConfigDir();
|
|
181
|
-
const tool = createRequestTool({ agentDir: configDir });
|
|
182
|
-
const schema = tool.parameters as { required: string[] };
|
|
183
|
-
expect(schema.required).toEqual([]);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('conduct requires task', () => {
|
|
187
|
-
const configDir = createTempConfigDir();
|
|
188
|
-
const tool = createConductTool({ agentDir: configDir });
|
|
189
|
-
const schema = tool.parameters as { required: string[] };
|
|
190
|
-
expect(schema.required).toContain('task');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('status has no required fields', () => {
|
|
194
|
-
const configDir = createTempConfigDir();
|
|
195
|
-
const tool = createStatusTool({ agentDir: configDir });
|
|
196
|
-
const schema = tool.parameters as { required: string[]; properties: Record<string, unknown> };
|
|
197
|
-
expect(schema.required).toEqual([]);
|
|
198
|
-
expect(Object.keys(schema.properties)).toHaveLength(0);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('publish requires card_json', () => {
|
|
202
|
-
const configDir = createTempConfigDir();
|
|
203
|
-
const tool = createPublishTool({ agentDir: configDir });
|
|
204
|
-
const schema = tool.parameters as { required: string[] };
|
|
205
|
-
expect(schema.required).toContain('card_json');
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// -------------------------------------------------------------------------
|
|
210
|
-
// 4. Result conversion
|
|
211
|
-
// -------------------------------------------------------------------------
|
|
212
|
-
describe('toAgentToolResult()', () => {
|
|
213
|
-
it('converts MCP format to AgentToolResult with parsed details', () => {
|
|
214
|
-
const mcpResult = {
|
|
215
|
-
content: [{ type: 'text' as const, text: '{"results":[],"count":0}' }],
|
|
216
|
-
};
|
|
217
|
-
const result = toAgentToolResult(mcpResult);
|
|
218
|
-
expect(result.content).toBe('{"results":[],"count":0}');
|
|
219
|
-
expect(result.details).toEqual({ results: [], count: 0 });
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('handles non-JSON text gracefully', () => {
|
|
223
|
-
const mcpResult = {
|
|
224
|
-
content: [{ type: 'text' as const, text: 'not json' }],
|
|
225
|
-
};
|
|
226
|
-
const result = toAgentToolResult(mcpResult);
|
|
227
|
-
expect(result.content).toBe('not json');
|
|
228
|
-
expect(result.details).toBeUndefined();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('handles empty content array', () => {
|
|
232
|
-
const mcpResult = { content: [] as Array<{ type: string; text: string }> };
|
|
233
|
-
const result = toAgentToolResult(mcpResult);
|
|
234
|
-
expect(result.content).toBe('{}');
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// -------------------------------------------------------------------------
|
|
239
|
-
// 5. Context caching
|
|
240
|
-
// -------------------------------------------------------------------------
|
|
241
|
-
describe('context caching', () => {
|
|
242
|
-
it('same configDir reuses cached context', () => {
|
|
243
|
-
const configDir = createTempConfigDir();
|
|
244
|
-
const ctx1 = buildMcpContext({ agentDir: configDir });
|
|
245
|
-
const ctx2 = buildMcpContext({ agentDir: configDir });
|
|
246
|
-
expect(ctx1).toBe(ctx2); // same reference
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('resetContextCache() clears the cache', () => {
|
|
250
|
-
const configDir = createTempConfigDir();
|
|
251
|
-
const ctx1 = buildMcpContext({ agentDir: configDir });
|
|
252
|
-
resetContextCache();
|
|
253
|
-
const ctx2 = buildMcpContext({ agentDir: configDir });
|
|
254
|
-
expect(ctx1).not.toBe(ctx2); // different reference
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// -------------------------------------------------------------------------
|
|
259
|
-
// 6. Multi-agent isolation
|
|
260
|
-
// -------------------------------------------------------------------------
|
|
261
|
-
describe('multi-agent isolation', () => {
|
|
262
|
-
it('different workspaceDir values produce different contexts', () => {
|
|
263
|
-
const dir1 = createTempConfigDir();
|
|
264
|
-
const dir2 = createTempConfigDir();
|
|
265
|
-
|
|
266
|
-
const ctx1 = buildMcpContext({ agentDir: dir1 });
|
|
267
|
-
const ctx2 = buildMcpContext({ agentDir: dir2 });
|
|
268
|
-
|
|
269
|
-
expect(ctx1).not.toBe(ctx2);
|
|
270
|
-
expect(ctx1.configDir).not.toBe(ctx2.configDir);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// -------------------------------------------------------------------------
|
|
275
|
-
// 7. resolveConfigDir
|
|
276
|
-
// -------------------------------------------------------------------------
|
|
277
|
-
describe('resolveConfigDir()', () => {
|
|
278
|
-
it('uses agentDir directly if it ends with .agentbnb', () => {
|
|
279
|
-
const dir = resolveConfigDir({ agentDir: '/tmp/test/.agentbnb' });
|
|
280
|
-
expect(dir).toBe('/tmp/test/.agentbnb');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('appends .agentbnb to agentDir if it does not end with .agentbnb', () => {
|
|
284
|
-
const dir = resolveConfigDir({ agentDir: '/tmp/test-agent' });
|
|
285
|
-
expect(dir).toBe('/tmp/test-agent/.agentbnb');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('derives from workspaceDir when agentDir is not set', () => {
|
|
289
|
-
const dir = resolveConfigDir({ workspaceDir: '/tmp/workspace' });
|
|
290
|
-
expect(dir).toBe('/tmp/workspace/.agentbnb');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('falls back to ~/.agentbnb when neither is set', () => {
|
|
294
|
-
const dir = resolveConfigDir({});
|
|
295
|
-
expect(dir).toMatch(/\.agentbnb$/);
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// -------------------------------------------------------------------------
|
|
300
|
-
// 8. buildMcpContext error on missing config
|
|
301
|
-
// -------------------------------------------------------------------------
|
|
302
|
-
describe('buildMcpContext()', () => {
|
|
303
|
-
it('throws when config.json does not exist', () => {
|
|
304
|
-
const dir = mkdtempSync(join(tmpdir(), 'agentbnb-noconfig-'));
|
|
305
|
-
expect(() => buildMcpContext({ agentDir: dir })).toThrow(/not initialized/);
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// -------------------------------------------------------------------------
|
|
310
|
-
// 9. Tool execution delegates to MCP handlers
|
|
311
|
-
// -------------------------------------------------------------------------
|
|
312
|
-
describe('tool execution', () => {
|
|
313
|
-
it('discover tool delegates to handleDiscover', async () => {
|
|
314
|
-
const configDir = createTempConfigDir();
|
|
315
|
-
const tool = createDiscoverTool({ agentDir: configDir });
|
|
316
|
-
const result = await tool.execute('call-1', { query: 'stock analysis' });
|
|
317
|
-
expect(result.content).toBeTruthy();
|
|
318
|
-
expect(result.details).toEqual({ results: [], count: 0 });
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('status tool delegates to handleStatus', async () => {
|
|
322
|
-
const configDir = createTempConfigDir();
|
|
323
|
-
const tool = createStatusTool({ agentDir: configDir });
|
|
324
|
-
const result = await tool.execute('call-2', {});
|
|
325
|
-
expect(result.details).toEqual({ agent_id: 'mock', balance: 50 });
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
});
|