@ic-reactor/candid 3.0.7-beta.1 → 3.0.8-beta.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 (59) hide show
  1. package/README.md +5 -1
  2. package/dist/display-reactor.d.ts +3 -2
  3. package/dist/display-reactor.d.ts.map +1 -1
  4. package/dist/display-reactor.js +6 -0
  5. package/dist/display-reactor.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/metadata-display-reactor.d.ts +73 -0
  11. package/dist/metadata-display-reactor.d.ts.map +1 -0
  12. package/dist/metadata-display-reactor.js +128 -0
  13. package/dist/metadata-display-reactor.js.map +1 -0
  14. package/dist/visitor/arguments/index.d.ts +69 -0
  15. package/dist/visitor/arguments/index.d.ts.map +1 -0
  16. package/dist/visitor/arguments/index.js +277 -0
  17. package/dist/visitor/arguments/index.js.map +1 -0
  18. package/dist/visitor/arguments/types.d.ts +92 -0
  19. package/dist/visitor/arguments/types.d.ts.map +1 -0
  20. package/dist/visitor/arguments/types.js +2 -0
  21. package/dist/visitor/arguments/types.js.map +1 -0
  22. package/dist/visitor/constants.d.ts +4 -0
  23. package/dist/visitor/constants.d.ts.map +1 -0
  24. package/dist/visitor/constants.js +61 -0
  25. package/dist/visitor/constants.js.map +1 -0
  26. package/dist/visitor/helpers.d.ts +30 -0
  27. package/dist/visitor/helpers.d.ts.map +1 -0
  28. package/dist/visitor/helpers.js +200 -0
  29. package/dist/visitor/helpers.js.map +1 -0
  30. package/dist/visitor/returns/index.d.ts +76 -0
  31. package/dist/visitor/returns/index.d.ts.map +1 -0
  32. package/dist/visitor/returns/index.js +425 -0
  33. package/dist/visitor/returns/index.js.map +1 -0
  34. package/dist/visitor/returns/types.d.ts +142 -0
  35. package/dist/visitor/returns/types.d.ts.map +1 -0
  36. package/dist/visitor/returns/types.js +2 -0
  37. package/dist/visitor/returns/types.js.map +1 -0
  38. package/dist/visitor/types.d.ts +6 -0
  39. package/dist/visitor/types.d.ts.map +1 -0
  40. package/dist/visitor/types.js +3 -0
  41. package/dist/visitor/types.js.map +1 -0
  42. package/package.json +4 -2
  43. package/src/adapter.ts +446 -0
  44. package/src/constants.ts +11 -0
  45. package/src/display-reactor.ts +332 -0
  46. package/src/index.ts +7 -0
  47. package/src/metadata-display-reactor.ts +184 -0
  48. package/src/reactor.ts +199 -0
  49. package/src/types.ts +107 -0
  50. package/src/utils.ts +28 -0
  51. package/src/visitor/arguments/index.test.ts +882 -0
  52. package/src/visitor/arguments/index.ts +405 -0
  53. package/src/visitor/arguments/types.ts +168 -0
  54. package/src/visitor/constants.ts +62 -0
  55. package/src/visitor/helpers.ts +221 -0
  56. package/src/visitor/returns/index.test.ts +2027 -0
  57. package/src/visitor/returns/index.ts +545 -0
  58. package/src/visitor/returns/types.ts +271 -0
  59. package/src/visitor/types.ts +29 -0
