@agrentingai/paperclip-adapter 0.2.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.
@@ -0,0 +1,949 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { AgrentingClient } from "./client.js";
3
+
4
+ const mockConfig = {
5
+ agrentingUrl: "https://api.agrenting.com",
6
+ apiKey: "test-api-key",
7
+ agentDid: "did:agrenting:test-agent",
8
+ };
9
+
10
+ function mockFetchResponse(
11
+ status: number,
12
+ body: unknown,
13
+ headers: Record<string, string> = {}
14
+ ) {
15
+ const fn = vi.fn().mockResolvedValue({
16
+ ok: status >= 200 && status < 300,
17
+ status,
18
+ headers: new Headers(headers),
19
+ text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
20
+ json: async () => body,
21
+ });
22
+ vi.stubGlobal("fetch", fn);
23
+ return fn;
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.unstubAllGlobals();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Constructor & headers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe("AgrentingClient", () => {
39
+ it("strips trailing slashes from baseUrl", () => {
40
+ const client = new AgrentingClient({
41
+ ...mockConfig,
42
+ agrentingUrl: "https://api.agrenting.com///",
43
+ });
44
+ // Access via request — verify the URL is normalized
45
+ mockFetchResponse(200, { data: { id: "1" } });
46
+ void client.getTask("1");
47
+ expect(fetch).toHaveBeenCalledWith(
48
+ "https://api.agrenting.com/api/v1/tasks/1",
49
+ expect.anything()
50
+ );
51
+ });
52
+
53
+ it("sends Content-Type and API key headers", () => {
54
+ const client = new AgrentingClient(mockConfig);
55
+ mockFetchResponse(200, { data: { id: "1" } });
56
+ void client.getTask("1");
57
+ expect(fetch).toHaveBeenCalledWith(
58
+ expect.any(String),
59
+ expect.objectContaining({
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ "X-API-Key": "test-api-key",
63
+ },
64
+ })
65
+ );
66
+ });
67
+
68
+ // -------------------------------------------------------------------------
69
+ // createTask
70
+ // -------------------------------------------------------------------------
71
+
72
+ describe("createTask", () => {
73
+ it("sends POST to /api/v1/tasks with correct body", async () => {
74
+ const client = new AgrentingClient(mockConfig);
75
+ const taskData = {
76
+ id: "task-123",
77
+ status: "pending",
78
+ client_agent_id: "client-1",
79
+ provider_agent_id: "did:agrenting:test-agent",
80
+ capability: "code-review",
81
+ input: "review this code",
82
+ created_at: "2025-01-01T00:00:00Z",
83
+ updated_at: "2025-01-01T00:00:00Z",
84
+ };
85
+ mockFetchResponse(200, { data: taskData });
86
+
87
+ const result = await client.createTask({
88
+ providerAgentId: "did:agrenting:test-agent",
89
+ capability: "code-review",
90
+ input: "review this code",
91
+ });
92
+
93
+ expect(result).toEqual(taskData);
94
+ // Verify task response uses `id` field (not `task_id`)
95
+ expect(result.id).toBe("task-123");
96
+ expect(fetch).toHaveBeenCalledWith(
97
+ "https://api.agrenting.com/api/v1/tasks",
98
+ expect.objectContaining({ method: "POST" })
99
+ );
100
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
101
+ const body = JSON.parse(call[1].body);
102
+ expect(body.provider_agent_id).toBe("did:agrenting:test-agent");
103
+ expect(body.input).toBe("review this code");
104
+ });
105
+
106
+ it("includes max_price when provided", async () => {
107
+ const client = new AgrentingClient(mockConfig);
108
+ mockFetchResponse(200, { data: { id: "1" } });
109
+
110
+ await client.createTask({
111
+ providerAgentId: "did:agrenting:test-agent",
112
+ capability: "code-review",
113
+ input: "review",
114
+ maxPrice: "50.00",
115
+ });
116
+
117
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
118
+ const body = JSON.parse(call[1].body);
119
+ expect(body.max_price).toBe("50.00");
120
+ });
121
+ });
122
+
123
+ // -------------------------------------------------------------------------
124
+ // getTask
125
+ // -------------------------------------------------------------------------
126
+
127
+ describe("getTask", () => {
128
+ it("fetches task by ID", async () => {
129
+ const client = new AgrentingClient(mockConfig);
130
+ const taskData = {
131
+ id: "task-456",
132
+ status: "completed",
133
+ client_agent_id: "c1",
134
+ provider_agent_id: "p1",
135
+ capability: "test",
136
+ input: "hello",
137
+ output: "world",
138
+ created_at: "2025-01-01T00:00:00Z",
139
+ updated_at: "2025-01-01T00:00:00Z",
140
+ };
141
+ mockFetchResponse(200, { data: taskData });
142
+
143
+ const result = await client.getTask("task-456");
144
+ expect(result).toEqual(taskData);
145
+ expect(fetch).toHaveBeenCalledWith(
146
+ "https://api.agrenting.com/api/v1/tasks/task-456",
147
+ expect.objectContaining({ method: "GET" })
148
+ );
149
+ });
150
+ });
151
+
152
+ // -------------------------------------------------------------------------
153
+ // cancelTask
154
+ // -------------------------------------------------------------------------
155
+
156
+ describe("cancelTask", () => {
157
+ it("POSTs to cancel endpoint and returns cancelled task", async () => {
158
+ const client = new AgrentingClient(mockConfig);
159
+ const cancelledTask = {
160
+ id: "t1",
161
+ status: "cancelled",
162
+ client_agent_id: "c1",
163
+ provider_agent_id: "p1",
164
+ capability: "test",
165
+ input: "hello",
166
+ created_at: "2025-01-01T00:00:00Z",
167
+ updated_at: "2025-01-01T00:00:00Z",
168
+ };
169
+ mockFetchResponse(200, { data: cancelledTask });
170
+
171
+ const result = await client.cancelTask("t1");
172
+ expect(result.id).toBe("t1");
173
+ expect(result.status).toBe("cancelled");
174
+ expect(fetch).toHaveBeenCalledWith(
175
+ "https://api.agrenting.com/api/v1/tasks/t1/cancel",
176
+ expect.objectContaining({ method: "POST" })
177
+ );
178
+ });
179
+ });
180
+
181
+ // -------------------------------------------------------------------------
182
+ // getTaskTimeline
183
+ // -------------------------------------------------------------------------
184
+
185
+ describe("getTaskTimeline", () => {
186
+ it("fetches timeline events for a task", async () => {
187
+ const client = new AgrentingClient(mockConfig);
188
+ const timelineData = {
189
+ events: [
190
+ { event_type: "task.created", timestamp: "2025-01-01T00:00:00Z" },
191
+ { event_type: "task.claimed", timestamp: "2025-01-01T00:01:00Z" },
192
+ { event_type: "task.completed", timestamp: "2025-01-01T00:05:00Z", progress_percent: 100, progress_message: "Done" },
193
+ ],
194
+ };
195
+ mockFetchResponse(200, { data: timelineData });
196
+
197
+ const result = await client.getTaskTimeline("task-456");
198
+ expect(result.events).toHaveLength(3);
199
+ expect(result.events[0].event_type).toBe("task.created");
200
+ expect(result.events[2].progress_percent).toBe(100);
201
+ expect(fetch).toHaveBeenCalledWith(
202
+ "https://api.agrenting.com/api/v1/tasks/task-456/timeline",
203
+ expect.objectContaining({ method: "GET" })
204
+ );
205
+ });
206
+ });
207
+
208
+ // -------------------------------------------------------------------------
209
+ // getTaskAttempts
210
+ // -------------------------------------------------------------------------
211
+
212
+ describe("getTaskAttempts", () => {
213
+ it("fetches attempt history for a task", async () => {
214
+ const client = new AgrentingClient(mockConfig);
215
+ const attemptsData = {
216
+ attempts: [
217
+ { id: "att-1", status: "failed", created_at: "2025-01-01T00:00:00Z", completed_at: "2025-01-01T00:02:00Z", error_reason: "Timeout" },
218
+ { id: "att-2", status: "completed", created_at: "2025-01-01T00:03:00Z", completed_at: "2025-01-01T00:05:00Z" },
219
+ ],
220
+ };
221
+ mockFetchResponse(200, { data: attemptsData });
222
+
223
+ const result = await client.getTaskAttempts("task-789");
224
+ expect(result.attempts).toHaveLength(2);
225
+ expect(result.attempts[0].error_reason).toBe("Timeout");
226
+ expect(result.attempts[1].status).toBe("completed");
227
+ expect(fetch).toHaveBeenCalledWith(
228
+ "https://api.agrenting.com/api/v1/tasks/task-789/attempts",
229
+ expect.objectContaining({ method: "GET" })
230
+ );
231
+ });
232
+ });
233
+
234
+ // -------------------------------------------------------------------------
235
+ // getBalance
236
+ // -------------------------------------------------------------------------
237
+
238
+ describe("getBalance", () => {
239
+ it("fetches balance from ledger endpoint with full shape", async () => {
240
+ const client = new AgrentingClient(mockConfig);
241
+ mockFetchResponse(200, {
242
+ data: { available: "100", escrow: "10", total: "110", currency: "USD" },
243
+ });
244
+
245
+ const result = await client.getBalance();
246
+ expect(result.available).toBe("100");
247
+ expect(result.escrow).toBe("10");
248
+ expect(result.total).toBe("110");
249
+ expect(result.currency).toBe("USD");
250
+ expect(fetch).toHaveBeenCalledWith(
251
+ "https://api.agrenting.com/api/v1/ledger/balance",
252
+ expect.anything()
253
+ );
254
+ });
255
+ });
256
+
257
+ // -------------------------------------------------------------------------
258
+ // discoverAgents
259
+ // -------------------------------------------------------------------------
260
+
261
+ describe("discoverAgents", () => {
262
+ it("sends query params for filters", async () => {
263
+ const client = new AgrentingClient(mockConfig);
264
+ mockFetchResponse(200, {
265
+ data: [{ id: "agent-1", name: "Test Agent" }],
266
+ });
267
+
268
+ const result = await client.discoverAgents({
269
+ capability: "code-review",
270
+ minPrice: 10,
271
+ maxPrice: 50,
272
+ minReputation: 4,
273
+ sortBy: "price",
274
+ limit: 5,
275
+ });
276
+
277
+ expect(result).toEqual([{ id: "agent-1", name: "Test Agent" }]);
278
+ const url = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
279
+ expect(url).toContain("capability=code-review");
280
+ expect(url).toContain("min_price=10");
281
+ expect(url).toContain("max_price=50");
282
+ expect(url).toContain("limit=5");
283
+ });
284
+
285
+ it("works with no options", async () => {
286
+ const client = new AgrentingClient(mockConfig);
287
+ mockFetchResponse(200, { data: [] });
288
+
289
+ const result = await client.discoverAgents();
290
+ expect(result).toEqual([]);
291
+ });
292
+ });
293
+
294
+ // -------------------------------------------------------------------------
295
+ // createTaskPayment
296
+ // -------------------------------------------------------------------------
297
+
298
+ describe("createTaskPayment", () => {
299
+ it("creates payment for a task", async () => {
300
+ const client = new AgrentingClient(mockConfig);
301
+ const paymentData = {
302
+ id: "pay-1",
303
+ task_id: "t1",
304
+ amount: "25.00",
305
+ currency: "USD",
306
+ status: "escrowed",
307
+ created_at: "2025-01-01T00:00:00Z",
308
+ };
309
+ mockFetchResponse(200, { data: paymentData });
310
+
311
+ const result = await client.createTaskPayment("t1", {
312
+ paymentType: "escrow",
313
+ });
314
+
315
+ expect(result).toEqual(paymentData);
316
+ expect(fetch).toHaveBeenCalledWith(
317
+ "https://api.agrenting.com/api/v1/tasks/t1/payments",
318
+ expect.objectContaining({ method: "POST" })
319
+ );
320
+ });
321
+ });
322
+
323
+ // -------------------------------------------------------------------------
324
+ // registerWebhook / listWebhooks / deleteWebhook
325
+ // -------------------------------------------------------------------------
326
+
327
+ describe("webhook operations", () => {
328
+ it("registers a webhook with flat request body", async () => {
329
+ const client = new AgrentingClient(mockConfig);
330
+ mockFetchResponse(200, {
331
+ data: {
332
+ id: "wh-1",
333
+ callback_url: "https://example.com/hook",
334
+ event_types: ["task.completed"],
335
+ secret_key: "secret",
336
+ status: "active",
337
+ },
338
+ });
339
+
340
+ const result = await client.registerWebhook({
341
+ callbackUrl: "https://example.com/hook",
342
+ eventTypes: ["task.completed"],
343
+ });
344
+
345
+ expect(result.id).toBe("wh-1");
346
+ expect(result.secret_key).toBe("secret");
347
+
348
+ // Verify flat shape (not nested under "webhook" key)
349
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
350
+ const body = JSON.parse(call[1].body);
351
+ expect(body.callback_url).toBe("https://example.com/hook");
352
+ expect(body.event_types).toEqual(["task.completed"]);
353
+ expect(body).not.toHaveProperty("webhook");
354
+ });
355
+
356
+ it("lists webhooks", async () => {
357
+ const client = new AgrentingClient(mockConfig);
358
+ mockFetchResponse(200, {
359
+ data: [{ id: "wh-1", callback_url: "https://example.com", status: "active", failure_count: 0 }],
360
+ });
361
+
362
+ const result = await client.listWebhooks();
363
+ expect(result).toHaveLength(1);
364
+ });
365
+
366
+ it("deletes a webhook", async () => {
367
+ const client = new AgrentingClient(mockConfig);
368
+ mockFetchResponse(200, { data: null });
369
+
370
+ await client.deleteWebhook("wh-1");
371
+ expect(fetch).toHaveBeenCalledWith(
372
+ "https://api.agrenting.com/api/v1/webhooks/wh-1",
373
+ expect.objectContaining({ method: "DELETE" })
374
+ );
375
+ });
376
+ });
377
+
378
+ // -------------------------------------------------------------------------
379
+ // ledger operations
380
+ // -------------------------------------------------------------------------
381
+
382
+ describe("ledger operations", () => {
383
+ it("lists transactions with filters", async () => {
384
+ const client = new AgrentingClient(mockConfig);
385
+ mockFetchResponse(200, {
386
+ data: [{ id: "tx-1", type: "deposit", amount: "100" }],
387
+ });
388
+
389
+ const result = await client.getTransactions({ limit: 10, type: "deposit" });
390
+ expect(result).toHaveLength(1);
391
+ const url = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
392
+ expect(url).toContain("limit=10");
393
+ expect(url).toContain("type=deposit");
394
+ });
395
+
396
+ it("deposits funds", async () => {
397
+ const client = new AgrentingClient(mockConfig);
398
+ mockFetchResponse(200, {
399
+ data: { transaction_id: "tx-2", status: "pending", deposit_address: "0xabc" },
400
+ });
401
+
402
+ const result = await client.deposit({ amount: "50.00" });
403
+ expect(result.transaction_id).toBe("tx-2");
404
+ });
405
+
406
+ it("withdraws funds", async () => {
407
+ const client = new AgrentingClient(mockConfig);
408
+ mockFetchResponse(200, {
409
+ data: { transaction_id: "tx-3", status: "processing" },
410
+ });
411
+
412
+ const result = await client.withdraw({ amount: "25.00" });
413
+ expect(result.transaction_id).toBe("tx-3");
414
+ });
415
+ });
416
+
417
+ // -------------------------------------------------------------------------
418
+ // testConnection
419
+ // -------------------------------------------------------------------------
420
+
421
+ describe("testConnection", () => {
422
+ it("returns ok when balance fetch succeeds", async () => {
423
+ const client = new AgrentingClient(mockConfig);
424
+ mockFetchResponse(200, {
425
+ data: { available: "100", escrow: "10", total: "110", currency: "USD" },
426
+ });
427
+
428
+ const result = await client.testConnection();
429
+ expect(result.ok).toBe(true);
430
+ expect(result.message).toContain("110");
431
+ expect(result.message).toContain("USD");
432
+ });
433
+
434
+ it("returns error when API fails", { timeout: 15_000 }, async () => {
435
+ const client = new AgrentingClient(mockConfig);
436
+ mockFetchResponse(401, "Unauthorized");
437
+
438
+ const result = await client.testConnection();
439
+ expect(result.ok).toBe(false);
440
+ expect(result.message).toContain("401");
441
+ });
442
+ });
443
+
444
+ // -------------------------------------------------------------------------
445
+ // uploadDocument
446
+ // -------------------------------------------------------------------------
447
+
448
+ describe("uploadDocument", () => {
449
+ it("uploads a base64-encoded document to /api/v1/uploads", async () => {
450
+ const client = new AgrentingClient(mockConfig);
451
+ mockFetchResponse(200, {
452
+ data: {
453
+ id: "doc-1",
454
+ name: "instructions",
455
+ file_url: "https://cdn.agrenting.com/doc-1",
456
+ content_type: "text/plain",
457
+ file_hash: "abc123",
458
+ document_type: "instructions",
459
+ },
460
+ });
461
+
462
+ const result = await client.uploadDocument({
463
+ name: "instructions",
464
+ content: "Do the thing",
465
+ documentType: "instructions",
466
+ });
467
+
468
+ expect(result.id).toBe("doc-1");
469
+ expect(fetch).toHaveBeenCalledWith(
470
+ "https://api.agrenting.com/api/v1/uploads",
471
+ expect.objectContaining({ method: "POST" })
472
+ );
473
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
474
+ const body = JSON.parse(call[1].body);
475
+ // Verify content is base64-encoded
476
+ expect(body.content).toBe(Buffer.from("Do the thing").toString("base64"));
477
+ expect(body.name).toBe("instructions");
478
+ expect(body.content_type).toBe("text/plain");
479
+ expect(body.document_type).toBe("instructions");
480
+ });
481
+
482
+ it("includes task_id when provided", async () => {
483
+ const client = new AgrentingClient(mockConfig);
484
+ mockFetchResponse(200, {
485
+ data: {
486
+ id: "doc-2",
487
+ name: "instructions",
488
+ file_url: "https://cdn.agrenting.com/doc-2",
489
+ content_type: "text/plain",
490
+ file_hash: "def456",
491
+ document_type: "instructions",
492
+ },
493
+ });
494
+
495
+ const result = await client.uploadDocument({
496
+ name: "instructions",
497
+ content: "task instructions",
498
+ documentType: "instructions",
499
+ taskId: "task-123",
500
+ });
501
+
502
+ expect(result.id).toBe("doc-2");
503
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
504
+ const body = JSON.parse(call[1].body);
505
+ expect(body.task_id).toBe("task-123");
506
+ });
507
+ });
508
+
509
+ // -------------------------------------------------------------------------
510
+ // Retry logic
511
+ // -------------------------------------------------------------------------
512
+
513
+ describe("retry logic", () => {
514
+ it("retries on 500 errors and succeeds on retry", async () => {
515
+ const client = new AgrentingClient(mockConfig);
516
+ const fn = vi.fn()
517
+ .mockResolvedValueOnce({
518
+ ok: false,
519
+ status: 500,
520
+ headers: new Headers(),
521
+ text: async () => "Internal Server Error",
522
+ json: async () => ({}),
523
+ })
524
+ .mockResolvedValueOnce({
525
+ ok: true,
526
+ status: 200,
527
+ headers: new Headers(),
528
+ text: async () => JSON.stringify({ data: { id: "task-ok" } }),
529
+ json: async () => ({ data: { id: "task-ok" } }),
530
+ });
531
+
532
+ vi.stubGlobal("fetch", fn);
533
+
534
+ const result = await client.getTask("retry-test");
535
+
536
+ expect(result.id).toBe("task-ok");
537
+ expect(fn).toHaveBeenCalledTimes(2);
538
+ });
539
+
540
+ it("retries on 429 errors", async () => {
541
+ const client = new AgrentingClient(mockConfig);
542
+ const fn = vi.fn()
543
+ .mockResolvedValueOnce({
544
+ ok: false,
545
+ status: 429,
546
+ headers: new Headers({ "Retry-After": "0" }),
547
+ text: async () => "Rate limited",
548
+ json: async () => ({}),
549
+ })
550
+ .mockResolvedValueOnce({
551
+ ok: true,
552
+ status: 200,
553
+ headers: new Headers(),
554
+ text: async () => JSON.stringify({ data: { id: "r1" } }),
555
+ json: async () => ({ data: { id: "r1" } }),
556
+ });
557
+
558
+ vi.stubGlobal("fetch", fn);
559
+
560
+ const result = await client.getTask("r1");
561
+ expect(result.id).toBe("r1");
562
+ }, 30_000);
563
+
564
+ it("throws after exhausting retries on 500", async () => {
565
+ const client = new AgrentingClient(mockConfig);
566
+ vi.stubGlobal(
567
+ "fetch",
568
+ vi.fn().mockResolvedValue({
569
+ ok: false,
570
+ status: 500,
571
+ headers: new Headers(),
572
+ text: async () => "Internal Server Error",
573
+ json: async () => ({}),
574
+ })
575
+ );
576
+
577
+ await expect(client.getTask("fail")).rejects.toThrow("500");
578
+ }, 60_000);
579
+
580
+ it("retries on network errors and eventually throws", async () => {
581
+ const client = new AgrentingClient(mockConfig);
582
+ vi.stubGlobal(
583
+ "fetch",
584
+ vi.fn().mockRejectedValue(new Error("ECONNREFUSED"))
585
+ );
586
+
587
+ await expect(client.getTask("net-fail")).rejects.toThrow("ECONNREFUSED");
588
+ }, 60_000);
589
+ });
590
+
591
+ // -------------------------------------------------------------------------
592
+ // Error envelope handling
593
+ // -------------------------------------------------------------------------
594
+
595
+ describe("error envelope", () => {
596
+ it("throws when response contains errors array", { timeout: 15_000 }, async () => {
597
+ const client = new AgrentingClient(mockConfig);
598
+ mockFetchResponse(200, {
599
+ data: null,
600
+ errors: ["Something went wrong", "Invalid parameter"],
601
+ });
602
+
603
+ await expect(client.getTask("err")).rejects.toThrow("Something went wrong");
604
+ });
605
+ });
606
+
607
+ // -------------------------------------------------------------------------
608
+ // createPaymentIntent
609
+ // -------------------------------------------------------------------------
610
+
611
+ describe("createPaymentIntent", () => {
612
+ it("creates a payment intent", async () => {
613
+ const client = new AgrentingClient(mockConfig);
614
+ mockFetchResponse(200, {
615
+ data: { id: "pi-1", status: "pending", payment_url: "https://pay.example.com" },
616
+ });
617
+
618
+ const result = await client.createPaymentIntent({
619
+ amount: "100.00",
620
+ currency: "USD",
621
+ paymentType: "crypto",
622
+ });
623
+
624
+ expect(result.id).toBe("pi-1");
625
+ expect(result.payment_url).toBe("https://pay.example.com");
626
+ });
627
+ });
628
+
629
+ // -------------------------------------------------------------------------
630
+ // hireAgent
631
+ // -------------------------------------------------------------------------
632
+
633
+ describe("hireAgent", () => {
634
+ it("hires an agent and returns adapter config", async () => {
635
+ const client = new AgrentingClient(mockConfig);
636
+ mockFetchResponse(200, {
637
+ data: {
638
+ agent_did: "did:agrenting:hire-me",
639
+ adapter_config: {
640
+ agrentingUrl: "https://www.agrenting.com",
641
+ agentDid: "did:agrenting:hire-me",
642
+ pricingModel: "fixed",
643
+ },
644
+ status: "hired",
645
+ hired_at: "2026-04-13T10:00:00Z",
646
+ },
647
+ });
648
+
649
+ const result = await client.hireAgent("did:agrenting:hire-me");
650
+
651
+ expect(result.agent_did).toBe("did:agrenting:hire-me");
652
+ expect(result.status).toBe("hired");
653
+ expect(result.adapter_config.pricingModel).toBe("fixed");
654
+ });
655
+
656
+ it("sends pricing model in request body", async () => {
657
+ const client = new AgrentingClient(mockConfig);
658
+ mockFetchResponse(200, {
659
+ data: {
660
+ agent_did: "did:agrenting:x",
661
+ adapter_config: { agrentingUrl: "https://www.agrenting.com", agentDid: "did:agrenting:x", pricingModel: "per-token" },
662
+ status: "hired",
663
+ hired_at: "2026-04-13T10:00:00Z",
664
+ },
665
+ });
666
+
667
+ await client.hireAgent("did:agrenting:x", { pricingModel: "per-token" });
668
+
669
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls.at(-1)!;
670
+ const body = JSON.parse(call[1].body);
671
+ expect(body.pricing_model).toBe("per-token");
672
+ });
673
+ });
674
+
675
+ // -------------------------------------------------------------------------
676
+ // getAgentProfile
677
+ // -------------------------------------------------------------------------
678
+
679
+ describe("getAgentProfile", () => {
680
+ it("fetches agent profile by DID", async () => {
681
+ const client = new AgrentingClient(mockConfig);
682
+ mockFetchResponse(200, {
683
+ data: {
684
+ id: "agent-1",
685
+ did: "did:agrenting:test-agent",
686
+ name: "Test Agent",
687
+ description: "A test agent",
688
+ capabilities: ["code-review", "data-analysis"],
689
+ pricing_model: "fixed",
690
+ base_price: "10.00",
691
+ reputation_score: 4.5,
692
+ total_tasks_completed: 100,
693
+ verified: true,
694
+ availability_status: "available",
695
+ },
696
+ });
697
+
698
+ const result = await client.getAgentProfile("did:agrenting:test-agent");
699
+
700
+ expect(result.did).toBe("did:agrenting:test-agent");
701
+ expect(result.name).toBe("Test Agent");
702
+ expect(result.capabilities).toContain("code-review");
703
+ expect(result.reputation_score).toBe(4.5);
704
+ expect(result.availability_status).toBe("available");
705
+ });
706
+ });
707
+
708
+ // -------------------------------------------------------------------------
709
+ // sendMessageToTask
710
+ // -------------------------------------------------------------------------
711
+
712
+ describe("sendMessageToTask", () => {
713
+ it("sends a message to a running task", async () => {
714
+ const client = new AgrentingClient(mockConfig);
715
+ mockFetchResponse(200, {
716
+ data: { message_id: "msg-1", task_id: "task-42", sent_at: "2026-04-13T10:00:00Z" },
717
+ });
718
+
719
+ const result = await client.sendMessageToTask("task-42", { message: "Please add error handling" });
720
+
721
+ expect(result.message_id).toBe("msg-1");
722
+ expect(result.task_id).toBe("task-42");
723
+ });
724
+
725
+ it("sends message type in request body", async () => {
726
+ const client = new AgrentingClient(mockConfig);
727
+ mockFetchResponse(200, {
728
+ data: { message_id: "msg-2", task_id: "task-42", sent_at: "2026-04-13T10:00:00Z" },
729
+ });
730
+
731
+ await client.sendMessageToTask("task-42", { message: "Good job!", messageType: "feedback" });
732
+
733
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls.at(-1)!;
734
+ const body = JSON.parse(call[1].body);
735
+ expect(body.message_type).toBe("feedback");
736
+ });
737
+ });
738
+
739
+ // -------------------------------------------------------------------------
740
+ // reassignTask
741
+ // -------------------------------------------------------------------------
742
+
743
+ describe("reassignTask", () => {
744
+ it("reassigns a task to a new agent", async () => {
745
+ const client = new AgrentingClient(mockConfig);
746
+ mockFetchResponse(200, {
747
+ data: {
748
+ task_id: "task-99",
749
+ previous_agent_did: "did:agrenting:old",
750
+ new_agent_did: "did:agrenting:new",
751
+ reassigned_at: "2026-04-13T10:00:00Z",
752
+ },
753
+ });
754
+
755
+ const result = await client.reassignTask("task-99", "did:agrenting:new");
756
+
757
+ expect(result.task_id).toBe("task-99");
758
+ expect(result.previous_agent_did).toBe("did:agrenting:old");
759
+ expect(result.new_agent_did).toBe("did:agrenting:new");
760
+ });
761
+
762
+ it("reassigns without specifying agent (platform picks best)", async () => {
763
+ const client = new AgrentingClient(mockConfig);
764
+ mockFetchResponse(200, {
765
+ data: {
766
+ task_id: "task-100",
767
+ previous_agent_did: "did:agrenting:old",
768
+ new_agent_did: "did:agrenting:auto-picked",
769
+ reassigned_at: "2026-04-13T10:00:00Z",
770
+ },
771
+ });
772
+
773
+ const result = await client.reassignTask("task-100");
774
+
775
+ expect(result.new_agent_did).toBe("did:agrenting:auto-picked");
776
+ // Verify empty body was sent
777
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls.at(-1)!;
778
+ const body = JSON.parse(call[1].body);
779
+ expect(body.new_agent_did).toBeUndefined();
780
+ });
781
+ });
782
+
783
+ describe("listCapabilities", () => {
784
+ it("GETs capabilities list", async () => {
785
+ const client = new AgrentingClient(mockConfig);
786
+ mockFetchResponse(200, {
787
+ data: [
788
+ { name: "code-review", description: "Review code quality", category: "development", agent_count: 10 },
789
+ { name: "data-analysis", description: "Analyze datasets", category: "analytics", agent_count: 5 },
790
+ ],
791
+ });
792
+
793
+ const result = await client.listCapabilities();
794
+
795
+ expect(result).toHaveLength(2);
796
+ expect(result[0].name).toBe("code-review");
797
+ expect(result[0].agent_count).toBe(10);
798
+ expect(fetch).toHaveBeenCalledWith(
799
+ "https://api.agrenting.com/api/v1/capabilities",
800
+ expect.objectContaining({ method: "GET" })
801
+ );
802
+ });
803
+ });
804
+
805
+ describe("sendMessageToHiring", () => {
806
+ it("POSTs message to hiring messages endpoint", async () => {
807
+ const client = new AgrentingClient(mockConfig);
808
+ mockFetchResponse(200, {
809
+ data: {
810
+ id: "msg-1",
811
+ hiring_id: "h-123",
812
+ sender_agent_id: "client-1",
813
+ content: "Hello from client",
814
+ created_at: "2025-01-01T00:00:00Z",
815
+ },
816
+ });
817
+
818
+ const result = await client.sendMessageToHiring("h-123", "Hello from client");
819
+
820
+ expect(result.id).toBe("msg-1");
821
+ expect(result.content).toBe("Hello from client");
822
+ expect(fetch).toHaveBeenCalledWith(
823
+ "https://api.agrenting.com/api/v1/hirings/h-123/messages",
824
+ expect.objectContaining({ method: "POST" })
825
+ );
826
+ });
827
+
828
+ it("throws when message exceeds 5000 chars", async () => {
829
+ const client = new AgrentingClient(mockConfig);
830
+ const longMessage = "x".repeat(5001);
831
+
832
+ await expect(client.sendMessageToHiring("h-123", longMessage)).rejects.toThrow(
833
+ "Message content exceeds 5000 character limit"
834
+ );
835
+ });
836
+ });
837
+
838
+ describe("getHiringMessages", () => {
839
+ it("GETs messages for a hiring", async () => {
840
+ const client = new AgrentingClient(mockConfig);
841
+ mockFetchResponse(200, {
842
+ data: [
843
+ { id: "msg-1", hiring_id: "h-123", sender_agent_id: "client-1", content: "Hello", created_at: "2025-01-01T00:00:00Z" },
844
+ ],
845
+ });
846
+
847
+ const result = await client.getHiringMessages("h-123");
848
+
849
+ expect(result).toHaveLength(1);
850
+ expect(fetch).toHaveBeenCalledWith(
851
+ "https://api.agrenting.com/api/v1/hirings/h-123/messages",
852
+ expect.objectContaining({ method: "GET" })
853
+ );
854
+ });
855
+ });
856
+
857
+ describe("retryHiring", () => {
858
+ it("POSTs to retry endpoint", async () => {
859
+ const client = new AgrentingClient(mockConfig);
860
+ mockFetchResponse(200, {
861
+ data: {
862
+ id: "h-123",
863
+ agent_id: "agent-1",
864
+ agent_did: "did:agrenting:test-agent",
865
+ client_agent_id: "client-1",
866
+ status: "active",
867
+ created_at: "2025-01-01T00:00:00Z",
868
+ updated_at: "2025-01-01T00:05:00Z",
869
+ },
870
+ });
871
+
872
+ const result = await client.retryHiring("h-123", { reason: "previous timeout" });
873
+
874
+ expect(result.id).toBe("h-123");
875
+ expect(result.status).toBe("active");
876
+ expect(fetch).toHaveBeenCalledWith(
877
+ "https://api.agrenting.com/api/v1/hirings/h-123/retry",
878
+ expect.objectContaining({ method: "POST" })
879
+ );
880
+ const call = (fetch as ReturnType<typeof vi.fn>).mock.calls[0];
881
+ const body = JSON.parse(call[1].body);
882
+ expect(body.reason).toBe("previous timeout");
883
+ });
884
+ });
885
+
886
+ describe("getHiring", () => {
887
+ it("GETs hiring by ID", async () => {
888
+ const client = new AgrentingClient(mockConfig);
889
+ mockFetchResponse(200, {
890
+ data: {
891
+ id: "h-123",
892
+ agent_id: "agent-1",
893
+ agent_did: "did:agrenting:test-agent",
894
+ client_agent_id: "client-1",
895
+ status: "active",
896
+ created_at: "2025-01-01T00:00:00Z",
897
+ updated_at: "2025-01-01T00:00:00Z",
898
+ },
899
+ });
900
+
901
+ const result = await client.getHiring("h-123");
902
+
903
+ expect(result.id).toBe("h-123");
904
+ expect(fetch).toHaveBeenCalledWith(
905
+ "https://api.agrenting.com/api/v1/hirings/h-123",
906
+ expect.objectContaining({ method: "GET" })
907
+ );
908
+ });
909
+ });
910
+
911
+ describe("listHirings", () => {
912
+ it("GETs hirings list with filters", async () => {
913
+ const client = new AgrentingClient(mockConfig);
914
+ mockFetchResponse(200, {
915
+ data: [
916
+ { id: "h-1", agent_id: "agent-1", status: "active", created_at: "2025-01-01T00:00:00Z" },
917
+ { id: "h-2", agent_id: "agent-2", status: "completed", created_at: "2025-01-02T00:00:00Z" },
918
+ ],
919
+ });
920
+
921
+ const result = await client.listHirings({ status: "active", limit: 10 });
922
+
923
+ expect(result).toHaveLength(2);
924
+ const url = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
925
+ expect(url).toContain("status=active");
926
+ expect(url).toContain("limit=10");
927
+ });
928
+ });
929
+
930
+ describe("listAgentsByCapability", () => {
931
+ it("GETs agents filtered by capability", async () => {
932
+ const client = new AgrentingClient(mockConfig);
933
+ mockFetchResponse(200, {
934
+ data: [
935
+ { id: "agent-1", did: "did:agrenting:agent-1", name: "Agent 1", capabilities: ["code-review"] },
936
+ { id: "agent-2", did: "did:agrenting:agent-2", name: "Agent 2", capabilities: ["code-review"] },
937
+ ],
938
+ });
939
+
940
+ const result = await client.listAgentsByCapability("code-review");
941
+
942
+ expect(result).toHaveLength(2);
943
+ expect(fetch).toHaveBeenCalledWith(
944
+ expect.stringContaining("/api/v1/agents?capability=code-review"),
945
+ expect.objectContaining({ method: "GET" })
946
+ );
947
+ });
948
+ });
949
+ });