agentbnb 4.0.4 → 5.1.0

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.
Files changed (45) hide show
  1. package/dist/chunk-AUBHR7HH.js +25 -0
  2. package/dist/chunk-B5FTAGFN.js +393 -0
  3. package/dist/{chunk-GGYC5U2Z.js → chunk-BTTL24TZ.js} +29 -91
  4. package/dist/chunk-C6KPAFCC.js +387 -0
  5. package/dist/{chunk-JXEOE7HX.js → chunk-CRFCWD6V.js} +163 -92
  6. package/dist/chunk-CSATDXZC.js +89 -0
  7. package/dist/{chunk-T7NS2J2B.js → chunk-DFBX3BBD.js} +84 -1
  8. package/dist/{chunk-DNWT5FZQ.js → chunk-EANI2N2V.js} +98 -1
  9. package/dist/{chunk-HH24WMFN.js → chunk-FLY3WIQR.js} +1 -1
  10. package/dist/{chunk-EVBX22YU.js → chunk-HLUEOLSZ.js} +11 -17
  11. package/dist/chunk-IVOYM3WG.js +25 -0
  12. package/dist/chunk-LCAIAAG2.js +916 -0
  13. package/dist/chunk-MLS6IGGG.js +294 -0
  14. package/dist/{chunk-4P3EMGL4.js → chunk-MNO4COST.js} +5 -3
  15. package/dist/chunk-NH2FIERR.js +138 -0
  16. package/dist/chunk-UKT6H7YT.js +29 -0
  17. package/dist/{chunk-BH6WGYFB.js → chunk-VE3E4AMH.js} +8 -8
  18. package/dist/{chunk-5QGXARLJ.js → chunk-W5BZMKMF.js} +159 -27
  19. package/dist/{chunk-FF226TIV.js → chunk-ZX5623ER.js} +0 -57
  20. package/dist/cli/index.js +362 -4633
  21. package/dist/{conduct-N52JX7RT.js → conduct-KM6ZNJGE.js} +10 -8
  22. package/dist/{conduct-GZQNFTRP.js → conduct-WGTMQND5.js} +10 -8
  23. package/dist/{conductor-mode-XUWGR4ZE.js → conductor-mode-OL2FNOYY.js} +6 -4
  24. package/dist/{conductor-mode-ESGFZ6T5.js → conductor-mode-VRO7TYW2.js} +20 -167
  25. package/dist/execute-CPFSOOO3.js +13 -0
  26. package/dist/execute-IP2QHALV.js +10 -0
  27. package/dist/index.d.ts +14 -8
  28. package/dist/index.js +186 -35
  29. package/dist/{peers-E4MKNNDN.js → peers-CJ7T4RJO.js} +2 -1
  30. package/dist/process-guard-CC7CNRQJ.js +176 -0
  31. package/dist/{request-4GQSSM4B.js → request-YOWPXVLQ.js} +13 -10
  32. package/dist/schema-7BSSLZ4S.js +8 -0
  33. package/dist/{serve-skill-Q6NHX2RA.js → serve-skill-JHFNR7BW.js} +8 -7
  34. package/dist/{server-B5E566CI.js → server-HKJJWFRG.js} +10 -8
  35. package/dist/service-coordinator-5R4LQW6L.js +4917 -0
  36. package/dist/skills/agentbnb/bootstrap.js +5028 -848
  37. package/dist/websocket-client-WRN3HO73.js +6 -0
  38. package/package.json +4 -1
  39. package/skills/agentbnb/SKILL.md +87 -70
  40. package/skills/agentbnb/bootstrap.test.ts +142 -242
  41. package/skills/agentbnb/bootstrap.ts +88 -95
  42. package/skills/agentbnb/install.sh +97 -27
  43. package/dist/card-RNEWSAQ6.js +0 -88
  44. package/dist/chunk-UB2NPFC7.js +0 -165
  45. package/dist/execute-QH6F54D7.js +0 -10
@@ -1,323 +1,223 @@
1
1
  /**
2
- * Integration test for bootstrap.ts activate()/deactivate() lifecycle.
2
+ * Unit tests for bootstrap.ts thin OpenClaw adapter.
3
3
  *
4
- * Tests the full lifecycle using real implementations with in-memory DBs:
5
- * activate() -> runtime + card published + gateway listening + IdleMonitor running
6
- * deactivate() -> gateway closed + runtime shutdown + resources cleaned up
7
- *
8
- * No mocks this is an end-to-end integration test.
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
9
12
  */
10
13
 