@@ -0,0 +1,2027 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { IDL } from "@icp-sdk/core/candid"
3
+ import { ResultFieldVisitor } from "./index"
4
+ import type {
5
+ ResultField,
6
+ RecordResultField,
7
+ VariantResultField,
8
+ TupleResultField,
9
+ OptionalResultField,
10
+ VectorResultField,
11
+ BlobResultField,
12
+ RecursiveResultField,
13
+ NumberResultField,
14
+ TextResultField,
15
+ PrincipalResultField,
16
+ BooleanResultField,
17
+ NullResultField,
18
+ MethodResultMeta,
19
+ ServiceResultMeta,
20
+ } from "./types"
21
+
22
+ describe("ResultFieldVisitor", () => {
23
+ const visitor = new ResultFieldVisitor()
24
+
25
+ // ════════════════════════════════════════════════════════════════════════
26
+ // Primitive Types
27
+ // ════════════════════════════════════════════════════════════════════════
28
+
29
+ describe("Primitive Types", () => {
30
+ it("should handle text type", () => {
31
+ const textType = IDL.Text
32
+ const field = visitor.visitText(textType, "message")
33
+
34
+ expect(field.type).toBe("text")
35
+ expect(field.label).toBe("message")
36
+ expect(field.candidType).toBe("text")
37
+ expect(field.displayType).toBe("string")
38
+ expect(field.textFormat).toBe("plain")
39
+ })
40
+
41
+ it("should handle text with special label detection", () => {
42
+ const emailField = visitor.visitText(IDL.Text, "email")
43
+ expect(emailField.textFormat).toBe("email")
44
+
45
+ const urlField = visitor.visitText(IDL.Text, "website_url")
46
+ expect(urlField.textFormat).toBe("url")
47
+
48
+ const phoneField = visitor.visitText(IDL.Text, "phone_number")
49
+ expect(phoneField.textFormat).toBe("phone")
50
+
51
+ const uuidField = visitor.visitText(IDL.Text, "transaction_uuid")
52
+ expect(uuidField.textFormat).toBe("uuid")
53
+
54
+ const btcField = visitor.visitText(IDL.Text, "btc_address")
55
+ expect(btcField.textFormat).toBe("btc")
56
+
57
+ const ethField = visitor.visitText(IDL.Text, "ethereum_address")
58
+ expect(ethField.textFormat).toBe("eth")
59
+
60
+ const accountIdField = visitor.visitText(IDL.Text, "account_id")
61
+ expect(accountIdField.textFormat).toBe("account-id")
62
+
63
+ const principalField = visitor.visitText(IDL.Text, "canister_id")
64
+ expect(principalField.textFormat).toBe("principal")
65
+ })
66
+
67
+ it("should handle bool type", () => {
68
+ const boolType = IDL.Bool
69
+ const field = visitor.visitBool(boolType, "isActive")
70
+
71
+ expect(field.type).toBe("boolean")
72
+ expect(field.label).toBe("isActive")
73
+ expect(field.candidType).toBe("bool")
74
+ expect(field.displayType).toBe("boolean")
75
+ })
76
+
77
+ it("should handle null type", () => {
78
+ const nullType = IDL.Null
79
+ const field = visitor.visitNull(nullType, "empty")
80
+
81
+ expect(field.type).toBe("null")
82
+ expect(field.candidType).toBe("null")
83
+ expect(field.displayType).toBe("null")
84
+ })
85
+
86
+ it("should handle principal type", () => {
87
+ const principalType = IDL.Principal
88
+ const field = visitor.visitPrincipal(IDL.Principal, "owner")
89
+
90
+ expect(field.type).toBe("principal")
91
+ expect(field.label).toBe("owner")
92
+ expect(field.candidType).toBe("principal")
93
+ expect(field.displayType).toBe("string") // Principal → string after transformation
94
+ })
95
+ })
96
+
97
+ // ════════════════════════════════════════════════════════════════════════
98
+ // Number Types - Display Type Mapping
99
+ // ════════════════════════════════════════════════════════════════════════
100
+
101
+ describe("Number Types", () => {
102
+ describe("BigInt types (display as string)", () => {
103
+ it("should map nat to string display type", () => {
104
+ const field = visitor.visitNat(IDL.Nat, "amount")
105
+
106
+ expect(field.type).toBe("number")
107
+ expect(field.candidType).toBe("nat")
108
+ expect(field.displayType).toBe("string") // BigInt → string
109
+ expect(field.numberFormat).toBe("normal")
110
+ })
111
+
112
+ it("should map int to string display type", () => {
113
+ const field = visitor.visitInt(IDL.Int, "balance")
114
+
115
+ expect(field.type).toBe("number")
116
+ expect(field.candidType).toBe("int")
117
+ expect(field.displayType).toBe("string")
118
+ })
119
+
120
+ it("should map nat64 to string display type", () => {
121
+ const field = visitor.visitFixedNat(IDL.Nat64, "timestamp")
122
+
123
+ expect(field.type).toBe("number")
124
+ expect(field.candidType).toBe("nat64")
125
+ expect(field.displayType).toBe("string") // 64-bit → string
126
+ })
127
+
128
+ it("should map int64 to string display type", () => {
129
+ const field = visitor.visitFixedInt(IDL.Int64, "offset")
130
+
131
+ expect(field.type).toBe("number")
132
+ expect(field.candidType).toBe("int64")
133
+ expect(field.displayType).toBe("string")
134
+ })
135
+ })
136
+
137
+ describe("Small int types (display as number)", () => {
138
+ it("should map nat8 to number display type", () => {
139
+ const field = visitor.visitFixedNat(IDL.Nat8, "byte")
140
+
141
+ expect(field.type).toBe("number")
142
+ expect(field.candidType).toBe("nat8")
143
+ expect(field.displayType).toBe("number") // Small int stays number
144
+ })
145
+
146
+ it("should map nat16 to number display type", () => {
147
+ const field = visitor.visitFixedNat(IDL.Nat16, "port")
148
+
149
+ expect(field.candidType).toBe("nat16")
150
+ expect(field.displayType).toBe("number")
151
+ })
152
+
153
+ it("should map nat32 to number display type", () => {
154
+ const field = visitor.visitFixedNat(IDL.Nat32, "count")
155
+
156
+ expect(field.candidType).toBe("nat32")
157
+ expect(field.displayType).toBe("number")
158
+ })
159
+
160
+ it("should map int8 to number display type", () => {
161
+ const field = visitor.visitFixedInt(IDL.Int8, "temp")
162
+
163
+ expect(field.candidType).toBe("int8")
164
+ expect(field.displayType).toBe("number")
165
+ })
166
+
167
+ it("should map int32 to number display type", () => {
168
+ const field = visitor.visitFixedInt(IDL.Int32, "index")
169
+
170
+ expect(field.candidType).toBe("int32")
171
+ expect(field.displayType).toBe("number")
172
+ })
173
+ })
174
+
175
+ describe("Float types (display as number)", () => {
176
+ it("should map float32 to number display type", () => {
177
+ const field = visitor.visitFloat(IDL.Float32, "rate")
178
+
179
+ expect(field.candidType).toBe("float32")
180
+ expect(field.displayType).toBe("number")
181
+ })
182
+
183
+ it("should map float64 to number display type", () => {
184
+ const field = visitor.visitFloat(IDL.Float64, "price")
185
+
186
+ expect(field.candidType).toBe("float64")
187
+ expect(field.displayType).toBe("number")
188
+ })
189
+ })
190
+
191
+ describe("Number format detection", () => {
192
+ it("should detect timestamp format from label", () => {
193
+ const timestampField = visitor.visitFixedNat(IDL.Nat64, "created_at")
194
+ expect(timestampField.numberFormat).toBe("timestamp")
195
+
196
+ const dateField = visitor.visitNat(IDL.Nat, "timestamp_nanos")
197
+ expect(dateField.numberFormat).toBe("timestamp")
198
+
199
+ const deadlineField = visitor.visitNat(IDL.Nat, "deadline")
200
+ expect(deadlineField.numberFormat).toBe("timestamp")
201
+ })
202
+
203
+ it("should detect cycle format from label", () => {
204
+ const cycleField = visitor.visitNat(IDL.Nat, "cycles")
205
+ expect(cycleField.numberFormat).toBe("cycle")
206
+
207
+ // "cycle" as standalone word
208
+ const cycleSingleField = visitor.visitNat(IDL.Nat, "cycle")
209
+ expect(cycleSingleField.numberFormat).toBe("cycle")
210
+ })
211
+
212
+ it("should default to normal format", () => {
213
+ const amountField = visitor.visitNat(IDL.Nat, "amount")
214
+ expect(amountField.numberFormat).toBe("normal")
215
+
216
+ const countField = visitor.visitNat(IDL.Nat, "count")
217
+ expect(countField.numberFormat).toBe("normal")
218
+ })
219
+ })
220
+ })
221
+
222
+ // ════════════════════════════════════════════════════════════════════════
223
+ // Compound Types
224
+ // ════════════════════════════════════════════════════════════════════════
225
+
226
+ describe("Record Types", () => {
227
+ it("should handle simple record", () => {
228
+ const recordType = IDL.Record({
229
+ name: IDL.Text,
230
+ age: IDL.Nat,
231
+ })
232
+ const field = visitor.visitRecord(
233
+ recordType,
234
+ [
235
+ ["name", IDL.Text],
236
+ ["age", IDL.Nat],
237
+ ],
238
+ "person"
239
+ )
240
+
241
+ expect(field.type).toBe("record")
242
+ expect(field.label).toBe("person")
243
+ expect(field.candidType).toBe("record")
244
+ expect(field.displayType).toBe("object")
245
+ expect(field.fields).toHaveLength(2)
246
+
247
+ const nameField = field.fields.find((f) => f.label === "name")
248
+ if (!nameField || nameField.type !== "text") {
249
+ throw new Error("Name field not found or not text")
250
+ }
251
+ expect(nameField.displayType).toBe("string")
252
+
253
+ const ageField = field.fields.find((f) => f.label === "age")
254
+ if (!ageField || ageField.type !== "number") {
255
+ throw new Error("Age field not found or not number")
256
+ }
257
+ expect(ageField.displayType).toBe("string") // nat → string
258
+ })
259
+
260
+ it("should handle nested record", () => {
261
+ const addressType = IDL.Record({
262
+ street: IDL.Text,
263
+ city: IDL.Text,
264
+ })
265
+ const personType = IDL.Record({
266
+ name: IDL.Text,
267
+ address: addressType,
268
+ })
269
+ const field = visitor.visitRecord(
270
+ personType,
271
+ [
272
+ ["name", IDL.Text],
273
+ ["address", addressType],
274
+ ],
275
+ "user"
276
+ )
277
+
278
+ expect(field.type).toBe("record")
279
+
280
+ const addressField = field.fields.find((f) => f.label === "address")
281
+ if (!addressField || addressField.type !== "record") {
282
+ throw new Error("Address field not found or not a record")
283
+ }
284
+
285
+ expect(addressField.type).toBe("record")
286
+ expect(addressField.displayType).toBe("object")
287
+ expect(addressField.fields).toHaveLength(2)
288
+ })
289
+
290
+ it("should handle ICRC-1 account record", () => {
291
+ const accountType = IDL.Record({
292
+ owner: IDL.Principal,
293
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
294
+ })
295
+ const field = visitor.visitRecord(
296
+ accountType,
297
+ [
298
+ ["owner", IDL.Principal],
299
+ ["subaccount", IDL.Opt(IDL.Vec(IDL.Nat8))],
300
+ ],
301
+ "account"
302
+ )
303
+
304
+ expect(field.type).toBe("record")
305
+
306
+ const ownerField = field.fields.find((f) => f.label === "owner")
307
+ if (!ownerField || ownerField.type !== "principal") {
308
+ throw new Error("Owner field not found or not principal")
309
+ }
310
+ expect(ownerField.type).toBe("principal")
311
+ expect(ownerField.displayType).toBe("string")
312
+
313
+ const subaccountField = field.fields.find((f) => f.label === "subaccount")
314
+ if (!subaccountField || subaccountField.type !== "optional") {
315
+ throw new Error("Subaccount field not found or not optional")
316
+ }
317
+ expect(subaccountField.type).toBe("optional")
318
+ if (subaccountField.innerField.type !== "blob") {
319
+ throw new Error("Subaccount inner field is not blob")
320
+ }
321
+ expect(subaccountField.innerField.type).toBe("blob")
322
+ })
323
+ })
324
+
325
+ describe("Variant Types", () => {
326
+ it("should handle simple variant", () => {
327
+ const statusType = IDL.Variant({
328
+ Active: IDL.Null,
329
+ Inactive: IDL.Null,
330
+ Pending: IDL.Null,
331
+ })
332
+ const field = visitor.visitVariant(
333
+ statusType,
334
+ [
335
+ ["Active", IDL.Null],
336
+ ["Inactive", IDL.Null],
337
+ ["Pending", IDL.Null],
338
+ ],
339
+ "status"
340
+ )
341
+
342
+ expect(field.type).toBe("variant")
343
+ expect(field.candidType).toBe("variant")
344
+ expect(field.displayType).toBe("variant")
345
+ expect(field.options).toContain("Active")
346
+ expect(field.options).toContain("Inactive")
347
+ expect(field.options).toContain("Pending")
348
+ expect(field.options).toHaveLength(3)
349
+ })
350
+
351
+ it("should handle variant with payloads", () => {
352
+ const eventType = IDL.Variant({
353
+ Transfer: IDL.Record({
354
+ from: IDL.Principal,
355
+ to: IDL.Principal,
356
+ amount: IDL.Nat,
357
+ }),
358
+ Approve: IDL.Nat,
359
+ Mint: IDL.Record({
360
+ to: IDL.Principal,
361
+ amount: IDL.Nat,
362
+ }),
363
+ })
364
+ const field = visitor.visitVariant(
365
+ eventType,
366
+ [
367
+ [
368
+ "Transfer",
369
+ IDL.Record({
370
+ from: IDL.Principal,
371
+ to: IDL.Principal,
372
+ amount: IDL.Nat,
373
+ }),
374
+ ],
375
+ ["Approve", IDL.Nat],
376
+ [
377
+ "Mint",
378
+ IDL.Record({
379
+ to: IDL.Principal,
380
+ amount: IDL.Nat,
381
+ }),
382
+ ],
383
+ ],
384
+ "event"
385
+ )
386
+
387
+ expect(field.type).toBe("variant")
388
+ expect(field.options).toContain("Transfer")
389
+ expect(field.options).toContain("Approve")
390
+ expect(field.options).toContain("Mint")
391
+ expect(field.options).toHaveLength(3)
392
+
393
+ const transferField = field.optionFields.find(
394
+ (f) => f.label === "Transfer"
395
+ )
396
+
397
+ if (!transferField || transferField.type !== "record") {
398
+ throw new Error("Transfer field not found or not record")
399
+ }
400
+ expect(transferField.type).toBe("record")
401
+ expect(transferField.fields).toHaveLength(3)
402
+
403
+ const approveField = field.optionFields.find((f) => f.label === "Approve")
404
+ if (!approveField || approveField.type !== "number") {
405
+ throw new Error("Approve field not found or not number")
406
+ }
407
+ expect(approveField.type).toBe("number")
408
+ })
409
+
410
+ it("should detect Result variant (Ok/Err)", () => {
411
+ const resultType = IDL.Variant({
412
+ Ok: IDL.Nat,
413
+ Err: IDL.Text,
414
+ })
415
+ const field = visitor.visitVariant(
416
+ resultType,
417
+ [
418
+ ["Ok", IDL.Nat],
419
+ ["Err", IDL.Text],
420
+ ],
421
+ "result"
422
+ )
423
+
424
+ expect(field.type).toBe("variant")
425
+ expect(field.displayType).toBe("result") // Special result type
426
+ expect(field.options).toContain("Ok")
427
+ expect(field.options).toContain("Err")
428
+
429
+ const okField = field.optionFields.find((f) => f.label === "Ok")
430
+ if (!okField || okField.type !== "number") {
431
+ throw new Error("Ok field not found or not number")
432
+ }
433
+ expect(okField.displayType).toBe("string")
434
+
435
+ const errField = field.optionFields.find((f) => f.label === "Err")
436
+ if (!errField || errField.type !== "text") {
437
+ throw new Error("Err field not found or not text")
438
+ }
439
+ expect(errField.displayType).toBe("string")
440
+ })
441
+
442
+ it("should detect complex Result variant", () => {
443
+ const resultType = IDL.Variant({
444
+ Ok: IDL.Record({
445
+ id: IDL.Nat,
446
+ data: IDL.Vec(IDL.Nat8),
447
+ }),
448
+ Err: IDL.Variant({
449
+ NotFound: IDL.Null,
450
+ Unauthorized: IDL.Null,
451
+ InvalidInput: IDL.Text,
452
+ }),
453
+ })
454
+ const field = visitor.visitVariant(
455
+ resultType,
456
+ [
457
+ [
458
+ "Ok",
459
+ IDL.Record({
460
+ id: IDL.Nat,
461
+ data: IDL.Vec(IDL.Nat8),
462
+ }),
463
+ ],
464
+ [
465
+ "Err",
466
+ IDL.Variant({
467
+ NotFound: IDL.Null,
468
+ Unauthorized: IDL.Null,
469
+ InvalidInput: IDL.Text,
470
+ }),
471
+ ],
472
+ ],
473
+ "result"
474
+ )
475
+
476
+ expect(field.displayType).toBe("result")
477
+
478
+ const okField = field.optionFields.find((f) => f.label === "Ok")
479
+ if (!okField || okField.type !== "record") {
480
+ throw new Error("Ok field not found or not record")
481
+ }
482
+ expect(okField.type).toBe("record")
483
+
484
+ const errField = field.optionFields.find((f) => f.label === "Err")
485
+ if (!errField || errField.type !== "variant") {
486
+ throw new Error("Err field not found or not variant")
487
+ }
488
+ expect(errField.type).toBe("variant")
489
+ })
490
+
491
+ it("should not detect non-Result variant with Ok and other options", () => {
492
+ const weirdType = IDL.Variant({
493
+ Ok: IDL.Nat,
494
+ Pending: IDL.Null,
495
+ Processing: IDL.Text,
496
+ })
497
+ const field = visitor.visitVariant(
498
+ weirdType,
499
+ [
500
+ ["Ok", IDL.Nat],
501
+ ["Pending", IDL.Null],
502
+ ["Processing", IDL.Text],
503
+ ],
504
+ "status"
505
+ )
506
+
507
+ expect(field.displayType).toBe("variant")
508
+ })
509
+ })
510
+
511
+ describe("Tuple Types", () => {
512
+ it("should handle simple tuple", () => {
513
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
514
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
515
+
516
+ expect(field.type).toBe("tuple")
517
+ expect(field.candidType).toBe("tuple")
518
+ expect(field.displayType).toBe("array")
519
+ expect(field.fields).toHaveLength(2)
520
+ expect(field.fields[0].type).toBe("text")
521
+ expect(field.fields[1].type).toBe("number")
522
+ })
523
+
524
+ it("should handle tuple with mixed types", () => {
525
+ const tupleType = IDL.Tuple(
526
+ IDL.Principal,
527
+ IDL.Nat64,
528
+ IDL.Bool,
529
+ IDL.Vec(IDL.Nat8)
530
+ )
531
+ const field = visitor.visitTuple(
532
+ tupleType,
533
+ [IDL.Principal, IDL.Nat64, IDL.Bool, IDL.Vec(IDL.Nat8)],
534
+ "data"
535
+ )
536
+
537
+ expect(field.fields).toHaveLength(4)
538
+ expect(field.fields[0].type).toBe("principal")
539
+ expect(field.fields[1].type).toBe("number")
540
+ const numField = field.fields[1]
541
+ if (numField.type === "number") {
542
+ expect(numField.displayType).toBe("string") // nat64 → string
543
+ } else {
544
+ throw new Error("Expected number field")
545
+ }
546
+ expect(field.fields[2].type).toBe("boolean")
547
+ expect(field.fields[3].type).toBe("blob")
548
+ })
549
+ })
550
+
551
+ describe("Optional Types", () => {
552
+ it("should handle optional primitive", () => {
553
+ const optType = IDL.Opt(IDL.Text)
554
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
555
+
556
+ expect(field.type).toBe("optional")
557
+ expect(field.candidType).toBe("opt")
558
+ expect(field.displayType).toBe("nullable") // opt T → T | null
559
+ expect(field.innerField.type).toBe("text")
560
+ })
561
+
562
+ it("should handle optional record", () => {
563
+ const recordInOpt = IDL.Record({
564
+ name: IDL.Text,
565
+ value: IDL.Nat,
566
+ })
567
+ const optType = IDL.Opt(recordInOpt)
568
+ const field = visitor.visitOpt(optType, recordInOpt, "metadata")
569
+
570
+ expect(field.type).toBe("optional")
571
+ expect(field.innerField.type).toBe("record")
572
+ })
573
+
574
+ it("should handle nested optional", () => {
575
+ const innerOpt = IDL.Opt(IDL.Nat)
576
+ const optType = IDL.Opt(innerOpt)
577
+ const field = visitor.visitOpt(optType, innerOpt, "maybeNumber")
578
+
579
+ expect(field.type).toBe("optional")
580
+ expect(field.innerField.type).toBe("optional")
581
+ const inner = field.innerField
582
+ if (inner.type === "optional") {
583
+ expect(inner.innerField.type).toBe("number")
584
+ } else {
585
+ throw new Error("Inner field is not optional")
586
+ }
587
+ })
588
+ })
589
+
590
+ describe("Vector Types", () => {
591
+ it("should handle vector of primitives", () => {
592
+ const vecType = IDL.Vec(IDL.Text)
593
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
594
+
595
+ expect(field.type).toBe("vector")
596
+ expect(field.candidType).toBe("vec")
597
+ expect(field.displayType).toBe("array")
598
+ if (field.type === "vector") {
599
+ expect(field.itemField.type).toBe("text")
600
+ } else {
601
+ throw new Error("Field is not a vector")
602
+ }
603
+ })
604
+
605
+ it("should handle vector of records", () => {
606
+ const recType = IDL.Record({
607
+ id: IDL.Nat,
608
+ name: IDL.Text,
609
+ })
610
+ const vecType = IDL.Vec(recType)
611
+ const field = visitor.visitVec(vecType, recType, "items")
612
+
613
+ expect(field.type).toBe("vector")
614
+ if (field.type === "vector") {
615
+ expect(field.itemField.type).toBe("record")
616
+ } else {
617
+ throw new Error("Field is not a vector")
618
+ }
619
+ })
620
+
621
+ it("should handle blob (vec nat8)", () => {
622
+ const blobType = IDL.Vec(IDL.Nat8)
623
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
624
+
625
+ expect(field.type).toBe("blob")
626
+ expect(field.candidType).toBe("blob")
627
+ expect(field.displayType).toBe("string") // Blob → hex string
628
+ if (field.type === "blob") {
629
+ expect(field.displayHint).toBe("hex")
630
+ } else {
631
+ throw new Error("Field is not a blob")
632
+ }
633
+ })
634
+
635
+ it("should handle nested vectors", () => {
636
+ const innerVec = IDL.Vec(IDL.Nat)
637
+ const nestedVecType = IDL.Vec(innerVec)
638
+ const field = visitor.visitVec(nestedVecType, innerVec, "matrix")
639
+
640
+ expect(field.type).toBe("vector")
641
+ if (field.type === "vector") {
642
+ expect(field.itemField.type).toBe("vector")
643
+ } else {
644
+ throw new Error("Field is not a vector")
645
+ }
646
+ })
647
+
648
+ it("should handle vec of tuples (Map-like)", () => {
649
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
650
+ const mapType = IDL.Vec(tupleType)
651
+ const field = visitor.visitVec(
652
+ mapType,
653
+ tupleType,
654
+ "metadata"
655
+ ) as VectorResultField
656
+
657
+ expect(field.type).toBe("vector")
658
+ expect(field.itemField.type).toBe("tuple")
659
+ const itemField = field.itemField
660
+ if (itemField.type === "tuple") {
661
+ expect(itemField.fields).toHaveLength(2)
662
+ } else {
663
+ throw new Error("Item field is not tuple")
664
+ }
665
+ })
666
+ })
667
+
668
+ describe("Recursive Types", () => {
669
+ it("should handle recursive tree type", () => {
670
+ const Tree = IDL.Rec()
671
+ Tree.fill(
672
+ IDL.Variant({
673
+ Leaf: IDL.Nat,
674
+ Node: IDL.Record({
675
+ left: Tree,
676
+ right: Tree,
677
+ }),
678
+ })
679
+ )
680
+
681
+ const field = visitor.visitRec(
682
+ Tree,
683
+ IDL.Variant({
684
+ Leaf: IDL.Nat,
685
+ Node: IDL.Record({
686
+ left: Tree,
687
+ right: Tree,
688
+ }),
689
+ }),
690
+ "tree"
691
+ )
692
+
693
+ expect(field.type).toBe("recursive")
694
+ expect(field.candidType).toBe("rec")
695
+ expect(field.displayType).toBe("recursive")
696
+ expect(field.typeName).toBeDefined()
697
+ expect(typeof field.extract).toBe("function")
698
+
699
+ // Extract should return the variant
700
+ const extracted = field.extract()
701
+ if (extracted.type !== "variant") {
702
+ throw new Error("Extracted field is not variant")
703
+ }
704
+ expect(extracted.type).toBe("variant")
705
+ expect(extracted.options).toContain("Leaf")
706
+ expect(extracted.options).toContain("Node")
707
+ })
708
+
709
+ it("should handle recursive linked list", () => {
710
+ const List = IDL.Rec()
711
+ List.fill(
712
+ IDL.Variant({
713
+ Nil: IDL.Null,
714
+ Cons: IDL.Record({
715
+ head: IDL.Nat,
716
+ tail: List,
717
+ }),
718
+ })
719
+ )
720
+
721
+ const field = visitor.visitRec(
722
+ List,
723
+ IDL.Variant({
724
+ Nil: IDL.Null,
725
+ Cons: IDL.Record({
726
+ head: IDL.Nat,
727
+ tail: List,
728
+ }),
729
+ }),
730
+ "list"
731
+ )
732
+
733
+ expect(field.type).toBe("recursive")
734
+
735
+ const extracted = field.extract()
736
+ if (extracted.type !== "variant") {
737
+ throw new Error("Extracted field is not variant")
738
+ }
739
+ expect(extracted.options).toEqual(["Nil", "Cons"])
740
+ })
741
+ })
742
+
743
+ // ════════════════════════════════════════════════════════════════════════
744
+ // Function Types
745
+ // ════════════════════════════════════════════════════════════════════════
746
+
747
+ describe("Function Types", () => {
748
+ it("should handle query function with single return", () => {
749
+ const funcType = IDL.Func([IDL.Principal], [IDL.Nat], ["query"])
750
+ const meta = visitor.visitFunc(funcType, "get_balance")
751
+
752
+ expect(meta.functionType).toBe("query")
753
+ expect(meta.functionName).toBe("get_balance")
754
+ expect(meta.returnCount).toBe(1)
755
+ expect(meta.resultFields).toHaveLength(1)
756
+
757
+ expect(meta.resultFields).toHaveLength(1)
758
+
759
+ const returnField = meta.resultFields[0]
760
+ if (returnField.type === "number") {
761
+ expect(returnField.candidType).toBe("nat")
762
+ expect(returnField.displayType).toBe("string")
763
+ } else {
764
+ throw new Error("Return field is not number")
765
+ }
766
+ })
767
+
768
+ it("should handle update function with Result return", () => {
769
+ const funcType = IDL.Func(
770
+ [
771
+ IDL.Record({
772
+ to: IDL.Principal,
773
+ amount: IDL.Nat,
774
+ }),
775
+ ],
776
+ [
777
+ IDL.Variant({
778
+ Ok: IDL.Nat,
779
+ Err: IDL.Text,
780
+ }),
781
+ ],
782
+ []
783
+ )
784
+ const meta = visitor.visitFunc(funcType, "transfer")
785
+
786
+ expect(meta.functionType).toBe("update")
787
+ expect(meta.returnCount).toBe(1)
788
+
789
+ const resultField = meta.resultFields[0]
790
+ if (resultField.type === "variant") {
791
+ expect(resultField.displayType).toBe("result")
792
+ } else {
793
+ throw new Error("Result field is not variant")
794
+ }
795
+ })
796
+
797
+ it("should handle function with multiple returns", () => {
798
+ const funcType = IDL.Func([], [IDL.Text, IDL.Nat, IDL.Bool], ["query"])
799
+ const meta = visitor.visitFunc(funcType, "get_info")
800
+
801
+ expect(meta.returnCount).toBe(3)
802
+ expect(meta.resultFields).toHaveLength(3)
803
+ expect(meta.resultFields[0].type).toBe("text")
804
+ expect(meta.resultFields[1].type).toBe("number")
805
+ expect(meta.resultFields[2].type).toBe("boolean")
806
+ })
807
+
808
+ it("should handle function with no returns", () => {
809
+ const funcType = IDL.Func([IDL.Text], [], [])
810
+ const meta = visitor.visitFunc(funcType, "log")
811
+
812
+ expect(meta.returnCount).toBe(0)
813
+ expect(meta.resultFields).toHaveLength(0)
814
+ })
815
+ })
816
+
817
+ // ════════════════════════════════════════════════════════════════════════
818
+ // Service Types
819
+ // ════════════════════════════════════════════════════════════════════════
820
+
821
+ describe("Service Types", () => {
822
+ it("should handle complete service", () => {
823
+ const serviceType = IDL.Service({
824
+ get_balance: IDL.Func([IDL.Principal], [IDL.Nat], ["query"]),
825
+ transfer: IDL.Func(
826
+ [
827
+ IDL.Record({
828
+ to: IDL.Principal,
829
+ amount: IDL.Nat,
830
+ }),
831
+ ],
832
+ [
833
+ IDL.Variant({
834
+ Ok: IDL.Nat,
835
+ Err: IDL.Text,
836
+ }),
837
+ ],
838
+ []
839
+ ),
840
+ get_metadata: IDL.Func(
841
+ [],
842
+ [IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))],
843
+ ["query"]
844
+ ),
845
+ })
846
+
847
+ const serviceMeta = visitor.visitService(serviceType)
848
+
849
+ expect(Object.keys(serviceMeta)).toHaveLength(3)
850
+
851
+ // Check get_balance
852
+ const getBalanceMeta = serviceMeta["get_balance"]
853
+ expect(getBalanceMeta.functionType).toBe("query")
854
+ expect(getBalanceMeta.returnCount).toBe(1)
855
+ expect(getBalanceMeta.resultFields[0].type).toBe("number")
856
+ const balanceField = getBalanceMeta.resultFields[0]
857
+ if (balanceField.type === "number") {
858
+ expect(balanceField.displayType).toBe("string")
859
+ } else {
860
+ throw new Error("Balance field is not number")
861
+ }
862
+
863
+ // Check transfer
864
+ const transferMeta = serviceMeta["transfer"]
865
+ expect(transferMeta.functionType).toBe("update")
866
+ expect(transferMeta.returnCount).toBe(1)
867
+ // Check get_metadata
868
+ const getMetadataMeta = serviceMeta["get_metadata"]
869
+ expect(getMetadataMeta.returnCount).toBe(1)
870
+ expect(getMetadataMeta.resultFields[0].type).toBe("vector")
871
+ })
872
+ })
873
+
874
+ // ════════════════════════════════════════════════════════════════════════
875
+ // Real-World Examples
876
+ // ════════════════════════════════════════════════════════════════════════
877
+
878
+ describe("Real-World Examples", () => {
879
+ it("should handle ICRC-1 balance_of return", () => {
880
+ const funcType = IDL.Func(
881
+ [
882
+ IDL.Record({
883
+ owner: IDL.Principal,
884
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
885
+ }),
886
+ ],
887
+ [IDL.Nat],
888
+ ["query"]
889
+ )
890
+ const meta = visitor.visitFunc(funcType, "icrc1_balance_of")
891
+
892
+ expect(meta.functionType).toBe("query")
893
+ expect(meta.returnCount).toBe(1)
894
+
895
+ const balanceField = meta.resultFields[0]
896
+ if (balanceField.type === "number") {
897
+ expect(balanceField.candidType).toBe("nat")
898
+ expect(balanceField.displayType).toBe("string")
899
+ } else {
900
+ throw new Error("Balance field is not number")
901
+ }
902
+ })
903
+
904
+ it("should handle ICRC-1 transfer return", () => {
905
+ const TransferResult = IDL.Variant({
906
+ Ok: IDL.Nat, // Block index
907
+ Err: IDL.Variant({
908
+ BadFee: IDL.Record({ expected_fee: IDL.Nat }),
909
+ BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
910
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
911
+ TooOld: IDL.Null,
912
+ CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
913
+ Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
914
+ TemporarilyUnavailable: IDL.Null,
915
+ GenericError: IDL.Record({ error_code: IDL.Nat, message: IDL.Text }),
916
+ }),
917
+ })
918
+
919
+ const field = visitor.visitVariant(
920
+ TransferResult,
921
+ [
922
+ ["Ok", IDL.Nat], // Block index
923
+ [
924
+ "Err",
925
+ IDL.Variant({
926
+ BadFee: IDL.Record({ expected_fee: IDL.Nat }),
927
+ BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }),
928
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
929
+ TooOld: IDL.Null,
930
+ CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }),
931
+ Duplicate: IDL.Record({ duplicate_of: IDL.Nat }),
932
+ TemporarilyUnavailable: IDL.Null,
933
+ GenericError: IDL.Record({
934
+ error_code: IDL.Nat,
935
+ message: IDL.Text,
936
+ }),
937
+ }),
938
+ ],
939
+ ],
940
+ "result"
941
+ )
942
+
943
+ expect(field.displayType).toBe("result")
944
+
945
+ const okField = field.optionFields.find((f) => f.label === "Ok")
946
+
947
+ if (!okField || okField.type !== "number") {
948
+ throw new Error("Ok field is not number")
949
+ }
950
+
951
+ expect(okField.candidType).toBe("nat")
952
+ expect(okField.displayType).toBe("string")
953
+
954
+ const errField = field.optionFields.find((f) => f.label === "Err")
955
+ if (!errField || errField.type !== "variant") {
956
+ throw new Error("Err field is not variant")
957
+ }
958
+
959
+ expect(errField.type).toBe("variant")
960
+ expect(errField.options).toContain("InsufficientFunds")
961
+ expect(errField.options).toContain("GenericError")
962
+ })
963
+
964
+ it("should handle SNS canister status return", () => {
965
+ const CanisterStatusResponse = IDL.Record({
966
+ status: IDL.Variant({
967
+ running: IDL.Null,
968
+ stopping: IDL.Null,
969
+ stopped: IDL.Null,
970
+ }),
971
+ settings: IDL.Record({
972
+ controllers: IDL.Vec(IDL.Principal),
973
+ compute_allocation: IDL.Nat,
974
+ memory_allocation: IDL.Nat,
975
+ freezing_threshold: IDL.Nat,
976
+ }),
977
+ module_hash: IDL.Opt(IDL.Vec(IDL.Nat8)),
978
+ memory_size: IDL.Nat,
979
+ cycles: IDL.Nat,
980
+ idle_cycles_burned_per_day: IDL.Nat,
981
+ })
982
+
983
+ const field = visitor.visitRecord(
984
+ CanisterStatusResponse,
985
+ [
986
+ [
987
+ "status",
988
+ IDL.Variant({
989
+ running: IDL.Null,
990
+ stopping: IDL.Null,
991
+ stopped: IDL.Null,
992
+ }),
993
+ ],
994
+ [
995
+ "settings",
996
+ IDL.Record({
997
+ controllers: IDL.Vec(IDL.Principal),
998
+ compute_allocation: IDL.Nat,
999
+ memory_allocation: IDL.Nat,
1000
+ freezing_threshold: IDL.Nat,
1001
+ }),
1002
+ ],
1003
+ ["module_hash", IDL.Opt(IDL.Vec(IDL.Nat8))],
1004
+ ["memory_size", IDL.Nat],
1005
+ ["cycles", IDL.Nat],
1006
+ ["idle_cycles_burned_per_day", IDL.Nat],
1007
+ ],
1008
+ "status"
1009
+ )
1010
+
1011
+ expect(field.type).toBe("record")
1012
+
1013
+ // Check status variant
1014
+ const statusField = field.fields.find((f) => f.label === "status")
1015
+ if (!statusField || statusField.type !== "variant") {
1016
+ throw new Error("Status field is not variant")
1017
+ }
1018
+ expect(statusField.type).toBe("variant")
1019
+ expect(statusField.options).toContain("running")
1020
+ expect(statusField.options).toContain("stopping")
1021
+ expect(statusField.options).toContain("stopped")
1022
+ expect(statusField.options).toHaveLength(3)
1023
+
1024
+ // Check settings record
1025
+ const settingsField = field.fields.find((f) => f.label === "settings")
1026
+ if (!settingsField || settingsField.type !== "record") {
1027
+ throw new Error("Settings field is not record")
1028
+ }
1029
+ expect(settingsField.type).toBe("record")
1030
+
1031
+ const controllersField = settingsField.fields.find(
1032
+ (f) => f.label === "controllers"
1033
+ )
1034
+ if (!controllersField || controllersField.type !== "vector") {
1035
+ throw new Error("Controllers field is not vector")
1036
+ }
1037
+ expect(controllersField.type).toBe("vector")
1038
+ expect(controllersField.itemField.type).toBe("principal")
1039
+
1040
+ // Check cycles - should detect special format
1041
+ const cyclesField = field.fields.find((f) => f.label === "cycles")
1042
+ if (!cyclesField || cyclesField.type !== "number") {
1043
+ throw new Error("Cycles field is not number")
1044
+ }
1045
+ expect(cyclesField.numberFormat).toBe("cycle")
1046
+
1047
+ // Check module_hash - optional blob
1048
+ const moduleHashField = field.fields.find(
1049
+ (f) => f.label === "module_hash"
1050
+ ) as OptionalResultField
1051
+ expect(moduleHashField.type).toBe("optional")
1052
+ expect(moduleHashField.innerField.type).toBe("blob")
1053
+ })
1054
+
1055
+ it("should handle complex governance proposal types", () => {
1056
+ const ProposalInfo = IDL.Record({
1057
+ id: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
1058
+ status: IDL.Nat32,
1059
+ topic: IDL.Nat32,
1060
+ failure_reason: IDL.Opt(
1061
+ IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
1062
+ ),
1063
+ ballots: IDL.Vec(
1064
+ IDL.Tuple(
1065
+ IDL.Nat64,
1066
+ IDL.Record({
1067
+ vote: IDL.Nat32,
1068
+ voting_power: IDL.Nat64,
1069
+ })
1070
+ )
1071
+ ),
1072
+ proposal_timestamp_seconds: IDL.Nat64,
1073
+ reward_event_round: IDL.Nat64,
1074
+ deadline_timestamp_seconds: IDL.Opt(IDL.Nat64),
1075
+ executed_timestamp_seconds: IDL.Nat64,
1076
+ reject_cost_e8s: IDL.Nat64,
1077
+ proposer: IDL.Opt(IDL.Record({ id: IDL.Nat64 })),
1078
+ reward_status: IDL.Nat32,
1079
+ })
1080
+
1081
+ const field = visitor.visitRecord(
1082
+ ProposalInfo,
1083
+ [
1084
+ ["id", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
1085
+ ["status", IDL.Nat32],
1086
+ ["topic", IDL.Nat32],
1087
+ [
1088
+ "failure_reason",
1089
+ IDL.Opt(
1090
+ IDL.Record({ error_type: IDL.Nat32, error_message: IDL.Text })
1091
+ ),
1092
+ ],
1093
+ [
1094
+ "ballots",
1095
+ IDL.Vec(
1096
+ IDL.Tuple(
1097
+ IDL.Nat64,
1098
+ IDL.Record({
1099
+ vote: IDL.Nat32,
1100
+ voting_power: IDL.Nat64,
1101
+ })
1102
+ )
1103
+ ),
1104
+ ],
1105
+ ["proposal_timestamp_seconds", IDL.Nat64],
1106
+ ["reward_event_round", IDL.Nat64],
1107
+ ["deadline_timestamp_seconds", IDL.Opt(IDL.Nat64)],
1108
+ ["executed_timestamp_seconds", IDL.Nat64],
1109
+ ["reject_cost_e8s: IDL.Nat64", IDL.Nat64], // Fixed label in original
1110
+ ["proposer", IDL.Opt(IDL.Record({ id: IDL.Nat64 }))],
1111
+ ["reward_status", IDL.Nat32],
1112
+ ],
1113
+ "proposal"
1114
+ )
1115
+
1116
+ expect(field.type).toBe("record")
1117
+ expect(field.fields.length).toBeGreaterThan(8)
1118
+
1119
+ // Check timestamp field (note: the label pattern matching may not match "proposal_timestamp_seconds")
1120
+ const timestampField = field.fields.find(
1121
+ (f) => f.label === "proposal_timestamp_seconds"
1122
+ )
1123
+ if (!timestampField || timestampField.type !== "number") {
1124
+ throw new Error("Timestamp field not found or not number")
1125
+ }
1126
+ expect(timestampField.type).toBe("number")
1127
+ expect(timestampField.displayType).toBe("string") // nat64 → string
1128
+
1129
+ // Check ballots - vec of tuples
1130
+ const ballotsField = field.fields.find((f) => f.label === "ballots")
1131
+ if (!ballotsField || ballotsField.type !== "vector") {
1132
+ throw new Error("Ballots field not found or not vector")
1133
+ }
1134
+ expect(ballotsField.type).toBe("vector")
1135
+ expect(ballotsField.itemField.type).toBe("tuple")
1136
+
1137
+ const ballotTuple = ballotsField.itemField
1138
+ if (ballotTuple.type !== "tuple") {
1139
+ throw new Error("Ballot item is not tuple")
1140
+ }
1141
+ expect(ballotTuple.fields).toHaveLength(2)
1142
+ expect(ballotTuple.fields[0].type).toBe("number") // nat64
1143
+ expect(ballotTuple.fields[1].type).toBe("record") // ballot record
1144
+ })
1145
+ })
1146
+
1147
+ // ════════════════════════════════════════════════════════════════════════
1148
+ // Display Type Verification
1149
+ // ════════════════════════════════════════════════════════════════════════
1150
+
1151
+ describe("Display Type Verification", () => {
1152
+ it("should correctly map all types to their display types", () => {
1153
+ const testCases: Array<{ type: IDL.Type; expectedDisplay: string }> = [
1154
+ { type: IDL.Text, expectedDisplay: "string" },
1155
+ { type: IDL.Principal, expectedDisplay: "string" },
1156
+ { type: IDL.Nat, expectedDisplay: "string" },
1157
+ { type: IDL.Int, expectedDisplay: "string" },
1158
+ { type: IDL.Nat64, expectedDisplay: "string" },
1159
+ { type: IDL.Int64, expectedDisplay: "string" },
1160
+ { type: IDL.Nat8, expectedDisplay: "number" },
1161
+ { type: IDL.Nat16, expectedDisplay: "number" },
1162
+ { type: IDL.Nat32, expectedDisplay: "number" },
1163
+ { type: IDL.Int8, expectedDisplay: "number" },
1164
+ { type: IDL.Int16, expectedDisplay: "number" },
1165
+ { type: IDL.Int32, expectedDisplay: "number" },
1166
+ { type: IDL.Float32, expectedDisplay: "number" },
1167
+ { type: IDL.Float64, expectedDisplay: "number" },
1168
+ { type: IDL.Bool, expectedDisplay: "boolean" },
1169
+ { type: IDL.Null, expectedDisplay: "null" },
1170
+ { type: IDL.Vec(IDL.Nat8), expectedDisplay: "string" }, // blob → hex string
1171
+ { type: IDL.Vec(IDL.Text), expectedDisplay: "array" },
1172
+ { type: IDL.Opt(IDL.Text), expectedDisplay: "nullable" },
1173
+ { type: IDL.Record({ a: IDL.Text }), expectedDisplay: "object" },
1174
+ { type: IDL.Tuple(IDL.Text, IDL.Nat), expectedDisplay: "array" },
1175
+ ]
1176
+
1177
+ testCases.forEach(({ type, expectedDisplay }) => {
1178
+ const field = type.accept(visitor, "test")
1179
+ if (typeof field === "object" && "displayType" in field) {
1180
+ expect(field.displayType).toBe(expectedDisplay)
1181
+ } else {
1182
+ throw new Error("Expected a ResultField")
1183
+ }
1184
+ })
1185
+ })
1186
+ })
1187
+
1188
+ // ════════════════════════════════════════════════════════════════════════
1189
+ // resolve() Method Tests
1190
+ // ════════════════════════════════════════════════════════════════════════
1191
+
1192
+ describe("resolve() Method", () => {
1193
+ describe("Primitive Types", () => {
1194
+ it("should resolve text field with value", () => {
1195
+ const field = visitor.visitText(IDL.Text, "message")
1196
+ const resolved = field.resolve("Hello World")
1197
+
1198
+ expect(resolved.field).toBe(field)
1199
+ expect(resolved.value).toBe("Hello World")
1200
+ })
1201
+
1202
+ it("should resolve number field with value", () => {
1203
+ const field = visitor.visitNat(IDL.Nat, "amount")
1204
+ const resolved = field.resolve(BigInt(1000000))
1205
+
1206
+ expect(resolved.field).toBe(field)
1207
+ expect(resolved.value).toBe("1000000")
1208
+ })
1209
+
1210
+ it("should resolve boolean field with value", () => {
1211
+ const field = visitor.visitBool(IDL.Bool, "active")
1212
+ const resolved = field.resolve(true)
1213
+
1214
+ expect(resolved.field).toBe(field)
1215
+ expect(resolved.value).toBe(true)
1216
+ })
1217
+
1218
+ it("should resolve null field", () => {
1219
+ const field = visitor.visitNull(IDL.Null, "empty")
1220
+ const resolved = field.resolve(null)
1221
+
1222
+ expect(resolved.field).toBe(field)
1223
+ expect(resolved.value).toBe(null)
1224
+ })
1225
+
1226
+ it("should resolve principal field with string value", () => {
1227
+ const field = visitor.visitPrincipal(IDL.Principal, "owner")
1228
+ const resolved = field.resolve("aaaaa-aa")
1229
+
1230
+ expect(resolved.field).toBe(field)
1231
+ expect(resolved.value).toBe("aaaaa-aa")
1232
+ })
1233
+ })
1234
+
1235
+ describe("Record Type", () => {
1236
+ it("should resolve record with nested field values", () => {
1237
+ const recordType = IDL.Record({
1238
+ name: IDL.Text,
1239
+ age: IDL.Nat32,
1240
+ active: IDL.Bool,
1241
+ })
1242
+ const field = visitor.visitRecord(
1243
+ recordType,
1244
+ [
1245
+ ["name", IDL.Text],
1246
+ ["age", IDL.Nat32],
1247
+ ["active", IDL.Bool],
1248
+ ],
1249
+ "user"
1250
+ )
1251
+
1252
+ const resolved = field.resolve({
1253
+ name: "Alice",
1254
+ age: 30,
1255
+ active: true,
1256
+ })
1257
+
1258
+ expect(resolved.field).toBe(field)
1259
+ const value = resolved.value as Record<
1260
+ string,
1261
+ { field: ResultField; value: unknown }
1262
+ >
1263
+ expect(value.name.value).toBe("Alice")
1264
+ expect(value.age.value).toBe(30)
1265
+ expect(value.active.value).toBe(true)
1266
+ })
1267
+
1268
+ it("should handle null record value", () => {
1269
+ const recordType = IDL.Record({ name: IDL.Text })
1270
+ const field = visitor.visitRecord(
1271
+ recordType,
1272
+ [["name", IDL.Text]],
1273
+ "user"
1274
+ )
1275
+
1276
+ const resolved = field.resolve(null)
1277
+ expect(resolved.value).toBe(null)
1278
+ })
1279
+ })
1280
+
1281
+ describe("Variant Type", () => {
1282
+ it("should resolve variant with active option", () => {
1283
+ const variantType = IDL.Variant({
1284
+ Ok: IDL.Text,
1285
+ Err: IDL.Text,
1286
+ })
1287
+ const field = visitor.visitVariant(
1288
+ variantType,
1289
+ [
1290
+ ["Ok", IDL.Text],
1291
+ ["Err", IDL.Text],
1292
+ ],
1293
+ "result"
1294
+ )
1295
+
1296
+ const resolved = field.resolve({ Ok: "Success" })
1297
+
1298
+ const value = resolved.value as {
1299
+ option: string
1300
+ value: { field: ResultField; value: unknown }
1301
+ }
1302
+ expect(value.option).toBe("Ok")
1303
+ expect(value.value.value).toBe("Success")
1304
+ })
1305
+
1306
+ it("should resolve variant with Err option", () => {
1307
+ const variantType = IDL.Variant({
1308
+ Ok: IDL.Nat,
1309
+ Err: IDL.Text,
1310
+ })
1311
+ const field = visitor.visitVariant(
1312
+ variantType,
1313
+ [
1314
+ ["Ok", IDL.Nat],
1315
+ ["Err", IDL.Text],
1316
+ ],
1317
+ "result"
1318
+ )
1319
+
1320
+ const resolved = field.resolve({ Err: "Something went wrong" })
1321
+
1322
+ const value = resolved.value as {
1323
+ option: string
1324
+ value: { field: ResultField; value: unknown }
1325
+ }
1326
+ expect(value.option).toBe("Err")
1327
+ expect(value.value.value).toBe("Something went wrong")
1328
+ })
1329
+
1330
+ it("should handle null variant value", () => {
1331
+ const variantType = IDL.Variant({ A: IDL.Null, B: IDL.Text })
1332
+ const field = visitor.visitVariant(
1333
+ variantType,
1334
+ [
1335
+ ["A", IDL.Null],
1336
+ ["B", IDL.Text],
1337
+ ],
1338
+ "choice"
1339
+ )
1340
+
1341
+ const resolved = field.resolve(null)
1342
+ expect(resolved.value).toBe(null)
1343
+ })
1344
+ })
1345
+
1346
+ describe("Tuple Type", () => {
1347
+ it("should resolve tuple with indexed values", () => {
1348
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat, IDL.Bool)
1349
+ const field = visitor.visitTuple(
1350
+ tupleType,
1351
+ [IDL.Text, IDL.Nat, IDL.Bool],
1352
+ "data"
1353
+ )
1354
+
1355
+ const resolved = field.resolve(["hello", 123n, true])
1356
+
1357
+ expect(resolved.field).toBe(field)
1358
+ const value = resolved.value as Array<{
1359
+ field: ResultField
1360
+ value: unknown
1361
+ }>
1362
+ expect(value).toHaveLength(3)
1363
+ expect(value[0].value).toBe("hello")
1364
+ expect(value[1].value).toBe("123")
1365
+ expect(value[2].value).toBe(true)
1366
+ })
1367
+
1368
+ it("should handle null tuple value", () => {
1369
+ const tupleType = IDL.Tuple(IDL.Text, IDL.Nat)
1370
+ const field = visitor.visitTuple(tupleType, [IDL.Text, IDL.Nat], "pair")
1371
+
1372
+ const resolved = field.resolve(null)
1373
+ expect(resolved.value).toBe(null)
1374
+ })
1375
+ })
1376
+
1377
+ describe("Optional Type", () => {
1378
+ it("should resolve optional with value", () => {
1379
+ const optType = IDL.Opt(IDL.Text)
1380
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
1381
+
1382
+ const resolved = field.resolve(["Bob"])
1383
+
1384
+ expect(resolved.field).toBe(field)
1385
+ const inner = resolved.value as { field: ResultField; value: unknown }
1386
+ expect(inner.value).toBe("Bob")
1387
+ })
1388
+
1389
+ it("should resolve optional with null", () => {
1390
+ const optType = IDL.Opt(IDL.Text)
1391
+ const field = visitor.visitOpt(optType, IDL.Text, "nickname")
1392
+
1393
+ const resolved = field.resolve(null)
1394
+
1395
+ expect(resolved.field).toBe(field)
1396
+ expect(resolved.value).toBe(null)
1397
+ })
1398
+ })
1399
+
1400
+ describe("Vector Type", () => {
1401
+ it("should resolve vector with array of values", () => {
1402
+ const vecType = IDL.Vec(IDL.Text)
1403
+ const field = visitor.visitVec(vecType, IDL.Text, "tags")
1404
+
1405
+ const resolved = field.resolve(["a", "b", "c"])
1406
+
1407
+ expect(resolved.field).toBe(field)
1408
+ const value = resolved.value as Array<{
1409
+ field: ResultField
1410
+ value: unknown
1411
+ }>
1412
+ expect(value).toHaveLength(3)
1413
+ expect(value[0].value).toBe("a")
1414
+ expect(value[1].value).toBe("b")
1415
+ expect(value[2].value).toBe("c")
1416
+ })
1417
+
1418
+ it("should handle empty vector", () => {
1419
+ const vecType = IDL.Vec(IDL.Nat)
1420
+ const field = visitor.visitVec(vecType, IDL.Nat, "numbers")
1421
+
1422
+ const resolved = field.resolve([])
1423
+
1424
+ const value = resolved.value as Array<{
1425
+ field: ResultField
1426
+ value: unknown
1427
+ }>
1428
+ expect(value).toHaveLength(0)
1429
+ })
1430
+
1431
+ it("should handle null vector value", () => {
1432
+ const vecType = IDL.Vec(IDL.Text)
1433
+ const field = visitor.visitVec(vecType, IDL.Text, "items")
1434
+
1435
+ const resolved = field.resolve(null)
1436
+ expect(resolved.value).toBe(null)
1437
+ })
1438
+ })
1439
+
1440
+ describe("Blob Type", () => {
1441
+ it("should resolve blob with hex string value", () => {
1442
+ const blobType = IDL.Vec(IDL.Nat8)
1443
+ const field = visitor.visitVec(blobType, IDL.Nat8, "data")
1444
+
1445
+ const resolved = field.resolve(new Uint8Array([0x12, 0x34, 0xab, 0xcd]))
1446
+
1447
+ expect(resolved.field).toBe(field)
1448
+ expect(resolved.value).toBe("1234abcd")
1449
+ })
1450
+ })
1451
+
1452
+ describe("Recursive Type", () => {
1453
+ it("should resolve recursive type", () => {
1454
+ const Node = IDL.Rec()
1455
+ Node.fill(
1456
+ IDL.Record({
1457
+ value: IDL.Nat,
1458
+ children: IDL.Vec(Node),
1459
+ })
1460
+ )
1461
+
1462
+ const field = visitor.visitRec(
1463
+ Node,
1464
+ // We can just pass null or the constructed type if available, but visitRec implementation calls extract which calls accept on the internal type.
1465
+ IDL.Record({
1466
+ value: IDL.Nat,
1467
+ children: IDL.Vec(Node),
1468
+ }),
1469
+ "tree"
1470
+ )
1471
+
1472
+ const resolved = field.resolve({
1473
+ value: BigInt(42),
1474
+ children: [],
1475
+ })
1476
+
1477
+ // The recursive type should delegate to its inner record type
1478
+ expect(resolved.field.type).toBe("record")
1479
+ })
1480
+ })
1481
+
1482
+ describe("Nested Structures", () => {
1483
+ it("should resolve deeply nested structure", () => {
1484
+ const nestedType = IDL.Record({
1485
+ user: IDL.Record({
1486
+ profile: IDL.Record({
1487
+ name: IDL.Text,
1488
+ verified: IDL.Bool,
1489
+ }),
1490
+ }),
1491
+ })
1492
+
1493
+ const field = visitor.visitRecord(
1494
+ nestedType,
1495
+ [
1496
+ [
1497
+ "user",
1498
+ IDL.Record({
1499
+ profile: IDL.Record({
1500
+ name: IDL.Text,
1501
+ verified: IDL.Bool,
1502
+ }),
1503
+ }),
1504
+ ],
1505
+ ],
1506
+ "data"
1507
+ )
1508
+
1509
+ const resolved = field.resolve({
1510
+ user: {
1511
+ profile: {
1512
+ name: "Alice",
1513
+ verified: true,
1514
+ },
1515
+ },
1516
+ })
1517
+
1518
+ const value = resolved.value as Record<
1519
+ string,
1520
+ { field: ResultField; value: unknown }
1521
+ >
1522
+ const userValue = value.user.value as Record<
1523
+ string,
1524
+ { field: ResultField; value: unknown }
1525
+ >
1526
+ const profileValue = userValue.profile.value as Record<
1527
+ string,
1528
+ { field: ResultField; value: unknown }
1529
+ >
1530
+ expect(profileValue.name.value).toBe("Alice")
1531
+ expect(profileValue.verified.value).toBe(true)
1532
+ })
1533
+ })
1534
+ })
1535
+
1536
+ // ════════════════════════════════════════════════════════════════════════
1537
+ // generateMetadata() Method Tests
1538
+ // ════════════════════════════════════════════════════════════════════════
1539
+
1540
+ describe("generateMetadata() Method", () => {
1541
+ it("should generate metadata for single return value", () => {
1542
+ const service = IDL.Service({
1543
+ getName: IDL.Func([], [IDL.Text], ["query"]),
1544
+ })
1545
+
1546
+ const serviceMeta = visitor.visitService(
1547
+ service as unknown as IDL.ServiceClass
1548
+ )
1549
+ const methodMeta = serviceMeta["getName"]
1550
+
1551
+ const result = methodMeta.generateMetadata("Alice")
1552
+
1553
+ expect(result.functionName).toBe("getName")
1554
+ expect(result.functionType).toBe("query")
1555
+ expect(result.results).toHaveLength(1)
1556
+ expect(result.results[0].value).toBe("Alice")
1557
+ expect(result.results[0].field.type).toBe("text")
1558
+ })
1559
+
1560
+ it("should generate metadata for multiple return values", () => {
1561
+ const service = IDL.Service({
1562
+ getStats: IDL.Func([], [IDL.Nat, IDL.Nat, IDL.Text], ["query"]),
1563
+ })
1564
+
1565
+ const serviceMeta = visitor.visitService(
1566
+ service as unknown as IDL.ServiceClass
1567
+ )
1568
+ const methodMeta = serviceMeta["getStats"]
1569
+
1570
+ const result = methodMeta.generateMetadata([
1571
+ BigInt(100),
1572
+ BigInt(200),
1573
+ "active",
1574
+ ])
1575
+
1576
+ expect(result.results).toHaveLength(3)
1577
+ expect(result.results[0].value).toBe("100")
1578
+ expect(result.results[0].field.type).toBe("number")
1579
+ expect(result.results[1].value).toBe("200")
1580
+ expect(result.results[2].value).toBe("active")
1581
+ expect(result.results[2].field.type).toBe("text")
1582
+ })
1583
+
1584
+ it("should generate metadata for record return value", () => {
1585
+ const service = IDL.Service({
1586
+ getUser: IDL.Func(
1587
+ [],
1588
+ [IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
1589
+ ["query"]
1590
+ ),
1591
+ })
1592
+
1593
+ const serviceMeta = visitor.visitService(
1594
+ service as unknown as IDL.ServiceClass
1595
+ )
1596
+ const methodMeta = serviceMeta["getUser"]
1597
+
1598
+ const result = methodMeta.generateMetadata({
1599
+ name: "Bob",
1600
+ balance: BigInt(1000),
1601
+ })
1602
+
1603
+ expect(result.results).toHaveLength(1)
1604
+ expect(result.results[0].field.type).toBe("record")
1605
+
1606
+ const recordValue = result.results[0].value
1607
+ if (typeof recordValue !== "object" || recordValue === null) {
1608
+ throw new Error("Expected record value object")
1609
+ }
1610
+ const val = recordValue as Record<
1611
+ string,
1612
+ { field: ResultField; value: unknown }
1613
+ >
1614
+ expect(val.name.value).toBe("Bob")
1615
+ expect(val.balance.value).toBe("1000")
1616
+ })
1617
+
1618
+ it("should generate metadata for Result variant", () => {
1619
+ const service = IDL.Service({
1620
+ transfer: IDL.Func(
1621
+ [],
1622
+ [IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
1623
+ []
1624
+ ),
1625
+ })
1626
+
1627
+ const serviceMeta = visitor.visitService(
1628
+ service as unknown as IDL.ServiceClass
1629
+ )
1630
+ const methodMeta = serviceMeta["transfer"]
1631
+
1632
+ // Test Ok case
1633
+ const okResult = methodMeta.generateMetadata({ Ok: BigInt(12345) })
1634
+ expect(okResult.results[0].field.type).toBe("variant")
1635
+ if (okResult.results[0].field.type === "variant") {
1636
+ expect(okResult.results[0].field.displayType).toBe("result")
1637
+ } else {
1638
+ throw new Error("Expected variant field")
1639
+ }
1640
+
1641
+ const okValue = okResult.results[0].value as {
1642
+ option: string
1643
+ value: { field: ResultField; value: unknown }
1644
+ }
1645
+ expect(okValue.option).toBe("Ok")
1646
+ expect(okValue.value.value).toBe("12345")
1647
+
1648
+ // Test Err case
1649
+ const errResult = methodMeta.generateMetadata({
1650
+ Err: "Insufficient funds",
1651
+ })
1652
+ const errValue = errResult.results[0].value as {
1653
+ option: string
1654
+ value: { field: ResultField; value: unknown }
1655
+ }
1656
+ expect(errValue.option).toBe("Err")
1657
+ expect(errValue.value.value).toBe("Insufficient funds")
1658
+ })
1659
+
1660
+ it("should generate metadata for optional return value", () => {
1661
+ const service = IDL.Service({
1662
+ findUser: IDL.Func([], [IDL.Opt(IDL.Text)], ["query"]),
1663
+ })
1664
+
1665
+ const serviceMeta = visitor.visitService(
1666
+ service as unknown as IDL.ServiceClass
1667
+ )
1668
+ const methodMeta = serviceMeta["findUser"]
1669
+
1670
+ // Test with value - Optional is [value]
1671
+ const foundResult = methodMeta.generateMetadata(["Alice"])
1672
+ expect(foundResult.results[0].field.type).toBe("optional")
1673
+ const foundInner = foundResult.results[0].value as {
1674
+ field: ResultField
1675
+ value: unknown
1676
+ }
1677
+ expect(foundInner.value).toBe("Alice")
1678
+
1679
+ // Test with null - optional is []
1680
+ const notFoundResult = methodMeta.generateMetadata([])
1681
+ expect(notFoundResult.results[0].value).toBe(null)
1682
+ })
1683
+
1684
+ it("should generate metadata for vector return value", () => {
1685
+ const service = IDL.Service({
1686
+ getItems: IDL.Func([], [IDL.Vec(IDL.Text)], ["query"]),
1687
+ })
1688
+
1689
+ const serviceMeta = visitor.visitService(
1690
+ service as unknown as IDL.ServiceClass
1691
+ )
1692
+ const methodMeta = serviceMeta["getItems"]
1693
+
1694
+ const result = methodMeta.generateMetadata(["item1", "item2", "item3"])
1695
+
1696
+ expect(result.results[0].field.type).toBe("vector")
1697
+ const vecValue = result.results[0].value as Array<{
1698
+ field: ResultField
1699
+ value: unknown
1700
+ }>
1701
+ expect(vecValue).toHaveLength(3)
1702
+ expect(vecValue[0].value).toBe("item1")
1703
+ expect(vecValue[1].value).toBe("item2")
1704
+ expect(vecValue[2].value).toBe("item3")
1705
+ })
1706
+
1707
+ it("should generate metadata for update function", () => {
1708
+ const service = IDL.Service({
1709
+ setName: IDL.Func([IDL.Text], [IDL.Bool], []),
1710
+ })
1711
+
1712
+ const serviceMeta = visitor.visitService(
1713
+ service as unknown as IDL.ServiceClass
1714
+ )
1715
+ const methodMeta = serviceMeta["setName"]
1716
+
1717
+ expect(methodMeta.functionType).toBe("update")
1718
+
1719
+ const rawData = [true]
1720
+ const result = methodMeta.generateMetadata(rawData[0])
1721
+
1722
+ expect(result.functionType).toBe("update")
1723
+ expect(result.functionName).toBe("setName")
1724
+ expect(result.results[0].value).toBe(true)
1725
+ expect(result.results[0].raw).toBe(true)
1726
+ })
1727
+
1728
+ it("should generate metadata for complex ICRC-1 like response", () => {
1729
+ const Account = IDL.Record({
1730
+ owner: IDL.Principal,
1731
+ subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)),
1732
+ })
1733
+
1734
+ const TransferResult = IDL.Variant({
1735
+ Ok: IDL.Nat,
1736
+ Err: IDL.Variant({
1737
+ InsufficientFunds: IDL.Record({ balance: IDL.Nat }),
1738
+ InvalidAccount: IDL.Null,
1739
+ TooOld: IDL.Null,
1740
+ }),
1741
+ })
1742
+
1743
+ const service = IDL.Service({
1744
+ icrc1_transfer: IDL.Func([], [TransferResult], []),
1745
+ })
1746
+
1747
+ const serviceMeta = visitor.visitService(
1748
+ service as unknown as IDL.ServiceClass
1749
+ )
1750
+ const methodMeta = serviceMeta["icrc1_transfer"]
1751
+
1752
+ // Test successful transfer
1753
+ const successResult = methodMeta.generateMetadata({ Ok: BigInt(1000) })
1754
+ console.log(
1755
+ "🚀 ~ result:",
1756
+ JSON.stringify(
1757
+ successResult,
1758
+ (_, v) => (typeof v === "bigint" ? `${v}n` : v),
1759
+ 2
1760
+ )
1761
+ )
1762
+
1763
+ const successValue = successResult.results[0].value as {
1764
+ option: string
1765
+ value: { field: ResultField; value: unknown }
1766
+ }
1767
+ expect(successValue.option).toBe("Ok")
1768
+ expect(successValue.value.value).toBe("1000")
1769
+
1770
+ // Test error case
1771
+ const errorResult = methodMeta.generateMetadata({
1772
+ Err: { InsufficientFunds: { balance: BigInt(50) } },
1773
+ })
1774
+ const errorValue = errorResult.results[0].value as {
1775
+ option: string
1776
+ value: { field: ResultField; value: unknown }
1777
+ }
1778
+ expect(errorValue.option).toBe("Err")
1779
+
1780
+ const val = errorValue.value.value
1781
+ if (typeof val !== "object" || val === null || !("option" in val)) {
1782
+ throw new Error("Expected variant value object")
1783
+ }
1784
+ expect(val.option).toBe("InsufficientFunds")
1785
+ })
1786
+
1787
+ it("should handle empty return", () => {
1788
+ const service = IDL.Service({
1789
+ doSomething: IDL.Func([], [], []),
1790
+ })
1791
+
1792
+ const serviceMeta = visitor.visitService(
1793
+ service as unknown as IDL.ServiceClass
1794
+ )
1795
+ const methodMeta = serviceMeta["doSomething"]
1796
+
1797
+ expect(methodMeta.returnCount).toBe(0)
1798
+
1799
+ const result = methodMeta.generateMetadata([])
1800
+ expect(result.results).toHaveLength(0)
1801
+ })
1802
+ })
1803
+
1804
+ // ════════════════════════════════════════════════════════════════════════════
1805
+ // generateMetadataWithRaw() Method Tests
1806
+ // ════════════════════════════════════════════════════════════════════════════
1807
+
1808
+ describe("generateMetadataWithRaw() Method", () => {
1809
+ it("should include both raw and display values for single return", () => {
1810
+ const service = IDL.Service({
1811
+ getBalance: IDL.Func([], [IDL.Nat], ["query"]),
1812
+ })
1813
+
1814
+ const serviceMeta = visitor.visitService(
1815
+ service as unknown as IDL.ServiceClass
1816
+ )
1817
+ const methodMeta = serviceMeta["getBalance"]
1818
+
1819
+ // Simulate raw BigInt and display string
1820
+ const rawData = [BigInt(1000000)]
1821
+
1822
+ const result = methodMeta.generateMetadata(rawData[0])
1823
+
1824
+ expect(result.functionName).toBe("getBalance")
1825
+ expect(result.functionType).toBe("query")
1826
+ expect(result.results).toHaveLength(1)
1827
+ expect(result.results[0].raw).toBe(BigInt(1000000))
1828
+ expect(result.results[0].value).toBe("1000000")
1829
+ expect(result.results[0].raw).toBe(BigInt(1000000))
1830
+ expect(result.results[0].field.type).toBe("number")
1831
+ })
1832
+
1833
+ it("should include both raw and display values for multiple returns", () => {
1834
+ const service = IDL.Service({
1835
+ getStats: IDL.Func([], [IDL.Nat64, IDL.Text, IDL.Bool], ["query"]),
1836
+ })
1837
+
1838
+ const serviceMeta = visitor.visitService(
1839
+ service as unknown as IDL.ServiceClass
1840
+ )
1841
+ const methodMeta = serviceMeta["getStats"]
1842
+
1843
+ // Use BigInt with string to safe safe integer
1844
+ const rawData = [BigInt("9007199254740993"), "active", true]
1845
+
1846
+ const result = methodMeta.generateMetadata(rawData)
1847
+
1848
+ expect(result.results).toHaveLength(3)
1849
+
1850
+ // nat64 → string display, BigInt raw
1851
+ expect(result.results[0].value).toBe("9007199254740993")
1852
+ expect(result.results[0].raw).toBe(BigInt("9007199254740993"))
1853
+ expect(result.results[0].field.candidType).toBe("nat64")
1854
+
1855
+ // text → same for both
1856
+ expect(result.results[1].value).toBe("active")
1857
+ expect(result.results[1].raw).toBe("active")
1858
+
1859
+ // bool → same for both
1860
+ expect(result.results[2].value).toBe(true)
1861
+ expect(result.results[2].raw).toBe(true)
1862
+ })
1863
+
1864
+ it("should handle record with raw and display values", () => {
1865
+ const service = IDL.Service({
1866
+ getUser: IDL.Func(
1867
+ [],
1868
+ [IDL.Record({ name: IDL.Text, balance: IDL.Nat })],
1869
+ ["query"]
1870
+ ),
1871
+ })
1872
+
1873
+ const serviceMeta = visitor.visitService(
1874
+ service as unknown as IDL.ServiceClass
1875
+ )
1876
+ const methodMeta = serviceMeta["getUser"]
1877
+
1878
+ const rawData = [{ name: "Alice", balance: BigInt(500) }]
1879
+ const result = methodMeta.generateMetadata(rawData[0])
1880
+
1881
+ expect(result.results[0].raw).toEqual({
1882
+ name: "Alice",
1883
+ balance: BigInt(500),
1884
+ })
1885
+
1886
+ const recordValue = result.results[0].value
1887
+ if (typeof recordValue !== "object" || recordValue === null) {
1888
+ throw new Error("Expected record value object")
1889
+ }
1890
+ const val = recordValue as Record<
1891
+ string,
1892
+ { field: ResultField; value: unknown }
1893
+ >
1894
+ expect(val.name.value).toBe("Alice")
1895
+ expect(val.balance.value).toBe("500")
1896
+ })
1897
+
1898
+ it("should handle Result variant with raw and display values", () => {
1899
+ const service = IDL.Service({
1900
+ transfer: IDL.Func(
1901
+ [],
1902
+ [IDL.Variant({ Ok: IDL.Nat, Err: IDL.Text })],
1903
+ []
1904
+ ),
1905
+ })
1906
+
1907
+ const serviceMeta = visitor.visitService(
1908
+ service as unknown as IDL.ServiceClass
1909
+ )
1910
+ const methodMeta = serviceMeta["transfer"]
1911
+
1912
+ // Test Ok case with raw BigInt
1913
+ const rawData = [{ Ok: BigInt(12345) }]
1914
+
1915
+ const result = methodMeta.generateMetadata(rawData[0])
1916
+
1917
+ expect(result.results[0].raw).toEqual({ Ok: BigInt(12345) })
1918
+
1919
+ const variantValue = result.results[0].value as any
1920
+ if (
1921
+ typeof variantValue !== "object" ||
1922
+ variantValue === null ||
1923
+ !("option" in variantValue)
1924
+ ) {
1925
+ throw new Error("Expected variant value object")
1926
+ }
1927
+ expect(variantValue.option).toBe("Ok")
1928
+ const innerVal = variantValue.value
1929
+ expect(innerVal.value).toBe("12345")
1930
+ })
1931
+
1932
+ it("should preserve raw Principal object", () => {
1933
+ const { Principal } = require("@icp-sdk/core/principal")
1934
+
1935
+ const service = IDL.Service({
1936
+ getOwner: IDL.Func([], [IDL.Principal], ["query"]),
1937
+ })
1938
+
1939
+ const serviceMeta = visitor.visitService(
1940
+ service as unknown as IDL.ServiceClass
1941
+ )
1942
+ const methodMeta = serviceMeta["getOwner"]
1943
+
1944
+ const principal = Principal.fromText("aaaaa-aa")
1945
+ const rawData = [principal]
1946
+
1947
+ const result = methodMeta.generateMetadata(rawData[0])
1948
+
1949
+ expect(result.results[0].value).toBe("aaaaa-aa")
1950
+ expect(result.results[0].raw).toBe(principal)
1951
+ expect(result.results[0].field.type).toBe("principal")
1952
+ })
1953
+
1954
+ it("should handle vector with raw and display values", () => {
1955
+ const service = IDL.Service({
1956
+ getAmounts: IDL.Func([], [IDL.Vec(IDL.Nat)], ["query"]),
1957
+ })
1958
+
1959
+ const serviceMeta = visitor.visitService(
1960
+ service as unknown as IDL.ServiceClass
1961
+ )
1962
+ const methodMeta = serviceMeta["getAmounts"]
1963
+
1964
+ const rawData = [[BigInt(100), BigInt(200), BigInt(300)]]
1965
+
1966
+ const result = methodMeta.generateMetadata(rawData[0])
1967
+
1968
+ const vecValue = result.results[0].value
1969
+ if (!Array.isArray(vecValue)) {
1970
+ throw new Error("Expected vector value array")
1971
+ }
1972
+ expect(vecValue).toHaveLength(3)
1973
+ expect(vecValue[0].value).toBe("100")
1974
+ expect(vecValue[1].value).toBe("200")
1975
+ expect(vecValue[2].value).toBe("300")
1976
+ })
1977
+
1978
+ it("should handle optional with raw and display values", () => {
1979
+ const service = IDL.Service({
1980
+ findBalance: IDL.Func([], [IDL.Opt(IDL.Nat)], ["query"]),
1981
+ })
1982
+
1983
+ const serviceMeta = visitor.visitService(
1984
+ service as unknown as IDL.ServiceClass
1985
+ )
1986
+ const methodMeta = serviceMeta["findBalance"]
1987
+
1988
+ // Test with value - Optional is [value]
1989
+ const rawDataWithValue = [[BigInt(999)]]
1990
+
1991
+ const resultWithValue = methodMeta.generateMetadata(rawDataWithValue[0])
1992
+
1993
+ expect(resultWithValue.results[0].raw).toEqual([BigInt(999)])
1994
+ const innerValue = resultWithValue.results[0].value
1995
+ if (
1996
+ typeof innerValue !== "object" ||
1997
+ innerValue === null ||
1998
+ !("value" in innerValue)
1999
+ ) {
2000
+ throw new Error("Expected optional value object")
2001
+ }
2002
+ expect((innerValue as any).value).toBe("999")
2003
+
2004
+ // Test with null - Optional is []
2005
+ const rawDataNull = [[]]
2006
+
2007
+ const resultNull = methodMeta.generateMetadata(rawDataNull[0])
2008
+ expect(resultNull.results[0].raw).toEqual([])
2009
+ expect(resultNull.results[0].value).toBe(null)
2010
+ })
2011
+
2012
+ it("should handle empty return", () => {
2013
+ const service = IDL.Service({
2014
+ doNothing: IDL.Func([], [], []),
2015
+ })
2016
+
2017
+ const serviceMeta = visitor.visitService(
2018
+ service as unknown as IDL.ServiceClass
2019
+ )
2020
+ const methodMeta = serviceMeta["doNothing"]
2021
+
2022
+ const result = methodMeta.generateMetadata([])
2023
+
2024
+ expect(result.results).toHaveLength(0)
2025
+ })
2026
+ })
2027
+ })