@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,147 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ checkBalance,
4
+ canSubmitTask,
5
+ formatLowBalanceComment,
6
+ formatInsufficientBalanceComment,
7
+ } from "./balance-monitor.js";
8
+
9
+ const mockConfig = {
10
+ agrentingUrl: "https://example.agrenting.com",
11
+ apiKey: "test-key",
12
+ agentDid: "did:agrenting:test",
13
+ };
14
+
15
+ function mockBalanceResponse(available: string, escrow = "0", total = available, currency = "USD") {
16
+ vi.stubGlobal(
17
+ "fetch",
18
+ vi.fn().mockResolvedValue({
19
+ ok: true,
20
+ json: async () => ({ available, escrow, total, currency }),
21
+ })
22
+ );
23
+ }
24
+
25
+ function mockFetchError() {
26
+ vi.stubGlobal(
27
+ "fetch",
28
+ vi.fn().mockRejectedValue(new Error("Network error"))
29
+ );
30
+ }
31
+
32
+ beforeEach(() => {
33
+ vi.unstubAllGlobals();
34
+ });
35
+
36
+ describe("checkBalance", () => {
37
+ it("returns balance info when API returns valid numbers", async () => {
38
+ mockBalanceResponse("50.00", "10.00", "60.00");
39
+ const result = await checkBalance({ config: mockConfig });
40
+ expect(result).toEqual({
41
+ available: 50,
42
+ escrow: 10,
43
+ total: 60,
44
+ currency: "USD",
45
+ isLow: false,
46
+ isInsufficient: false,
47
+ });
48
+ });
49
+
50
+ it("marks balance as low when below threshold", async () => {
51
+ mockBalanceResponse("5.00", "0", "5.00"); // $5, below $10 threshold
52
+ const result = await checkBalance({ config: mockConfig });
53
+ expect(result.isLow).toBe(true);
54
+ expect(result.isInsufficient).toBe(false);
55
+ });
56
+
57
+ it("marks balance as insufficient when below minimum submission", async () => {
58
+ mockBalanceResponse("0.50", "0", "0.50"); // $0.50, below $1 threshold
59
+ const result = await checkBalance({ config: mockConfig });
60
+ expect(result.isLow).toBe(true);
61
+ expect(result.isInsufficient).toBe(true);
62
+ });
63
+
64
+ it("handles NaN balance gracefully", async () => {
65
+ mockBalanceResponse("not-a-number", "0", "not-a-number");
66
+ const result = await checkBalance({ config: mockConfig });
67
+ expect(result).toEqual({
68
+ available: 0,
69
+ escrow: 0,
70
+ total: 0,
71
+ currency: "USD",
72
+ isLow: true,
73
+ isInsufficient: true,
74
+ });
75
+ });
76
+
77
+ it("handles fetch errors by returning insufficient balance", { timeout: 15_000 }, async () => {
78
+ mockFetchError();
79
+ const result = await checkBalance({ config: mockConfig });
80
+ expect(result).toEqual({
81
+ available: 0,
82
+ escrow: 0,
83
+ total: 0,
84
+ currency: "USD",
85
+ isLow: true,
86
+ isInsufficient: true,
87
+ });
88
+ });
89
+
90
+ it("respects custom thresholds", async () => {
91
+ mockBalanceResponse("20.00", "0", "20.00"); // $20
92
+ const result = await checkBalance({
93
+ config: mockConfig,
94
+ lowBalanceThresholdUsd: 50,
95
+ minSubmissionBalanceUsd: 5,
96
+ });
97
+ expect(result.isLow).toBe(true);
98
+ expect(result.isInsufficient).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe("canSubmitTask", () => {
103
+ it("returns ok when balance is sufficient", async () => {
104
+ mockBalanceResponse("50.00");
105
+ const result = await canSubmitTask({ config: mockConfig });
106
+ expect(result).toEqual({ ok: true });
107
+ });
108
+
109
+ it("returns reason when balance is insufficient", async () => {
110
+ mockBalanceResponse("0.50");
111
+ const result = await canSubmitTask({ config: mockConfig });
112
+ expect(result).toEqual({
113
+ ok: false,
114
+ reason: "Insufficient balance: $0.50. Minimum required: $1.00.",
115
+ });
116
+ });
117
+ });
118
+
119
+ describe("formatLowBalanceComment", () => {
120
+ it("returns a markdown warning comment", () => {
121
+ const comment = formatLowBalanceComment({
122
+ available: 8,
123
+ escrow: 2,
124
+ total: 10,
125
+ currency: "USD",
126
+ isLow: true,
127
+ isInsufficient: false,
128
+ });
129
+ expect(comment).toContain("**Agrenting balance warning**");
130
+ expect(comment).toContain("$8.00");
131
+ });
132
+ });
133
+
134
+ describe("formatInsufficientBalanceComment", () => {
135
+ it("returns a markdown blocking comment", () => {
136
+ const comment = formatInsufficientBalanceComment({
137
+ available: 0,
138
+ escrow: 0,
139
+ total: 0,
140
+ currency: "USD",
141
+ isLow: true,
142
+ isInsufficient: true,
143
+ });
144
+ expect(comment).toContain("**Agrenting task blocked**");
145
+ expect(comment).toContain("$0.00");
146
+ });
147
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Balance monitoring for the Agrenting adapter.
3
+ *
4
+ * Monitors the platform balance via `GET /api/v1/ledger/balance`
5
+ * and provides low-balance warnings and pre-submission checks.
6
+ */
7
+
8
+ import { AgrentingClient } from "./client.js";
9
+ import type { AgrentingAdapterConfig } from "./types.js";
10
+
11
+ export interface BalanceInfo {
12
+ available: number;
13
+ escrow: number;
14
+ total: number;
15
+ currency: string;
16
+ isLow: boolean;
17
+ isInsufficient: boolean;
18
+ }
19
+
20
+ export interface BalanceCheckOptions {
21
+ config: AgrentingAdapterConfig;
22
+ /** Low balance threshold in USD (default: $10) */
23
+ lowBalanceThresholdUsd?: number;
24
+ /** Minimum balance required to submit a task in USD (default: $1) */
25
+ minSubmissionBalanceUsd?: number;
26
+ }
27
+
28
+ /**
29
+ * Fetch the current platform balance and evaluate thresholds.
30
+ * The server returns { available, escrow, total } — we use `available`
31
+ * for threshold checks since escrowed funds are already committed.
32
+ */
33
+ export async function checkBalance(
34
+ options: BalanceCheckOptions
35
+ ): Promise<BalanceInfo> {
36
+ const client = new AgrentingClient(options.config);
37
+ const lowThreshold = options.lowBalanceThresholdUsd ?? 10;
38
+ const minSubmission = options.minSubmissionBalanceUsd ?? 1;
39
+
40
+ try {
41
+ const raw = await client.getBalance();
42
+ const available = parseFloat(raw.available);
43
+ const escrow = parseFloat(raw.escrow);
44
+ const total = parseFloat(raw.total);
45
+
46
+ const anyNaN = Number.isNaN(available) || Number.isNaN(escrow) || Number.isNaN(total);
47
+ if (anyNaN) {
48
+ return {
49
+ available: 0,
50
+ escrow: 0,
51
+ total: 0,
52
+ currency: raw.currency ?? "USD",
53
+ isLow: true,
54
+ isInsufficient: true,
55
+ };
56
+ }
57
+
58
+ return {
59
+ available,
60
+ escrow,
61
+ total,
62
+ currency: raw.currency ?? "USD",
63
+ isLow: available < lowThreshold,
64
+ isInsufficient: available < minSubmission,
65
+ };
66
+ } catch {
67
+ // If we can't fetch balance, assume insufficient to avoid failed submissions
68
+ return {
69
+ available: 0,
70
+ escrow: 0,
71
+ total: 0,
72
+ currency: "USD",
73
+ isLow: true,
74
+ isInsufficient: true,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Pre-submission balance check.
81
+ * Returns { ok: true } if sufficient, or { ok: false, reason } if not.
82
+ */
83
+ export async function canSubmitTask(
84
+ options: BalanceCheckOptions
85
+ ): Promise<{ ok: true } | { ok: false; reason: string }> {
86
+ const balanceInfo = await checkBalance(options);
87
+
88
+ if (balanceInfo.isInsufficient) {
89
+ return {
90
+ ok: false,
91
+ reason: `Insufficient balance: ${formatUsd(balanceInfo.available)}. Minimum required: ${formatUsd(options.minSubmissionBalanceUsd ?? 1)}.`,
92
+ };
93
+ }
94
+
95
+ return { ok: true };
96
+ }
97
+
98
+ /**
99
+ * Format a USD amount as a human-readable dollar string.
100
+ */
101
+ function formatUsd(amount: number): string {
102
+ return `$${amount.toFixed(2)}`;
103
+ }
104
+
105
+ /**
106
+ * Generate a warning comment for low balance.
107
+ * Returns the markdown comment to post on the Paperclip issue.
108
+ */
109
+ export function formatLowBalanceComment(balanceInfo: BalanceInfo): string {
110
+ return `**Agrenting balance warning** — Available: ${formatUsd(balanceInfo.available)} ${balanceInfo.currency} (Escrowed: ${formatUsd(balanceInfo.escrow)}). Funds are running low. Add credits to avoid task submission failures.`;
111
+ }
112
+
113
+ /**
114
+ * Generate a blocking comment for insufficient balance.
115
+ */
116
+ export function formatInsufficientBalanceComment(balanceInfo: BalanceInfo): string {
117
+ return `**Agrenting task blocked** — Insufficient balance: ${formatUsd(balanceInfo.available)} ${balanceInfo.currency}. Please add credits to your Agrenting account before retrying.`;
118
+ }