gopherhole_openclaw_a2a 0.3.11 → 0.3.13

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.
@@ -76,8 +76,15 @@ export declare class A2AConnectionManager {
76
76
  discoverAgents(options?: {
77
77
  query?: string;
78
78
  category?: string;
79
+ tag?: string;
80
+ skillTag?: string;
81
+ contentMode?: string;
82
+ sort?: string;
83
+ owner?: string;
79
84
  verified?: boolean;
80
85
  limit?: number;
86
+ offset?: number;
87
+ scope?: string;
81
88
  }): Promise<Array<{
82
89
  id: string;
83
90
  name: string;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+ export {};
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { A2AConnectionManager } from './connection';
7
+ // Mock fetch
8
+ const mockFetch = vi.fn();
9
+ global.fetch = mockFetch;
10
+ // Mock @gopherhole/sdk
11
+ vi.mock('@gopherhole/sdk', () => ({
12
+ GopherHole: vi.fn().mockImplementation(() => ({
13
+ connect: vi.fn(),
14
+ disconnect: vi.fn(),
15
+ on: vi.fn(),
16
+ connected: false,
17
+ id: null,
18
+ })),
19
+ getTaskResponseText: vi.fn(),
20
+ }));
21
+ describe('A2AConnectionManager Discovery', () => {
22
+ let manager;
23
+ const mockApiKey = 'gph_test_key';
24
+ beforeEach(() => {
25
+ manager = new A2AConnectionManager({
26
+ enabled: true,
27
+ apiKey: mockApiKey,
28
+ bridgeUrl: 'wss://test.gopherhole.ai/ws',
29
+ });
30
+ mockFetch.mockReset();
31
+ });
32
+ afterEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+ const mockDiscoverResponse = {
36
+ agents: [
37
+ {
38
+ id: 'agent-1',
39
+ name: 'Test Agent',
40
+ description: 'A test agent',
41
+ verified: true,
42
+ tenantName: 'TestTenant',
43
+ avgRating: 4.5,
44
+ },
45
+ ],
46
+ };
47
+ const setupMockRpcResponse = (result) => {
48
+ mockFetch.mockResolvedValueOnce({
49
+ ok: true,
50
+ json: () => Promise.resolve({ result }),
51
+ });
52
+ };
53
+ describe('discoverAgents() with new params', () => {
54
+ it('should call discover with tag param', async () => {
55
+ setupMockRpcResponse(mockDiscoverResponse);
56
+ await manager.discoverAgents({ tag: 'ai' });
57
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
58
+ method: 'POST',
59
+ body: expect.stringContaining('"tag":"ai"'),
60
+ }));
61
+ });
62
+ it('should call discover with skillTag param', async () => {
63
+ setupMockRpcResponse(mockDiscoverResponse);
64
+ await manager.discoverAgents({ skillTag: 'nlp' });
65
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
66
+ body: expect.stringContaining('"skillTag":"nlp"'),
67
+ }));
68
+ });
69
+ it('should call discover with contentMode param', async () => {
70
+ setupMockRpcResponse(mockDiscoverResponse);
71
+ await manager.discoverAgents({ contentMode: 'text/markdown' });
72
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
73
+ body: expect.stringContaining('"contentMode":"text/markdown"'),
74
+ }));
75
+ });
76
+ it('should call discover with sort param', async () => {
77
+ setupMockRpcResponse(mockDiscoverResponse);
78
+ await manager.discoverAgents({ sort: 'rating' });
79
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
80
+ body: expect.stringContaining('"sort":"rating"'),
81
+ }));
82
+ });
83
+ it('should call discover with offset param', async () => {
84
+ setupMockRpcResponse(mockDiscoverResponse);
85
+ await manager.discoverAgents({ offset: 20 });
86
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
87
+ body: expect.stringContaining('"offset":20'),
88
+ }));
89
+ });
90
+ it('should call discover with scope=tenant', async () => {
91
+ setupMockRpcResponse(mockDiscoverResponse);
92
+ await manager.discoverAgents({ scope: 'tenant' });
93
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
94
+ body: expect.stringContaining('"scope":"tenant"'),
95
+ }));
96
+ });
97
+ });
98
+ describe('discoverAgents() with combined params', () => {
99
+ it('should combine multiple new params', async () => {
100
+ setupMockRpcResponse(mockDiscoverResponse);
101
+ await manager.discoverAgents({
102
+ tag: 'ai',
103
+ skillTag: 'nlp',
104
+ sort: 'popular',
105
+ offset: 10,
106
+ });
107
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
108
+ expect(body.params.tag).toBe('ai');
109
+ expect(body.params.skillTag).toBe('nlp');
110
+ expect(body.params.sort).toBe('popular');
111
+ expect(body.params.offset).toBe(10);
112
+ });
113
+ it('should combine query with new params', async () => {
114
+ setupMockRpcResponse(mockDiscoverResponse);
115
+ await manager.discoverAgents({
116
+ query: 'weather',
117
+ tag: 'api',
118
+ contentMode: 'application/json',
119
+ sort: 'recent',
120
+ limit: 25,
121
+ });
122
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
123
+ expect(body.params.query).toBe('weather');
124
+ expect(body.params.tag).toBe('api');
125
+ expect(body.params.contentMode).toBe('application/json');
126
+ expect(body.params.sort).toBe('recent');
127
+ expect(body.params.limit).toBe(25);
128
+ });
129
+ });
130
+ describe('discoverAgents() edge cases', () => {
131
+ it('should handle empty results', async () => {
132
+ setupMockRpcResponse({ agents: [] });
133
+ const result = await manager.discoverAgents({ tag: 'nonexistent' });
134
+ expect(result).toHaveLength(0);
135
+ });
136
+ it('should handle RPC errors gracefully', async () => {
137
+ mockFetch.mockResolvedValueOnce({
138
+ ok: true,
139
+ json: () => Promise.resolve({ error: { message: 'Not found' } }),
140
+ });
141
+ const result = await manager.discoverAgents({ tag: 'test' });
142
+ expect(result).toEqual([]);
143
+ });
144
+ it('should handle network errors', async () => {
145
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
146
+ const result = await manager.discoverAgents({ tag: 'test' });
147
+ expect(result).toEqual([]);
148
+ });
149
+ it('should return empty array when apiKey not configured', async () => {
150
+ const managerNoKey = new A2AConnectionManager({
151
+ enabled: true,
152
+ // No apiKey
153
+ });
154
+ const result = await managerNoKey.discoverAgents({ tag: 'test' });
155
+ expect(result).toEqual([]);
156
+ expect(mockFetch).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+ describe('discoverAgents() sort values', () => {
160
+ it('should accept rating sort', async () => {
161
+ setupMockRpcResponse(mockDiscoverResponse);
162
+ await manager.discoverAgents({ sort: 'rating' });
163
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
164
+ expect(body.params.sort).toBe('rating');
165
+ });
166
+ it('should accept popular sort', async () => {
167
+ setupMockRpcResponse(mockDiscoverResponse);
168
+ await manager.discoverAgents({ sort: 'popular' });
169
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
170
+ expect(body.params.sort).toBe('popular');
171
+ });
172
+ it('should accept recent sort', async () => {
173
+ setupMockRpcResponse(mockDiscoverResponse);
174
+ await manager.discoverAgents({ sort: 'recent' });
175
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
176
+ expect(body.params.sort).toBe('recent');
177
+ });
178
+ });
179
+ describe('discoverAgents() response mapping', () => {
180
+ it('should map response correctly', async () => {
181
+ setupMockRpcResponse({
182
+ agents: [
183
+ {
184
+ id: 'agent-1',
185
+ name: 'Test Agent',
186
+ description: 'A test agent',
187
+ verified: true,
188
+ tenantName: 'TestTenant',
189
+ avgRating: 4.5,
190
+ },
191
+ ],
192
+ });
193
+ const result = await manager.discoverAgents({ query: 'test' });
194
+ expect(result).toEqual([
195
+ {
196
+ id: 'agent-1',
197
+ name: 'Test Agent',
198
+ description: 'A test agent',
199
+ verified: true,
200
+ tenantName: 'TestTenant',
201
+ avgRating: 4.5,
202
+ },
203
+ ]);
204
+ });
205
+ });
206
+ describe('listAvailableAgents()', () => {
207
+ it('should list available agents', async () => {
208
+ setupMockRpcResponse({
209
+ agents: [
210
+ {
211
+ id: 'agent-1',
212
+ name: 'Test Agent',
213
+ description: 'A test agent',
214
+ verified: true,
215
+ accessType: 'same-tenant',
216
+ },
217
+ ],
218
+ });
219
+ const result = await manager.listAvailableAgents();
220
+ expect(result).toHaveLength(1);
221
+ expect(result[0].accessType).toBe('same-tenant');
222
+ });
223
+ });
224
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "GopherHole A2A plugin for OpenClaw - connect your AI agent to the GopherHole network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,8 @@
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "clean": "rm -rf dist",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
11
13
  "prepublishOnly": "npm run build"
12
14
  },
13
15
  "clawdbot": {
@@ -37,7 +39,8 @@
37
39
  "devDependencies": {
38
40
  "@types/node": "^25.5.0",
39
41
  "@types/uuid": "^10.0.0",
40
- "typescript": "^5.9.3"
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^1.6.0"
41
44
  },
42
45
  "peerDependencies": {
43
46
  "openclaw": "*"
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import { A2AConnectionManager } from './connection';
8
+
9
+ // Mock fetch
10
+ const mockFetch = vi.fn();
11
+ global.fetch = mockFetch;
12
+
13
+ // Mock @gopherhole/sdk
14
+ vi.mock('@gopherhole/sdk', () => ({
15
+ GopherHole: vi.fn().mockImplementation(() => ({
16
+ connect: vi.fn(),
17
+ disconnect: vi.fn(),
18
+ on: vi.fn(),
19
+ connected: false,
20
+ id: null,
21
+ })),
22
+ getTaskResponseText: vi.fn(),
23
+ }));
24
+
25
+ describe('A2AConnectionManager Discovery', () => {
26
+ let manager: A2AConnectionManager;
27
+ const mockApiKey = 'gph_test_key';
28
+
29
+ beforeEach(() => {
30
+ manager = new A2AConnectionManager({
31
+ enabled: true,
32
+ apiKey: mockApiKey,
33
+ bridgeUrl: 'wss://test.gopherhole.ai/ws',
34
+ });
35
+ mockFetch.mockReset();
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ const mockDiscoverResponse = {
43
+ agents: [
44
+ {
45
+ id: 'agent-1',
46
+ name: 'Test Agent',
47
+ description: 'A test agent',
48
+ verified: true,
49
+ tenantName: 'TestTenant',
50
+ avgRating: 4.5,
51
+ },
52
+ ],
53
+ };
54
+
55
+ const setupMockRpcResponse = (result: any) => {
56
+ mockFetch.mockResolvedValueOnce({
57
+ ok: true,
58
+ json: () => Promise.resolve({ result }),
59
+ });
60
+ };
61
+
62
+ describe('discoverAgents() with new params', () => {
63
+ it('should call discover with tag param', async () => {
64
+ setupMockRpcResponse(mockDiscoverResponse);
65
+
66
+ await manager.discoverAgents({ tag: 'ai' });
67
+
68
+ expect(mockFetch).toHaveBeenCalledWith(
69
+ expect.any(String),
70
+ expect.objectContaining({
71
+ method: 'POST',
72
+ body: expect.stringContaining('"tag":"ai"'),
73
+ })
74
+ );
75
+ });
76
+
77
+ it('should call discover with skillTag param', async () => {
78
+ setupMockRpcResponse(mockDiscoverResponse);
79
+
80
+ await manager.discoverAgents({ skillTag: 'nlp' });
81
+
82
+ expect(mockFetch).toHaveBeenCalledWith(
83
+ expect.any(String),
84
+ expect.objectContaining({
85
+ body: expect.stringContaining('"skillTag":"nlp"'),
86
+ })
87
+ );
88
+ });
89
+
90
+ it('should call discover with contentMode param', async () => {
91
+ setupMockRpcResponse(mockDiscoverResponse);
92
+
93
+ await manager.discoverAgents({ contentMode: 'text/markdown' });
94
+
95
+ expect(mockFetch).toHaveBeenCalledWith(
96
+ expect.any(String),
97
+ expect.objectContaining({
98
+ body: expect.stringContaining('"contentMode":"text/markdown"'),
99
+ })
100
+ );
101
+ });
102
+
103
+ it('should call discover with sort param', async () => {
104
+ setupMockRpcResponse(mockDiscoverResponse);
105
+
106
+ await manager.discoverAgents({ sort: 'rating' });
107
+
108
+ expect(mockFetch).toHaveBeenCalledWith(
109
+ expect.any(String),
110
+ expect.objectContaining({
111
+ body: expect.stringContaining('"sort":"rating"'),
112
+ })
113
+ );
114
+ });
115
+
116
+ it('should call discover with offset param', async () => {
117
+ setupMockRpcResponse(mockDiscoverResponse);
118
+
119
+ await manager.discoverAgents({ offset: 20 });
120
+
121
+ expect(mockFetch).toHaveBeenCalledWith(
122
+ expect.any(String),
123
+ expect.objectContaining({
124
+ body: expect.stringContaining('"offset":20'),
125
+ })
126
+ );
127
+ });
128
+
129
+ it('should call discover with scope=tenant', async () => {
130
+ setupMockRpcResponse(mockDiscoverResponse);
131
+
132
+ await manager.discoverAgents({ scope: 'tenant' });
133
+
134
+ expect(mockFetch).toHaveBeenCalledWith(
135
+ expect.any(String),
136
+ expect.objectContaining({
137
+ body: expect.stringContaining('"scope":"tenant"'),
138
+ })
139
+ );
140
+ });
141
+ });
142
+
143
+ describe('discoverAgents() with combined params', () => {
144
+ it('should combine multiple new params', async () => {
145
+ setupMockRpcResponse(mockDiscoverResponse);
146
+
147
+ await manager.discoverAgents({
148
+ tag: 'ai',
149
+ skillTag: 'nlp',
150
+ sort: 'popular',
151
+ offset: 10,
152
+ });
153
+
154
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
155
+ expect(body.params.tag).toBe('ai');
156
+ expect(body.params.skillTag).toBe('nlp');
157
+ expect(body.params.sort).toBe('popular');
158
+ expect(body.params.offset).toBe(10);
159
+ });
160
+
161
+ it('should combine query with new params', async () => {
162
+ setupMockRpcResponse(mockDiscoverResponse);
163
+
164
+ await manager.discoverAgents({
165
+ query: 'weather',
166
+ tag: 'api',
167
+ contentMode: 'application/json',
168
+ sort: 'recent',
169
+ limit: 25,
170
+ });
171
+
172
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
173
+ expect(body.params.query).toBe('weather');
174
+ expect(body.params.tag).toBe('api');
175
+ expect(body.params.contentMode).toBe('application/json');
176
+ expect(body.params.sort).toBe('recent');
177
+ expect(body.params.limit).toBe(25);
178
+ });
179
+ });
180
+
181
+ describe('discoverAgents() edge cases', () => {
182
+ it('should handle empty results', async () => {
183
+ setupMockRpcResponse({ agents: [] });
184
+
185
+ const result = await manager.discoverAgents({ tag: 'nonexistent' });
186
+
187
+ expect(result).toHaveLength(0);
188
+ });
189
+
190
+ it('should handle RPC errors gracefully', async () => {
191
+ mockFetch.mockResolvedValueOnce({
192
+ ok: true,
193
+ json: () => Promise.resolve({ error: { message: 'Not found' } }),
194
+ });
195
+
196
+ const result = await manager.discoverAgents({ tag: 'test' });
197
+
198
+ expect(result).toEqual([]);
199
+ });
200
+
201
+ it('should handle network errors', async () => {
202
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
203
+
204
+ const result = await manager.discoverAgents({ tag: 'test' });
205
+
206
+ expect(result).toEqual([]);
207
+ });
208
+
209
+ it('should return empty array when apiKey not configured', async () => {
210
+ const managerNoKey = new A2AConnectionManager({
211
+ enabled: true,
212
+ // No apiKey
213
+ });
214
+
215
+ const result = await managerNoKey.discoverAgents({ tag: 'test' });
216
+
217
+ expect(result).toEqual([]);
218
+ expect(mockFetch).not.toHaveBeenCalled();
219
+ });
220
+ });
221
+
222
+ describe('discoverAgents() sort values', () => {
223
+ it('should accept rating sort', async () => {
224
+ setupMockRpcResponse(mockDiscoverResponse);
225
+ await manager.discoverAgents({ sort: 'rating' });
226
+
227
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
228
+ expect(body.params.sort).toBe('rating');
229
+ });
230
+
231
+ it('should accept popular sort', async () => {
232
+ setupMockRpcResponse(mockDiscoverResponse);
233
+ await manager.discoverAgents({ sort: 'popular' });
234
+
235
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
236
+ expect(body.params.sort).toBe('popular');
237
+ });
238
+
239
+ it('should accept recent sort', async () => {
240
+ setupMockRpcResponse(mockDiscoverResponse);
241
+ await manager.discoverAgents({ sort: 'recent' });
242
+
243
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
244
+ expect(body.params.sort).toBe('recent');
245
+ });
246
+ });
247
+
248
+ describe('discoverAgents() response mapping', () => {
249
+ it('should map response correctly', async () => {
250
+ setupMockRpcResponse({
251
+ agents: [
252
+ {
253
+ id: 'agent-1',
254
+ name: 'Test Agent',
255
+ description: 'A test agent',
256
+ verified: true,
257
+ tenantName: 'TestTenant',
258
+ avgRating: 4.5,
259
+ },
260
+ ],
261
+ });
262
+
263
+ const result = await manager.discoverAgents({ query: 'test' });
264
+
265
+ expect(result).toEqual([
266
+ {
267
+ id: 'agent-1',
268
+ name: 'Test Agent',
269
+ description: 'A test agent',
270
+ verified: true,
271
+ tenantName: 'TestTenant',
272
+ avgRating: 4.5,
273
+ },
274
+ ]);
275
+ });
276
+ });
277
+
278
+ describe('listAvailableAgents()', () => {
279
+ it('should list available agents', async () => {
280
+ setupMockRpcResponse({
281
+ agents: [
282
+ {
283
+ id: 'agent-1',
284
+ name: 'Test Agent',
285
+ description: 'A test agent',
286
+ verified: true,
287
+ accessType: 'same-tenant',
288
+ },
289
+ ],
290
+ });
291
+
292
+ const result = await manager.listAvailableAgents();
293
+
294
+ expect(result).toHaveLength(1);
295
+ expect(result[0].accessType).toBe('same-tenant');
296
+ });
297
+ });
298
+ });
package/src/connection.ts CHANGED
@@ -423,8 +423,15 @@ export class A2AConnectionManager {
423
423
  async discoverAgents(options?: {
424
424
  query?: string;
425
425
  category?: string;
426
- verified?: boolean;
426
+ tag?: string;
427
+ skillTag?: string;
428
+ contentMode?: string;
429
+ sort?: string;
430
+ owner?: string; // Filter by organization/tenant name
431
+ verified?: boolean; // Only show agents from verified organizations
427
432
  limit?: number;
433
+ offset?: number;
434
+ scope?: string;
428
435
  }): Promise<Array<{
429
436
  id: string;
430
437
  name: string;
package/tsconfig.json CHANGED
@@ -13,5 +13,5 @@
13
13
  "forceConsistentCasingInFileNames": true
14
14
  },
15
15
  "include": ["index.ts", "src/**/*.ts"],
16
- "exclude": ["node_modules", "dist"]
16
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
17
17
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts'],
8
+ },
9
+ });