11
- import { describe, it, expect, afterEach } from 'vitest';
12
- import { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs';
13
- import { tmpdir } from 'node:os';
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { homedir } from 'node:os';
14
16
  import { join } from 'node:path';
15
17
 
16
- import { activate, deactivate } from './bootstrap.js';
17
- import type { BootstrapContext } from './bootstrap.js';
18
- import { IdleMonitor } from '../../src/autonomy/idle-monitor.js';
19
- import { loadIdentity } from '../../src/identity/identity.js';
20
-
21
18
  // ---------------------------------------------------------------------------
22
- // Test fixture: minimal SOUL.md with 2 skills
19
+ // Mocks must be hoisted before imports
23
20
  // ---------------------------------------------------------------------------
24
- const SOUL_MD_CONTENT = `# Test Agent
25
21
 
26
- A test agent for integration testing.
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
+ }));
27
37
 
28
- ## Code Review
29
- Reviews code for quality and bugs.
38
+ vi.mock('../../src/runtime/process-guard.js', () => ({
39
+ ProcessGuard: vi.fn().mockImplementation(() => ({})),
40
+ }));
30
41
 
31
- ## Translation
32
- Translates text between languages.
33
- `;
42
+ vi.mock('../../src/cli/config.js', () => ({
43
+ loadConfig: vi.fn(),
44
+ getConfigDir: vi.fn(() => join(homedir(), '.agentbnb')),
45
+ }));
46
+
47
+ import { loadConfig } from '../../src/cli/config.js';
48
+ import { activate, deactivate } from './bootstrap.js';
49
+ import type { BootstrapContext } from './bootstrap.js';
50
+
51
+ const mockLoadConfig = vi.mocked(loadConfig);
52
+
53
+ const MINIMAL_CONFIG = {
54
+ owner: 'test-agent',
55
+ gateway_url: 'http://localhost:7700',
56
+ gateway_port: 7700,
57
+ db_path: ':memory:',
58
+ credit_db_path: ':memory:',
59
+ token: 'test-token',
60
+ api_key: 'test-api-key',
61
+ registry: 'https://agentbnb.fly.dev',
62
+ };
63
+
64
+ const MOCK_STATUS = {
65
+ state: 'running' as const,
66
+ pid: 1234,
67
+ port: 7700,
68
+ owner: 'test-agent',
69
+ relayConnected: false,
70
+ uptime_ms: 100,
71
+ };
34
72
 
35
73
  // ---------------------------------------------------------------------------
36
74
  // Test suite
37
75
  // ---------------------------------------------------------------------------
38
76
 
39
77
  describe('bootstrap activate/deactivate lifecycle', () => {
40
- let tmpDir: string | undefined;
41
78
  let ctx: BootstrapContext | undefined;
42
79
 
43
- /**
44
- * Create a fresh temp dir with a SOUL.md file.
45
- * Returns the absolute path to the SOUL.md file.
46
- */
47
- function setupSoulMd(): string {
48
- tmpDir = mkdtempSync(join(tmpdir(), 'agentbnb-test-'));
49
- const path = join(tmpDir, 'SOUL.md');
50
- writeFileSync(path, SOUL_MD_CONTENT, 'utf8');
51
- return path;
52
- }
53
-
54
- // Ensure all resources are torn down after every test
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ mockEnsureRunning.mockResolvedValue('started');
83
+ mockGetNodeStatus.mockResolvedValue(MOCK_STATUS);
84
+ mockStop.mockResolvedValue(undefined);
85
+ mockLoadConfig.mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
86
+ });
87
+
55
88
  afterEach(async () => {
56
89
  if (ctx) {
57
90
  await deactivate(ctx).catch(() => undefined);
58
91
  ctx = undefined;
59
92
  }
60
- if (tmpDir) {
61
- rmSync(tmpDir, { recursive: true, force: true });
62
- tmpDir = undefined;
63
- }
93
+ process.removeAllListeners('SIGTERM');
94
+ process.removeAllListeners('SIGINT');
64
95
  });
65
96
 
66
97
  // ---------------------------------------------------------------------------
67
- // Test 1: activate() returns BootstrapContext with all components
98
+ // Test 1: CONFIG_NOT_FOUND when no config exists
68
99
  // ---------------------------------------------------------------------------
69
- it('activate() returns BootstrapContext with all components', async () => {
70
- const soulMdPath = setupSoulMd();
71
-
72
- ctx = await activate({
73
- owner: 'test-agent',
74
- soulMdPath,
75
- registryDbPath: ':memory:',
76
- creditDbPath: ':memory:',
77
- gatewayPort: 0,
78
- silent: true,
100
+ it('activate() throws CONFIG_NOT_FOUND when config is missing', async () => {
101
+ mockLoadConfig.mockReturnValue(null);
102
+
103
+ await expect(activate()).rejects.toMatchObject({
104
+ code: 'CONFIG_NOT_FOUND',
79
105
  });
106
+ });
80
107
 
81
- // runtime has both DB handles
82
- expect(ctx.runtime).toBeDefined();
83
- expect(ctx.runtime.registryDb).toBeDefined();
84
- expect(ctx.runtime.creditDb).toBeDefined();
108
+ // ---------------------------------------------------------------------------
109
+ // Test 2: BootstrapContext shape
110
+ // ---------------------------------------------------------------------------
111
+ it('activate() returns BootstrapContext with correct shape', async () => {
112
+ ctx = await activate();
85
113
 
86
- // gateway is a Fastify instance (has inject method)
87
- expect(ctx.gateway).toBeDefined();
88
- expect(typeof ctx.gateway.inject).toBe('function');
114
+ expect(ctx).toHaveProperty('service');
115
+ expect(ctx).toHaveProperty('status');
116
+ expect(ctx).toHaveProperty('startDisposition');
117
+ expect(ctx).toHaveProperty('_removeSignalHandlers');
118
+ expect(typeof ctx._removeSignalHandlers).toBe('function');
119
+ });
89
120
 
90
- // idleMonitor is an IdleMonitor instance
91
- expect(ctx.idleMonitor).toBeInstanceOf(IdleMonitor);
121
+ // ---------------------------------------------------------------------------
122
+ // Test 3: startDisposition reflects ensureRunning result
123
+ // ---------------------------------------------------------------------------
124
+ it('startDisposition is "started" when ensureRunning returns "started"', async () => {
125
+ mockEnsureRunning.mockResolvedValue('started');
126
+ ctx = await activate();
127
+ expect(ctx.startDisposition).toBe('started');
128
+ });
92
129
 
93
- // card has correct structure: spec_version 2.0, 2 skills
94
- expect(ctx.card.spec_version).toBe('2.0');
95
- expect(Array.isArray(ctx.card.skills)).toBe(true);
96
- expect(ctx.card.skills!.length).toBe(2);
130
+ it('startDisposition is "already_running" when node was already up', async () => {
131
+ mockEnsureRunning.mockResolvedValue('already_running');
132
+ ctx = await activate();
133
+ expect(ctx.startDisposition).toBe('already_running');
97
134
  });
98
135
 
99
136
  // ---------------------------------------------------------------------------
100
- // Test 2: activate() publishes card from SOUL.md into registry
137
+ // Test 4: status snapshot from getNodeStatus
101
138
  // ---------------------------------------------------------------------------
102
- it('activate() publishes card from SOUL.md into registry', async () => {
103
- const soulMdPath = setupSoulMd();
104
-
105
- ctx = await activate({
106
- owner: 'test-agent',
107
- soulMdPath,
108
- registryDbPath: ':memory:',
109
- creditDbPath: ':memory:',
110
- gatewayPort: 0,
111
- silent: true,
112
- });
113
-
114
- // Query the registry DB for capability_cards owned by this agent
115
- const rows = ctx.runtime.registryDb
116
- .prepare('SELECT id, owner, data FROM capability_cards WHERE owner = ?')
117
- .all('test-agent') as Array<{ id: string; owner: string; data: string }>;
118
-
119
- expect(rows.length).toBe(1);
120
- expect(rows[0].owner).toBe('test-agent');
121
-
122
- // Parse the card data and verify both skills are present
123
- const cardData = JSON.parse(rows[0].data) as {
124
- skills?: Array<{ name: string }>;
125
- };
126
- const skillNames = (cardData.skills ?? []).map((s) => s.name);
127
- expect(skillNames).toContain('Code Review');
128
- expect(skillNames).toContain('Translation');
139
+ it('activate() status reflects getNodeStatus() snapshot', async () => {
140
+ ctx = await activate();
141
+ expect(ctx.status).toEqual(MOCK_STATUS);
129
142
  });
130
143
 
131
144
  // ---------------------------------------------------------------------------
132
- // Test 3: activate() starts gateway that responds to health check
145
+ // Test 5: signal handlers registered
133
146
  // ---------------------------------------------------------------------------
134
- it('activate() starts gateway that responds to health check', async () => {
135
- const soulMdPath = setupSoulMd();
136
-
137
- ctx = await activate({
138
- owner: 'test-agent',
139
- soulMdPath,
140
- registryDbPath: ':memory:',
141
- creditDbPath: ':memory:',
142
- gatewayPort: 0,
143
- silent: true,
144
- });
147
+ it('activate() registers SIGTERM and SIGINT handlers', async () => {
148
+ const sigtermBefore = process.listenerCount('SIGTERM');
149
+ const sigintBefore = process.listenerCount('SIGINT');
145
150
 
146
- // Use Fastify inject — no real HTTP connection needed
147
- const response = await ctx.gateway.inject({
148
- method: 'GET',
149
- url: '/health',
150
- });
151
+ ctx = await activate();
151
152
 
152
- expect(response.statusCode).toBe(200);
153
- const body = JSON.parse(response.body) as { status: string };
154
- expect(body.status).toBe('ok');
153
+ expect(process.listenerCount('SIGTERM')).toBe(sigtermBefore + 1);
154
+ expect(process.listenerCount('SIGINT')).toBe(sigintBefore + 1);
155
155
  });
156
156
 
157
157
  // ---------------------------------------------------------------------------
158
- // Test 4: activate() registers IdleMonitor job in runtime.jobs
158
+ // Test 6: _removeSignalHandlers removes them
159
159
  // ---------------------------------------------------------------------------
160
- it('activate() registers IdleMonitor job in runtime.jobs', async () => {
161
- const soulMdPath = setupSoulMd();
162
-
163
- ctx = await activate({
164
- owner: 'test-agent',
165
- soulMdPath,
166
- registryDbPath: ':memory:',
167
- creditDbPath: ':memory:',
168
- gatewayPort: 0,
169
- silent: true,
170
- });
160
+ it('_removeSignalHandlers() removes SIGTERM and SIGINT handlers', async () => {
161
+ ctx = await activate();
162
+ const sigtermAfterActivate = process.listenerCount('SIGTERM');
163
+ const sigintAfterActivate = process.listenerCount('SIGINT');
164
+
165
+ ctx._removeSignalHandlers();
166
+
167
+ expect(process.listenerCount('SIGTERM')).toBe(sigtermAfterActivate - 1);
168
+ expect(process.listenerCount('SIGINT')).toBe(sigintAfterActivate - 1);
171
169
 
172
- // At least one cron job registered (the IdleMonitor's polling job)
173
- expect(ctx.runtime.jobs.length).toBeGreaterThanOrEqual(1);
170
+ ctx = undefined;
174
171
  });
175
172
 
176
173
  // ---------------------------------------------------------------------------
177
- // Test 5: deactivate() sets runtime.isDraining to true
174
+ // Test 7: deactivate() stops node when startDisposition === 'started'
178
175
  // ---------------------------------------------------------------------------
179
- it('deactivate() sets runtime.isDraining to true', async () => {
180
- const soulMdPath = setupSoulMd();
181
-
182
- ctx = await activate({
183
- owner: 'test-agent',
184
- soulMdPath,
185
- registryDbPath: ':memory:',
186
- creditDbPath: ':memory:',
187
- gatewayPort: 0,
188
- silent: true,
189
- });
190
-
191
- expect(ctx.runtime.isDraining).toBe(false);
176
+ it('deactivate() calls service.stop() when startDisposition is "started"', async () => {
177
+ mockEnsureRunning.mockResolvedValue('started');
178
+ ctx = await activate();
192
179
 
193
180
  await deactivate(ctx);
194
- // Clear so afterEach doesn't double-deactivate
195
- const savedCtx = ctx;
196
181
  ctx = undefined;
197
182
 
198
- expect(savedCtx.runtime.isDraining).toBe(true);
183
+ expect(mockStop).toHaveBeenCalledTimes(1);
199
184
  });
200
185
 
201
186
  // ---------------------------------------------------------------------------
202
- // Test 6: deactivate() closes DB handles
187
+ // Test 8: deactivate() does NOT stop node when already_running
203
188
  // ---------------------------------------------------------------------------
204
- it('deactivate() closes DB handles', async () => {
205
- const soulMdPath = setupSoulMd();
206
-
207
- ctx = await activate({
208
- owner: 'test-agent',
209
- soulMdPath,
210
- registryDbPath: ':memory:',
211
- creditDbPath: ':memory:',
212
- gatewayPort: 0,
213
- silent: true,
214
- });
189
+ it('deactivate() does NOT call service.stop() when startDisposition is "already_running"', async () => {
190
+ mockEnsureRunning.mockResolvedValue('already_running');
191
+ ctx = await activate();
215
192
 
216
- // Capture reference before clearing ctx
217
- const runtime = ctx.runtime;
218
193
  await deactivate(ctx);
219
194
  ctx = undefined;
220
195
 
221
- // Queries should throw after DB handles are closed
222
- expect(() => {
223
- runtime.registryDb.prepare('SELECT 1').get();
224
- }).toThrow();
196
+ expect(mockStop).not.toHaveBeenCalled();
225
197
  });
226
198
 
227
199
  // ---------------------------------------------------------------------------
228
- // Test 7: deactivate() is idempotent
200
+ // Test 9: deactivate() is idempotent
229
201
  // ---------------------------------------------------------------------------
230
- it('deactivate() is idempotent', async () => {
231
- const soulMdPath = setupSoulMd();
232
-
233
- ctx = await activate({
234
- owner: 'test-agent',
235
- soulMdPath,
236
- registryDbPath: ':memory:',
237
- creditDbPath: ':memory:',
238
- gatewayPort: 0,
239
- silent: true,
240
- });
202
+ it('deactivate() is idempotent — second call does not throw', async () => {
203
+ ctx = await activate();
241
204
 
242
205
  await deactivate(ctx);
243
- // Second call must not throw
244
206
  await expect(deactivate(ctx)).resolves.not.toThrow();
245
207
 
246
208
  ctx = undefined;
247
209
  });
248
210
 
249
211
  // ---------------------------------------------------------------------------
250
- // Test 8: activate() throws when SOUL.md does not exist
212
+ // Test 10: deactivate() removes signal handlers
251
213
  // ---------------------------------------------------------------------------
252
- it('activate() throws when SOUL.md does not exist', async () => {
253
- await expect(
254
- activate({
255
- owner: 'test-agent',
256
- soulMdPath: '/nonexistent/path/SOUL.md',
257
- registryDbPath: ':memory:',
258
- creditDbPath: ':memory:',
259
- gatewayPort: 0,
260
- silent: true,
261
- }),
262
- ).rejects.toThrow();
263
- });
264
-
265
- // ---------------------------------------------------------------------------
266
- // Test 9: activate() with identityRequired creates identity.json
267
- // ---------------------------------------------------------------------------
268
- it('activate() with identityRequired creates identity.json', async () => {
269
- const soulMdPath = setupSoulMd();
270
- const identityDir = mkdtempSync(join(tmpdir(), 'agentbnb-identity-'));
271
-
272
- // Temporarily override HOME so identity writes to temp dir
273
- const origHome = process.env['HOME'];
274
- process.env['HOME'] = identityDir;
275
-
276
- try {
277
- ctx = await activate({
278
- owner: 'identity-agent',
279
- soulMdPath,
280
- registryDbPath: ':memory:',
281
- creditDbPath: ':memory:',
282
- gatewayPort: 0,
283
- silent: true,
284
- identityRequired: true,
285
- });
286
-
287
- // identity should be populated
288
- expect(ctx.identity).not.toBeNull();
289
- expect(ctx.identity!.owner).toBe('identity-agent');
290
- expect(ctx.identity!.agent_id).toBeTruthy();
291
-
292
- // identity.json should exist on disk
293
- const identityPath = join(identityDir, '.agentbnb', 'identity.json');
294
- expect(existsSync(identityPath)).toBe(true);
295
-
296
- const loaded = loadIdentity(join(identityDir, '.agentbnb'));
297
- expect(loaded).not.toBeNull();
298
- expect(loaded!.agent_id).toBe(ctx.identity!.agent_id);
299
- } finally {
300
- process.env['HOME'] = origHome;
301
- rmSync(identityDir, { recursive: true, force: true });
302
- }
303
- });
214
+ it('deactivate() removes signal handlers', async () => {
215
+ ctx = await activate();
216
+ const sigtermAfterActivate = process.listenerCount('SIGTERM');
304
217
 
305
- // ---------------------------------------------------------------------------
306
- // Test 10: activate() without identityRequired skips identity creation
307
- // ---------------------------------------------------------------------------
308
- it('activate() without identityRequired skips identity creation', async () => {
309
- const soulMdPath = setupSoulMd();
310
-
311
- ctx = await activate({
312
- owner: 'test-agent',
313
- soulMdPath,
314
- registryDbPath: ':memory:',
315
- creditDbPath: ':memory:',
316
- gatewayPort: 0,
317
- silent: true,
318
- identityRequired: false,
319
- });
218
+ await deactivate(ctx);
219
+ ctx = undefined;
320
220
 
321
- expect(ctx.identity).toBeNull();
221
+ expect(process.listenerCount('SIGTERM')).toBeLessThan(sigtermAfterActivate);
322
222
  });
323
223
  });