@umpledger/sdk 2.0.0-alpha.1

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,525 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { UMP, PricingTemplates, PricingEngine } from '../src';
3
+
4
+ describe('UMP SDK v2.0', () => {
5
+ let ump: UMP;
6
+
7
+ beforeEach(() => {
8
+ ump = new UMP({ apiKey: 'ump_sk_test_123' });
9
+ });
10
+
11
+ // ═══════════════════════════════════════════════════════════
12
+ // LAYER 1: Identity & Value
13
+ // ═══════════════════════════════════════════════════════════
14
+
15
+ describe('Agent Identity', () => {
16
+ it('should create an agent with authority scope', () => {
17
+ const agent = ump.agents.create({
18
+ name: 'test-agent',
19
+ type: 'AI_AGENT',
20
+ authority: { maxPerTransaction: '$50', maxPerDay: '$500' },
21
+ });
22
+
23
+ expect(agent.agentId).toMatch(/^agt_/);
24
+ expect(agent.displayName).toBe('test-agent');
25
+ expect(agent.agentType).toBe('AI_AGENT');
26
+ expect(agent.authorityScope.maxPerTransaction).toBe(50);
27
+ expect(agent.authorityScope.maxPerDay).toBe(500);
28
+ expect(agent.status).toBe('ACTIVE');
29
+ });
30
+
31
+ it('should enforce child authority cannot exceed parent', () => {
32
+ const parent = ump.agents.create({
33
+ name: 'parent-org',
34
+ type: 'ORGANIZATION',
35
+ authority: { maxPerTransaction: '$100', maxPerDay: '$1000' },
36
+ });
37
+
38
+ const child = ump.agents.create({
39
+ name: 'child-agent',
40
+ type: 'AI_AGENT',
41
+ parentId: parent.agentId,
42
+ authority: { maxPerTransaction: '$200', maxPerDay: '$2000' }, // exceeds parent
43
+ });
44
+
45
+ // Child should be capped at parent's limits
46
+ expect(child.authorityScope.maxPerTransaction).toBe(100);
47
+ expect(child.authorityScope.maxPerDay).toBe(1000);
48
+ });
49
+
50
+ it('should cascade revocation to children', () => {
51
+ const parent = ump.agents.create({
52
+ name: 'parent',
53
+ type: 'ORGANIZATION',
54
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
55
+ });
56
+
57
+ const child1 = ump.agents.create({
58
+ name: 'child-1',
59
+ type: 'AI_AGENT',
60
+ parentId: parent.agentId,
61
+ authority: { maxPerTransaction: 50, maxPerDay: 500 },
62
+ });
63
+
64
+ const child2 = ump.agents.create({
65
+ name: 'child-2',
66
+ type: 'AI_AGENT',
67
+ parentId: parent.agentId,
68
+ authority: { maxPerTransaction: 50, maxPerDay: 500 },
69
+ });
70
+
71
+ const revoked = ump.agents.revoke(parent.agentId);
72
+ expect(revoked).toContain(parent.agentId);
73
+ expect(revoked).toContain(child1.agentId);
74
+ expect(revoked).toContain(child2.agentId);
75
+ expect(ump.agents.get(child1.agentId).status).toBe('REVOKED');
76
+ });
77
+
78
+ it('should check authority for counterparty allowlist', () => {
79
+ const agent = ump.agents.create({
80
+ name: 'restricted-agent',
81
+ type: 'AI_AGENT',
82
+ authority: {
83
+ maxPerTransaction: 100,
84
+ maxPerDay: 1000,
85
+ allowedCounterparties: ['agt_acme_*', 'agt_specific_123'],
86
+ },
87
+ });
88
+
89
+ const allowed = ump.agents.checkAuthority(agent.agentId, 50, 'agt_acme_test');
90
+ expect(allowed.allowed).toBe(true);
91
+
92
+ const blocked = ump.agents.checkAuthority(agent.agentId, 50, 'agt_evil_corp');
93
+ expect(blocked.allowed).toBe(false);
94
+ expect(blocked.reason).toContain('not in allowlist');
95
+ });
96
+ });
97
+
98
+ describe('Wallet', () => {
99
+ it('should create a wallet and fund it', () => {
100
+ const handle = ump.registerAgent({
101
+ name: 'funded-agent',
102
+ type: 'AI_AGENT',
103
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
104
+ });
105
+
106
+ handle.wallet.fund({ amount: '$100' });
107
+ expect(handle.wallet.balance()).toBe(100);
108
+ });
109
+
110
+ it('should debit and credit between wallets', () => {
111
+ const source = ump.registerAgent({
112
+ name: 'buyer',
113
+ type: 'AI_AGENT',
114
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
115
+ });
116
+ const target = ump.registerAgent({
117
+ name: 'seller',
118
+ type: 'SERVICE',
119
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
120
+ });
121
+
122
+ source.wallet.fund({ amount: 100 });
123
+
124
+ const sourceWallet = ump.wallets.getByAgent(source.id);
125
+ const targetWallet = ump.wallets.getByAgent(target.id);
126
+
127
+ ump.wallets.debit(sourceWallet.walletId, 25, target.id, 'txn_test');
128
+ ump.wallets.credit(targetWallet.walletId, 25, source.id, 'txn_test');
129
+
130
+ expect(source.wallet.balance()).toBe(75);
131
+ expect(target.wallet.balance()).toBe(25);
132
+ });
133
+
134
+ it('should reject transactions on frozen wallet', () => {
135
+ const handle = ump.registerAgent({
136
+ name: 'freeze-test',
137
+ type: 'AI_AGENT',
138
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
139
+ });
140
+
141
+ handle.wallet.fund({ amount: 100 });
142
+ handle.wallet.freeze();
143
+
144
+ const wallet = ump.wallets.getByAgent(handle.id);
145
+ expect(() => {
146
+ ump.wallets.debit(wallet.walletId, 10, 'other', 'txn_test');
147
+ }).toThrow('frozen');
148
+ });
149
+
150
+ it('should reject insufficient funds', () => {
151
+ const handle = ump.registerAgent({
152
+ name: 'broke-agent',
153
+ type: 'AI_AGENT',
154
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
155
+ });
156
+
157
+ handle.wallet.fund({ amount: 5 });
158
+
159
+ const wallet = ump.wallets.getByAgent(handle.id);
160
+ expect(() => {
161
+ ump.wallets.debit(wallet.walletId, 50, 'other', 'txn_test');
162
+ }).toThrow('Insufficient funds');
163
+ });
164
+
165
+ it('should maintain immutable ledger', () => {
166
+ const handle = ump.registerAgent({
167
+ name: 'ledger-test',
168
+ type: 'AI_AGENT',
169
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
170
+ });
171
+
172
+ const wallet = ump.wallets.getByAgent(handle.id);
173
+ handle.wallet.fund({ amount: 100 });
174
+ ump.wallets.debit(wallet.walletId, 30, 'other', 'txn_1');
175
+ ump.wallets.credit(wallet.walletId, 10, 'other', 'txn_2');
176
+
177
+ const ledger = ump.wallets.getLedger(wallet.walletId);
178
+ expect(ledger).toHaveLength(3); // fund + debit + credit
179
+ expect(ledger[0].type).toBe('TOPUP');
180
+ expect(ledger[1].type).toBe('DEBIT');
181
+ expect(ledger[2].type).toBe('CREDIT');
182
+ });
183
+ });
184
+
185
+ // ═══════════════════════════════════════════════════════════
186
+ // LAYER 2: Terms & Metering
187
+ // ═══════════════════════════════════════════════════════════
188
+
189
+ describe('Pricing Engine', () => {
190
+ const engine = new PricingEngine();
191
+
192
+ it('should evaluate FIXED pricing', () => {
193
+ const amount = engine.simulate({
194
+ ruleId: 'r1', name: 'Flat', primitive: 'FIXED',
195
+ amount: 9.99, period: 'MONTHLY',
196
+ }, 1);
197
+ expect(amount).toBe(9.99);
198
+ });
199
+
200
+ it('should evaluate UNIT_RATE pricing', () => {
201
+ const amount = engine.simulate({
202
+ ruleId: 'r1', name: 'Per-token', primitive: 'UNIT_RATE',
203
+ rate: 0.00003, unit: 'TOKEN',
204
+ }, 1000);
205
+ expect(amount).toBe(0.03);
206
+ });
207
+
208
+ it('should evaluate TIERED graduated pricing', () => {
209
+ const amount = engine.simulate({
210
+ ruleId: 'r1', name: 'Tiered', primitive: 'TIERED',
211
+ mode: 'GRADUATED',
212
+ tiers: [
213
+ { from: 0, to: 100, rate: 0.10 },
214
+ { from: 100, to: 1000, rate: 0.05 },
215
+ { from: 1000, to: null, rate: 0.01 },
216
+ ],
217
+ }, 500);
218
+ // First 100 at $0.10 = $10, next 400 at $0.05 = $20 → $30
219
+ expect(amount).toBe(30);
220
+ });
221
+
222
+ it('should evaluate PERCENTAGE with min/max', () => {
223
+ const amount = engine.simulate({
224
+ ruleId: 'r1', name: 'Commission', primitive: 'PERCENTAGE',
225
+ percentage: 0.10, referenceField: 'transaction_amount',
226
+ min: 1.00, max: 50.00,
227
+ }, 1, { transaction_amount: 5 });
228
+ // 10% of $5 = $0.50, but min is $1.00
229
+ expect(amount).toBe(1);
230
+ });
231
+
232
+ it('should evaluate COMPOSITE (ADD) — hybrid pricing', () => {
233
+ const amount = engine.simulate({
234
+ ruleId: 'r1', name: 'Hybrid', primitive: 'COMPOSITE',
235
+ operator: 'ADD',
236
+ rules: [
237
+ { ruleId: 'r2', name: 'Base', primitive: 'FIXED', amount: 10, period: 'MONTHLY' },
238
+ { ruleId: 'r3', name: 'Usage', primitive: 'UNIT_RATE', rate: 0.05, unit: 'CALL' },
239
+ ],
240
+ }, 100);
241
+ // $10 fixed + 100 × $0.05 = $15
242
+ expect(amount).toBe(15);
243
+ });
244
+
245
+ it('should use PricingTemplates.perToken', () => {
246
+ const rule = PricingTemplates.perToken(30, 60); // $30/M input, $60/M output
247
+ const inputCost = engine.simulate(rule, 1_000_000, { direction: 'input' });
248
+ const outputCost = engine.simulate(rule, 1_000_000, { direction: 'output' });
249
+ expect(inputCost).toBe(30);
250
+ expect(outputCost).toBe(60);
251
+ });
252
+
253
+ it('should explain pricing calculations', () => {
254
+ const rule = PricingTemplates.subscriptionPlusUsage(99, 1000, 0.05);
255
+ const explanation = engine.explain(rule, {
256
+ event: {
257
+ eventId: 'e1', sourceAgentId: 's', targetAgentId: 't',
258
+ contractId: 'c', serviceId: 'svc', timestamp: new Date(),
259
+ quantity: 1500, unit: 'CALL', dimensions: {},
260
+ },
261
+ });
262
+ expect(explanation).toContain('COMPOSITE');
263
+ expect(explanation).toContain('FIXED');
264
+ expect(explanation).toContain('THRESHOLD');
265
+ });
266
+ });
267
+
268
+ describe('Contracts', () => {
269
+ it('should create and find active contracts', () => {
270
+ const agent1 = ump.agents.create({
271
+ name: 'buyer', type: 'AI_AGENT',
272
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
273
+ });
274
+ const agent2 = ump.agents.create({
275
+ name: 'seller', type: 'SERVICE',
276
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
277
+ });
278
+
279
+ const contract = ump.contracts.create(agent1.agentId, {
280
+ targetAgentId: agent2.agentId,
281
+ pricingRules: [{
282
+ ruleId: 'r1', name: 'Per-call', primitive: 'UNIT_RATE',
283
+ rate: 0.01, unit: 'API_CALL',
284
+ }],
285
+ });
286
+
287
+ expect(contract.status).toBe('ACTIVE');
288
+
289
+ const found = ump.contracts.findActive(agent1.agentId, agent2.agentId);
290
+ expect(found?.contractId).toBe(contract.contractId);
291
+ });
292
+
293
+ it('should support dynamic negotiation flow', () => {
294
+ const a1 = ump.agents.create({
295
+ name: 'a1', type: 'AI_AGENT',
296
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
297
+ });
298
+ const a2 = ump.agents.create({
299
+ name: 'a2', type: 'AI_AGENT',
300
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
301
+ });
302
+
303
+ // a1 proposes
304
+ const proposal = ump.contracts.propose(a1.agentId, {
305
+ targetAgentId: a2.agentId,
306
+ pricingRules: [{
307
+ name: 'Per-file', primitive: 'UNIT_RATE', rate: 0.50, unit: 'FILE',
308
+ } as any],
309
+ });
310
+ expect(proposal.status).toBe('PROPOSED');
311
+
312
+ // a2 counters
313
+ const counter = ump.contracts.counter(proposal.contractId, a2.agentId, [{
314
+ name: 'Per-file', primitive: 'UNIT_RATE', rate: 0.35, unit: 'FILE',
315
+ } as any]);
316
+ expect(counter.status).toBe('PROPOSED');
317
+ expect(counter.metadata).toHaveProperty('counterTo');
318
+
319
+ // a1 accepts
320
+ const accepted = ump.contracts.accept(counter.contractId);
321
+ expect(accepted.status).toBe('ACTIVE');
322
+ });
323
+ });
324
+
325
+ describe('Metering', () => {
326
+ it('should record usage events idempotently', () => {
327
+ const e1 = ump.metering.record({
328
+ eventId: 'evt_dedup_test',
329
+ sourceAgentId: 's1', targetAgentId: 't1',
330
+ contractId: 'c1', serviceId: 'svc1',
331
+ quantity: 100, unit: 'TOKEN', dimensions: {},
332
+ });
333
+
334
+ // Resubmit same ID → should return original
335
+ const e2 = ump.metering.record({
336
+ eventId: 'evt_dedup_test',
337
+ sourceAgentId: 's1', targetAgentId: 't1',
338
+ contractId: 'c1', serviceId: 'svc1',
339
+ quantity: 999, unit: 'TOKEN', dimensions: {},
340
+ });
341
+
342
+ expect(e2.quantity).toBe(100); // original, not 999
343
+ expect(e1.eventId).toBe(e2.eventId);
344
+ });
345
+ });
346
+
347
+ // ═══════════════════════════════════════════════════════════
348
+ // LAYER 3: Settlement & Governance
349
+ // ═══════════════════════════════════════════════════════════
350
+
351
+ describe('Settlement Bus', () => {
352
+ it('should execute instant drawdown settlement', () => {
353
+ const buyer = ump.registerAgent({
354
+ name: 'buyer', type: 'AI_AGENT',
355
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
356
+ });
357
+ const seller = ump.registerAgent({
358
+ name: 'seller', type: 'SERVICE',
359
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
360
+ });
361
+
362
+ buyer.wallet.fund({ amount: 100 });
363
+
364
+ const result = ump.settlement.settleInstant(
365
+ buyer.id, seller.id,
366
+ [{
367
+ ratedRecordId: 'r1', usageEventId: 'e1', contractId: 'c1',
368
+ pricingRuleId: 'p1', quantity: 1000, rate: 0.00003,
369
+ amount: 0.03, currency: 'USD', ratedAt: new Date(),
370
+ }],
371
+ );
372
+
373
+ expect(result.settlement.status).toBe('SETTLED');
374
+ expect(result.settlement.totalAmount).toBe(0.03);
375
+ expect(buyer.wallet.balance()).toBe(99.97);
376
+ expect(seller.wallet.balance()).toBe(0.03);
377
+ });
378
+
379
+ it('should execute escrow → release flow', () => {
380
+ const buyer = ump.registerAgent({
381
+ name: 'buyer', type: 'AI_AGENT',
382
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
383
+ });
384
+ const seller = ump.registerAgent({
385
+ name: 'seller', type: 'SERVICE',
386
+ authority: { maxPerTransaction: 100, maxPerDay: 1000 },
387
+ });
388
+
389
+ buyer.wallet.fund({ amount: 100 });
390
+
391
+ // Create escrow
392
+ const escrowId = ump.settlement.createEscrow(buyer.id, seller.id, 50, 'txn_1');
393
+
394
+ // Buyer's available should be reduced, but total still shows reserved
395
+ expect(buyer.wallet.balance()).toBe(50); // 100 - 50 reserved
396
+
397
+ // Release escrow (simulate outcome verified)
398
+ const result = ump.settlement.releaseEscrow(escrowId, 47); // partial: 47 of 50
399
+
400
+ expect(result.settlement.totalAmount).toBe(47);
401
+ expect(seller.wallet.balance()).toBe(47);
402
+ });
403
+
404
+ it('should execute waterfall splits', () => {
405
+ const buyer = ump.registerAgent({
406
+ name: 'buyer', type: 'AI_AGENT',
407
+ authority: { maxPerTransaction: 200, maxPerDay: 2000 },
408
+ });
409
+ const platform = ump.registerAgent({
410
+ name: 'platform', type: 'SERVICE',
411
+ authority: { maxPerTransaction: 200, maxPerDay: 2000 },
412
+ });
413
+ const vendor = ump.registerAgent({
414
+ name: 'vendor', type: 'SERVICE',
415
+ authority: { maxPerTransaction: 200, maxPerDay: 2000 },
416
+ });
417
+
418
+ buyer.wallet.fund({ amount: 100 });
419
+
420
+ const results = ump.settlement.settleWaterfall(
421
+ buyer.id,
422
+ [
423
+ { agentId: platform.id, amount: 15 }, // 15% commission
424
+ { agentId: vendor.id, amount: 85 }, // 85% to vendor
425
+ ],
426
+ [],
427
+ );
428
+
429
+ expect(results).toHaveLength(2);
430
+ expect(buyer.wallet.balance()).toBe(0);
431
+ expect(platform.wallet.balance()).toBe(15);
432
+ expect(vendor.wallet.balance()).toBe(85);
433
+ });
434
+ });
435
+
436
+ describe('Audit Trail', () => {
437
+ it('should record and query audit entries', () => {
438
+ const auditId = ump.audit.record({
439
+ what: { operation: 'TEST', entityType: 'test', entityId: 'e1', amount: 42 },
440
+ who: { sourceAgentId: 'a1', targetAgentId: 'a2' },
441
+ why: { contractId: 'c1', justification: 'Test audit' },
442
+ how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
443
+ result: { status: 'SUCCESS' },
444
+ });
445
+
446
+ expect(auditId).toMatch(/^aud_/);
447
+
448
+ const records = ump.audit.query({ agentId: 'a1' });
449
+ expect(records.length).toBeGreaterThan(0);
450
+ expect(records[0].what.operation).toBe('TEST');
451
+ });
452
+
453
+ it('should fire onAudit callback', () => {
454
+ const captured: any[] = [];
455
+ const ump2 = new UMP({
456
+ apiKey: 'test',
457
+ onAudit: (record) => captured.push(record),
458
+ });
459
+
460
+ ump2.audit.record({
461
+ what: { operation: 'CB_TEST', entityType: 'test', entityId: 'e1' },
462
+ who: { sourceAgentId: 'a1', targetAgentId: 'a2' },
463
+ why: {},
464
+ how: { policiesEvaluated: [], policiesPassed: [] },
465
+ result: { status: 'OK' },
466
+ });
467
+
468
+ expect(captured).toHaveLength(1);
469
+ expect(captured[0].what.operation).toBe('CB_TEST');
470
+ });
471
+ });
472
+
473
+ // ═══════════════════════════════════════════════════════════
474
+ // END-TO-END: The 10-Line Quick Start
475
+ // ═══════════════════════════════════════════════════════════
476
+
477
+ describe('End-to-End Transaction', () => {
478
+ it('should execute the Quick Start flow', async () => {
479
+ // 1. Initialize
480
+ const ump = new UMP({ apiKey: 'ump_sk_test' });
481
+
482
+ // 2. Register agents
483
+ const buyer = ump.registerAgent({
484
+ name: 'my-coding-agent',
485
+ type: 'AI_AGENT',
486
+ authority: { maxPerTransaction: '$50', maxPerDay: '$500' },
487
+ });
488
+
489
+ const seller = ump.registerAgent({
490
+ name: 'code-review-service',
491
+ type: 'SERVICE',
492
+ authority: { maxPerTransaction: '$100', maxPerDay: '$5000' },
493
+ });
494
+
495
+ // 3. Fund the wallet
496
+ buyer.wallet.fund({ amount: '$100' });
497
+
498
+ // 4. Create contract
499
+ ump.contracts.create(buyer.id, {
500
+ targetAgentId: seller.id,
501
+ pricingRules: [{
502
+ ruleId: 'default',
503
+ name: 'Per code review',
504
+ primitive: 'FIXED',
505
+ amount: 0.12,
506
+ period: 'PER_EVENT',
507
+ }],
508
+ });
509
+
510
+ // 5. Transact!
511
+ const result = await ump.transact({
512
+ from: buyer.id,
513
+ to: seller.id,
514
+ service: 'code_review',
515
+ payload: { repo: 'github.com/acme/app', pr: 42 },
516
+ });
517
+
518
+ expect(result.cost).toBe(0.12);
519
+ expect(result.auditId).toMatch(/^aud_/);
520
+ expect(result.duration).toBeLessThan(1000); // sub-second
521
+ expect(buyer.wallet.balance()).toBe(99.88);
522
+ expect(seller.wallet.balance()).toBe(0.12);
523
+ });
524
+ });
525
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noImplicitReturns": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "paths": {
23
+ "@/*": ["./src/*"]
24
+ }
25
+ },
26
+ "include": ["src/**/*.ts"],
27
+ "exclude": ["node_modules", "dist", "tests"]
28
+ }