@sundaeswap/sprinkles 0.6.1 → 0.8.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 (197) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  7. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  9. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +744 -0
  11. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  13. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  14. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +711 -0
  15. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  16. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  17. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  18. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  19. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  20. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  21. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  22. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  23. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/index.js +174 -0
  29. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  30. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  31. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  32. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  33. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  34. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  35. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  36. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  37. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  38. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  39. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  40. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  41. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  42. package/dist/cjs/Sprinkle/actions/cli-adapter.js +390 -0
  43. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  44. package/dist/cjs/Sprinkle/actions/index.js +139 -0
  45. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  46. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +557 -0
  47. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  48. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  49. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  50. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  51. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  52. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  53. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  54. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  55. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  56. package/dist/cjs/Sprinkle/index.js +678 -5
  57. package/dist/cjs/Sprinkle/index.js.map +1 -1
  58. package/dist/cjs/Sprinkle/prompts.js +12 -7
  59. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  60. package/dist/cjs/Sprinkle/schemas.js +17 -1
  61. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  62. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  63. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  64. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  65. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  66. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  67. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  68. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  69. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  70. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  71. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  72. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +742 -0
  73. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  74. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  75. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  76. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +710 -0
  77. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  78. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  79. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  80. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  81. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  82. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  83. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  84. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  85. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  86. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  87. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  88. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  89. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  90. package/dist/esm/Sprinkle/actions/builtin/index.js +37 -0
  91. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  92. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  93. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  94. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  95. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  96. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  97. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  98. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  99. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  100. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  101. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  102. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  103. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  104. package/dist/esm/Sprinkle/actions/cli-adapter.js +379 -0
  105. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  106. package/dist/esm/Sprinkle/actions/index.js +12 -0
  107. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  108. package/dist/esm/Sprinkle/actions/mcp-adapter.js +547 -0
  109. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  110. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  111. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  112. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  113. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  114. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  115. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  116. package/dist/esm/Sprinkle/actions/types.js +61 -0
  117. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  118. package/dist/esm/Sprinkle/index.js +517 -7
  119. package/dist/esm/Sprinkle/index.js.map +1 -1
  120. package/dist/esm/Sprinkle/prompts.js +12 -7
  121. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  122. package/dist/esm/Sprinkle/schemas.js +16 -0
  123. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  124. package/dist/esm/Sprinkle/type-guards.js +3 -0
  125. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  126. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  127. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  128. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  129. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  130. package/dist/types/Sprinkle/actions/builtin/index.d.ts +30 -0
  131. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  132. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  133. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  134. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  135. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  136. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  137. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  138. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  139. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  140. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  141. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  142. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  143. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  144. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  145. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  146. package/dist/types/Sprinkle/actions/index.d.ts +13 -0
  147. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  148. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +116 -0
  149. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  150. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  151. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  152. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  153. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  154. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  155. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  156. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  157. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  158. package/dist/types/Sprinkle/index.d.ts +84 -2
  159. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  160. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  161. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  162. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  163. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  164. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  165. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  166. package/package.json +9 -2
  167. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  168. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  169. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  170. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  171. package/src/Sprinkle/__tests__/cli-adapter.test.ts +736 -0
  172. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  173. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +720 -0
  174. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  175. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  176. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  177. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  178. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  179. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  180. package/src/Sprinkle/actions/builtin/index.ts +125 -0
  181. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  182. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  183. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  184. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  185. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  186. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  187. package/src/Sprinkle/actions/cli-adapter.ts +446 -0
  188. package/src/Sprinkle/actions/index.ts +33 -0
  189. package/src/Sprinkle/actions/mcp-adapter.ts +638 -0
  190. package/src/Sprinkle/actions/registry.ts +97 -0
  191. package/src/Sprinkle/actions/runner.ts +200 -0
  192. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  193. package/src/Sprinkle/actions/types.ts +91 -0
  194. package/src/Sprinkle/index.ts +612 -3
  195. package/src/Sprinkle/prompts.ts +118 -72
  196. package/src/Sprinkle/schemas.ts +20 -0
  197. package/src/Sprinkle/type-guards.ts +9 -0
@@ -0,0 +1,341 @@
1
+ import { describe, expect, test, mock } from "bun:test";
2
+ import { ActionError } from "../actions/types.js";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Mocks — mirror the Blaze SDK surface used by native-script.ts
6
+ // ---------------------------------------------------------------------------
7
+
8
+ let provideScriptCalls: any[] = [];
9
+ const mockCompletedTx = { toCbor: () => "mock-tx-cbor" };
10
+
11
+ mock.module("@blaze-cardano/sdk", () => ({
12
+ Core: {
13
+ Ed25519KeyHashHex: (h: string) => h,
14
+ Slot: (n: number) => n,
15
+ HexBlob: (s: string) => s,
16
+ ScriptPubkey: class {
17
+ hash: string;
18
+ constructor(h: string) {
19
+ this.hash = h;
20
+ }
21
+ },
22
+ ScriptAll: class {
23
+ scripts: any[];
24
+ constructor(s: any[]) {
25
+ this.scripts = s;
26
+ }
27
+ },
28
+ ScriptAny: class {
29
+ scripts: any[];
30
+ constructor(s: any[]) {
31
+ this.scripts = s;
32
+ }
33
+ },
34
+ ScriptNOfK: class {
35
+ scripts: any[];
36
+ n: number;
37
+ constructor(s: any[], n: number) {
38
+ this.scripts = s;
39
+ this.n = n;
40
+ }
41
+ },
42
+ TimelockExpiry: class {
43
+ slot: number;
44
+ constructor(s: number) {
45
+ this.slot = s;
46
+ }
47
+ },
48
+ TimelockStart: class {
49
+ slot: number;
50
+ constructor(s: number) {
51
+ this.slot = s;
52
+ }
53
+ },
54
+ NativeScript: {
55
+ fromCbor: (hex: string) => {
56
+ if (hex === "invalid") throw new Error("Invalid CBOR");
57
+ return { __fromCbor: hex };
58
+ },
59
+ newScriptPubkey: (sp: any) => ({ __type: "pubkey", value: sp }),
60
+ newScriptAll: (sa: any) => ({ __type: "all", value: sa }),
61
+ newScriptAny: (sa: any) => ({ __type: "any", value: sa }),
62
+ newScriptNOfK: (sn: any) => ({ __type: "nofk", value: sn }),
63
+ newTimelockExpiry: (te: any) => ({ __type: "timelockExpiry", value: te }),
64
+ newTimelockStart: (ts: any) => ({ __type: "timelockStart", value: ts }),
65
+ },
66
+ Script: {
67
+ newNativeScript: (ns: any) => ({
68
+ __nativeScript: ns,
69
+ hash: () => {
70
+ // Produce a deterministic hex hash from the native script content
71
+ if (ns?.__type === "pubkey") return ns.value.hash;
72
+ if (ns?.__fromCbor) return "cbor" + ns.__fromCbor;
73
+ return "0".repeat(56);
74
+ },
75
+ }),
76
+ },
77
+ },
78
+ }));
79
+
80
+ const { toNativeScript, completeWithScripts } = await import(
81
+ "../actions/builtin/native-script.js"
82
+ );
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // toNativeScript — CBOR hex input
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("toNativeScript — CBOR hex", () => {
89
+ test("converts valid CBOR hex string", () => {
90
+ const result = toNativeScript("82018200");
91
+ expect(result.__nativeScript).toEqual({ __fromCbor: "82018200" });
92
+ expect(typeof result.hash).toBe("function");
93
+ });
94
+
95
+ test("throws INVALID_NATIVE_SCRIPT for invalid CBOR", () => {
96
+ expect(() => toNativeScript("invalid")).toThrow(ActionError);
97
+ try {
98
+ toNativeScript("invalid");
99
+ } catch (err: any) {
100
+ expect(err.code).toBe("INVALID_NATIVE_SCRIPT");
101
+ }
102
+ });
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // toNativeScript — MultisigScript JSON input
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe("toNativeScript — MultisigScript JSON", () => {
110
+ const keyHash = "a".repeat(56);
111
+
112
+ test("converts Signature", () => {
113
+ const result = toNativeScript({ Signature: { key_hash: keyHash } });
114
+ expect(result.__nativeScript.__type).toBe("pubkey");
115
+ expect(result.__nativeScript.value.hash).toBe(keyHash);
116
+ });
117
+
118
+ test("converts AllOf with nested scripts", () => {
119
+ const result = toNativeScript({
120
+ AllOf: {
121
+ scripts: [
122
+ { Signature: { key_hash: keyHash } },
123
+ { Signature: { key_hash: "b".repeat(56) } },
124
+ ],
125
+ },
126
+ });
127
+ expect(result.__nativeScript.__type).toBe("all");
128
+ expect(result.__nativeScript.value.scripts).toHaveLength(2);
129
+ });
130
+
131
+ test("converts AnyOf", () => {
132
+ const result = toNativeScript({
133
+ AnyOf: {
134
+ scripts: [{ Signature: { key_hash: keyHash } }],
135
+ },
136
+ });
137
+ expect(result.__nativeScript.__type).toBe("any");
138
+ });
139
+
140
+ test("converts AtLeast", () => {
141
+ const result = toNativeScript({
142
+ AtLeast: {
143
+ required: 2n,
144
+ scripts: [
145
+ { Signature: { key_hash: keyHash } },
146
+ { Signature: { key_hash: "b".repeat(56) } },
147
+ { Signature: { key_hash: "c".repeat(56) } },
148
+ ],
149
+ },
150
+ });
151
+ expect(result.__nativeScript.__type).toBe("nofk");
152
+ expect(result.__nativeScript.value.n).toBe(2);
153
+ expect(result.__nativeScript.value.scripts).toHaveLength(3);
154
+ });
155
+
156
+ test("converts Before (TimelockExpiry)", () => {
157
+ const result = toNativeScript({ Before: { time: 1000n } });
158
+ expect(result.__nativeScript.__type).toBe("timelockExpiry");
159
+ expect(result.__nativeScript.value.slot).toBe(1000);
160
+ });
161
+
162
+ test("converts After (TimelockStart)", () => {
163
+ const result = toNativeScript({ After: { time: 500n } });
164
+ expect(result.__nativeScript.__type).toBe("timelockStart");
165
+ expect(result.__nativeScript.value.slot).toBe(500);
166
+ });
167
+
168
+ test("converts Script (script hash reference)", () => {
169
+ const result = toNativeScript({ Script: { script_hash: keyHash } });
170
+ expect(result.__nativeScript.__type).toBe("pubkey");
171
+ expect(result.__nativeScript.value.hash).toBe(keyHash);
172
+ });
173
+
174
+ test("handles deeply nested structures", () => {
175
+ const result = toNativeScript({
176
+ AllOf: {
177
+ scripts: [
178
+ {
179
+ AtLeast: {
180
+ required: 1n,
181
+ scripts: [
182
+ { Signature: { key_hash: keyHash } },
183
+ {
184
+ AnyOf: {
185
+ scripts: [{ Signature: { key_hash: "b".repeat(56) } }],
186
+ },
187
+ },
188
+ ],
189
+ },
190
+ },
191
+ { After: { time: 100n } },
192
+ ],
193
+ },
194
+ });
195
+ expect(result.__nativeScript.__type).toBe("all");
196
+ expect(result.__nativeScript.value.scripts).toHaveLength(2);
197
+ });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // completeWithScripts
202
+ // ---------------------------------------------------------------------------
203
+
204
+ describe("completeWithScripts", () => {
205
+ function makeTxBuilder() {
206
+ provideScriptCalls = [];
207
+ const builder: any = {
208
+ provideScript: (script: any) => {
209
+ provideScriptCalls.push(script);
210
+ return builder;
211
+ },
212
+ complete: async () => mockCompletedTx,
213
+ };
214
+ return builder;
215
+ }
216
+
217
+ test("completes without scripts when none provided", async () => {
218
+ const builder = makeTxBuilder();
219
+ const tx = await completeWithScripts(builder);
220
+ expect(tx).toBe(mockCompletedTx);
221
+ expect(provideScriptCalls).toHaveLength(0);
222
+ });
223
+
224
+ test("completes without scripts when empty array provided", async () => {
225
+ const builder = makeTxBuilder();
226
+ const tx = await completeWithScripts(builder, []);
227
+ expect(tx).toBe(mockCompletedTx);
228
+ expect(provideScriptCalls).toHaveLength(0);
229
+ });
230
+
231
+ test("attaches scripts before completing", async () => {
232
+ const builder = makeTxBuilder();
233
+ const keyHash = "a".repeat(56);
234
+ const tx = await completeWithScripts(builder, [
235
+ { Signature: { key_hash: keyHash } },
236
+ "82018200",
237
+ ]);
238
+ expect(tx).toBe(mockCompletedTx);
239
+ expect(provideScriptCalls).toHaveLength(2);
240
+ });
241
+
242
+ test("propagates ActionError from invalid script input", async () => {
243
+ const builder = makeTxBuilder();
244
+ await expect(
245
+ completeWithScripts(builder, ["invalid"]),
246
+ ).rejects.toMatchObject({ code: "INVALID_NATIVE_SCRIPT" });
247
+ });
248
+
249
+ test("retries with addressbook script on missing script hash error", async () => {
250
+ const keyHash = "a".repeat(56);
251
+ const expectedScriptHash = keyHash;
252
+ let attempt = 0;
253
+ provideScriptCalls = [];
254
+ const builder: any = {
255
+ provideScript: (script: any) => {
256
+ provideScriptCalls.push(script);
257
+ return builder;
258
+ },
259
+ complete: async () => {
260
+ attempt++;
261
+ if (attempt === 1) {
262
+ throw new Error(
263
+ `complete: Could not resolve script hash ${expectedScriptHash}`,
264
+ );
265
+ }
266
+ return mockCompletedTx;
267
+ },
268
+ };
269
+
270
+ const context = {
271
+ sprinkle: {
272
+ addressbook: {
273
+ "my-multisig": { Signature: { key_hash: keyHash } },
274
+ },
275
+ },
276
+ settings: {},
277
+ } as any;
278
+
279
+ const tx = await completeWithScripts(builder, undefined, context);
280
+ expect(tx).toBe(mockCompletedTx);
281
+ expect(attempt).toBe(2);
282
+ expect(provideScriptCalls).toHaveLength(1);
283
+ });
284
+
285
+ test("throws original error when script hash not in addressbook", async () => {
286
+ const builder: any = {
287
+ provideScript: () => builder,
288
+ complete: async () => {
289
+ throw new Error("complete: Could not resolve script hash deadbeef1234");
290
+ },
291
+ };
292
+
293
+ const context = {
294
+ sprinkle: {
295
+ addressbook: {
296
+ "unrelated": { Signature: { key_hash: "b".repeat(56) } },
297
+ },
298
+ },
299
+ settings: {},
300
+ } as any;
301
+
302
+ await expect(
303
+ completeWithScripts(builder, undefined, context),
304
+ ).rejects.toThrow("Could not resolve script hash deadbeef1234");
305
+ });
306
+
307
+ test("throws original error when no addressbook in context", async () => {
308
+ const builder: any = {
309
+ provideScript: () => builder,
310
+ complete: async () => {
311
+ throw new Error("complete: Could not resolve script hash deadbeef1234");
312
+ },
313
+ };
314
+
315
+ await expect(
316
+ completeWithScripts(builder, undefined, undefined),
317
+ ).rejects.toThrow("Could not resolve script hash deadbeef1234");
318
+ });
319
+
320
+ test("throws non-script errors without retry", async () => {
321
+ const builder: any = {
322
+ provideScript: () => builder,
323
+ complete: async () => {
324
+ throw new Error("some other error");
325
+ },
326
+ };
327
+
328
+ const context = {
329
+ sprinkle: {
330
+ addressbook: {
331
+ "my-sig": { Signature: { key_hash: "a".repeat(56) } },
332
+ },
333
+ },
334
+ settings: {},
335
+ } as any;
336
+
337
+ await expect(
338
+ completeWithScripts(builder, undefined, context),
339
+ ).rejects.toThrow("some other error");
340
+ });
341
+ });
@@ -0,0 +1,325 @@
1
+ import { describe, expect, test, mock, beforeEach } from "bun:test";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { promptAndExecute } from "../actions/tui-helpers.js";
4
+ import { UserCancelledError } from "../types.js";
5
+ import type { IAction } from "../actions/types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Fixtures
9
+ // ---------------------------------------------------------------------------
10
+
11
+ // Minimal action used throughout these tests
12
+ const greetAction: IAction<{ name: string }, { greeting: string }, any> = {
13
+ name: "greet",
14
+ description: "Greets someone",
15
+ inputSchema: Type.Object({ name: Type.String() }),
16
+ outputSchema: Type.Object({ greeting: Type.String() }),
17
+ execute: async (input: { name: string }) => ({
18
+ greeting: `Hello, ${input.name}!`,
19
+ }),
20
+ };
21
+
22
+ // Build a minimal Sprinkle-shaped mock. promptAndExecute only calls:
23
+ // - sprinkle.FillInStruct(schema, defaults)
24
+ // - sprinkle.settings (read once for context)
25
+ // so we only need those two members.
26
+ function makeMockSprinkle(fillInStructImpl: (schema: any, defaults?: any) => Promise<any>) {
27
+ return {
28
+ FillInStruct: mock(fillInStructImpl),
29
+ settings: { apiUrl: "https://example.com" },
30
+ } as any;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // describe: promptAndExecute – success path
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe("promptAndExecute – success path", () => {
38
+ test("returns success result when FillInStruct resolves", async () => {
39
+ const sprinkle = makeMockSprinkle(async () => ({ name: "Alice" }));
40
+
41
+ const result = await promptAndExecute(sprinkle, greetAction);
42
+
43
+ expect(result.success).toBe(true);
44
+ if (result.success) {
45
+ expect(result.data).toEqual({ greeting: "Hello, Alice!" });
46
+ }
47
+ });
48
+
49
+ test("passes the schema from the action to FillInStruct", async () => {
50
+ let capturedSchema: any;
51
+ const sprinkle = makeMockSprinkle(async (schema: any) => {
52
+ capturedSchema = schema;
53
+ return { name: "Bob" };
54
+ });
55
+
56
+ await promptAndExecute(sprinkle, greetAction);
57
+
58
+ expect(capturedSchema).toBe(greetAction.inputSchema);
59
+ });
60
+
61
+ test("passes action execute function the validated input", async () => {
62
+ let capturedInput: any;
63
+ const action = {
64
+ ...greetAction,
65
+ execute: async (input: { name: string }) => {
66
+ capturedInput = input;
67
+ return { greeting: `Hi, ${input.name}!` };
68
+ },
69
+ };
70
+ const sprinkle = makeMockSprinkle(async () => ({ name: "Charlie" }));
71
+
72
+ await promptAndExecute(sprinkle, action);
73
+
74
+ expect(capturedInput).toEqual({ name: "Charlie" });
75
+ });
76
+
77
+ test("provides the sprinkle instance via context to execute", async () => {
78
+ let capturedContext: any;
79
+ const action = {
80
+ ...greetAction,
81
+ execute: async (_input: any, ctx: any) => {
82
+ capturedContext = ctx;
83
+ return { greeting: "hi" };
84
+ },
85
+ };
86
+ const sprinkle = makeMockSprinkle(async () => ({ name: "Dan" }));
87
+
88
+ await promptAndExecute(sprinkle, action);
89
+
90
+ // The context's sprinkle should refer to the same mock instance
91
+ expect(capturedContext).toBeDefined();
92
+ expect(capturedContext.sprinkle).toBe(sprinkle);
93
+ });
94
+
95
+ test("provides sprinkle.settings via context to execute", async () => {
96
+ let capturedSettings: any;
97
+ const action = {
98
+ ...greetAction,
99
+ execute: async (_input: any, ctx: any) => {
100
+ capturedSettings = ctx.settings;
101
+ return { greeting: "hi" };
102
+ },
103
+ };
104
+ const sprinkle = makeMockSprinkle(async () => ({ name: "Eve" }));
105
+
106
+ await promptAndExecute(sprinkle, action);
107
+
108
+ expect(capturedSettings).toEqual({ apiUrl: "https://example.com" });
109
+ });
110
+
111
+ test("returns failure when execute throws an Error", async () => {
112
+ const action = {
113
+ ...greetAction,
114
+ execute: async () => {
115
+ throw new Error("execute failed");
116
+ },
117
+ };
118
+ const sprinkle = makeMockSprinkle(async () => ({ name: "Fail" }));
119
+
120
+ const result = await promptAndExecute(sprinkle, action);
121
+
122
+ expect(result.success).toBe(false);
123
+ if (!result.success) {
124
+ expect(result.error.code).toBe("EXECUTION_ERROR");
125
+ }
126
+ });
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // describe: promptAndExecute – defaults parameter
131
+ // ---------------------------------------------------------------------------
132
+
133
+ describe("promptAndExecute – defaults parameter", () => {
134
+ test("passes defaults to FillInStruct when provided", async () => {
135
+ let capturedDefaults: any;
136
+ const sprinkle = makeMockSprinkle(async (_schema: any, defaults: any) => {
137
+ capturedDefaults = defaults;
138
+ return { name: "Frank" };
139
+ });
140
+
141
+ await promptAndExecute(sprinkle, greetAction, { name: "Frank" });
142
+
143
+ expect(capturedDefaults).toEqual({ name: "Frank" });
144
+ });
145
+
146
+ test("passes undefined to FillInStruct when defaults are omitted", async () => {
147
+ let capturedDefaults: any = "sentinel";
148
+ const sprinkle = makeMockSprinkle(async (_schema: any, defaults: any) => {
149
+ capturedDefaults = defaults;
150
+ return { name: "Grace" };
151
+ });
152
+
153
+ await promptAndExecute(sprinkle, greetAction);
154
+
155
+ expect(capturedDefaults).toBeUndefined();
156
+ });
157
+
158
+ test("passes partial defaults to FillInStruct", async () => {
159
+ // Action with multiple fields so we can supply only some defaults
160
+ const multiAction: IAction<{ name: string; count: number }, { result: string }, any> = {
161
+ name: "multi",
162
+ description: "Takes multiple fields",
163
+ inputSchema: Type.Object({ name: Type.String(), count: Type.Number() }),
164
+ outputSchema: Type.Object({ result: Type.String() }),
165
+ execute: async (input) => ({ result: `${input.name}:${input.count}` }),
166
+ };
167
+
168
+ let capturedDefaults: any;
169
+ const sprinkle = makeMockSprinkle(async (_schema: any, defaults: any) => {
170
+ capturedDefaults = defaults;
171
+ return { name: "Henry", count: 42 };
172
+ });
173
+
174
+ await promptAndExecute(sprinkle, multiAction, { name: "Henry" });
175
+
176
+ expect(capturedDefaults).toEqual({ name: "Henry" });
177
+ });
178
+ });
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // describe: promptAndExecute – UserCancelledError handling
182
+ // ---------------------------------------------------------------------------
183
+
184
+ describe("promptAndExecute – UserCancelledError handling", () => {
185
+ test("returns IActionFailure with code USER_CANCELLED when FillInStruct throws UserCancelledError", async () => {
186
+ const sprinkle = makeMockSprinkle(async () => {
187
+ throw new UserCancelledError("cancelled");
188
+ });
189
+
190
+ const result = await promptAndExecute(sprinkle, greetAction);
191
+
192
+ expect(result.success).toBe(false);
193
+ if (!result.success) {
194
+ expect(result.error.code).toBe("USER_CANCELLED");
195
+ }
196
+ });
197
+
198
+ test("does not throw when FillInStruct throws UserCancelledError", async () => {
199
+ const sprinkle = makeMockSprinkle(async () => {
200
+ throw new UserCancelledError();
201
+ });
202
+
203
+ // Should resolve (not reject)
204
+ await expect(promptAndExecute(sprinkle, greetAction)).resolves.toBeDefined();
205
+ });
206
+
207
+ test("USER_CANCELLED failure has a descriptive message", async () => {
208
+ const sprinkle = makeMockSprinkle(async () => {
209
+ throw new UserCancelledError("user pressed escape");
210
+ });
211
+
212
+ const result = await promptAndExecute(sprinkle, greetAction);
213
+
214
+ expect(result.success).toBe(false);
215
+ if (!result.success) {
216
+ expect(typeof result.error.message).toBe("string");
217
+ expect(result.error.message.length).toBeGreaterThan(0);
218
+ }
219
+ });
220
+
221
+ test("USER_CANCELLED failure carries the original UserCancelledError as details", async () => {
222
+ const cancelled = new UserCancelledError("esc pressed");
223
+ const sprinkle = makeMockSprinkle(async () => {
224
+ throw cancelled;
225
+ });
226
+
227
+ const result = await promptAndExecute(sprinkle, greetAction);
228
+
229
+ expect(result.success).toBe(false);
230
+ if (!result.success) {
231
+ expect(result.error.details).toBe(cancelled);
232
+ }
233
+ });
234
+
235
+ test("does not call execute when FillInStruct throws UserCancelledError", async () => {
236
+ let executeCalled = false;
237
+ const action = {
238
+ ...greetAction,
239
+ execute: async (_input: any) => {
240
+ executeCalled = true;
241
+ return { greeting: "hi" };
242
+ },
243
+ };
244
+ const sprinkle = makeMockSprinkle(async () => {
245
+ throw new UserCancelledError();
246
+ });
247
+
248
+ await promptAndExecute(sprinkle, action);
249
+
250
+ expect(executeCalled).toBe(false);
251
+ });
252
+ });
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // describe: promptAndExecute – unexpected FillInStruct errors
256
+ // ---------------------------------------------------------------------------
257
+
258
+ describe("promptAndExecute – unexpected FillInStruct errors", () => {
259
+ test("returns IActionFailure with code PROMPT_ERROR when FillInStruct throws a non-UserCancelledError", async () => {
260
+ const sprinkle = makeMockSprinkle(async () => {
261
+ throw new Error("TTY not available");
262
+ });
263
+
264
+ const result = await promptAndExecute(sprinkle, greetAction);
265
+
266
+ expect(result.success).toBe(false);
267
+ if (!result.success) {
268
+ expect(result.error.code).toBe("PROMPT_ERROR");
269
+ }
270
+ });
271
+
272
+ test("PROMPT_ERROR message includes the original error message", async () => {
273
+ const sprinkle = makeMockSprinkle(async () => {
274
+ throw new Error("underlying prompt failure");
275
+ });
276
+
277
+ const result = await promptAndExecute(sprinkle, greetAction);
278
+
279
+ expect(result.success).toBe(false);
280
+ if (!result.success) {
281
+ expect(result.error.message).toContain("underlying prompt failure");
282
+ }
283
+ });
284
+
285
+ test("does not throw when FillInStruct throws an unexpected error", async () => {
286
+ const sprinkle = makeMockSprinkle(async () => {
287
+ throw new Error("unexpected");
288
+ });
289
+
290
+ await expect(promptAndExecute(sprinkle, greetAction)).resolves.toBeDefined();
291
+ });
292
+
293
+ test("PROMPT_ERROR result includes non-Error thrown values in message", async () => {
294
+ const sprinkle = makeMockSprinkle(async () => {
295
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
296
+ throw "some string error";
297
+ });
298
+
299
+ const result = await promptAndExecute(sprinkle, greetAction);
300
+
301
+ expect(result.success).toBe(false);
302
+ if (!result.success) {
303
+ expect(result.error.code).toBe("PROMPT_ERROR");
304
+ expect(result.error.message).toContain("some string error");
305
+ }
306
+ });
307
+ });
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // describe: promptAndExecute – input validation
311
+ // ---------------------------------------------------------------------------
312
+
313
+ describe("promptAndExecute – input validation", () => {
314
+ test("returns VALIDATION_ERROR when FillInStruct returns data that fails schema validation", async () => {
315
+ // FillInStruct returns a value that does not conform to the inputSchema
316
+ const sprinkle = makeMockSprinkle(async () => ({ name: 42 })); // name should be a string
317
+
318
+ const result = await promptAndExecute(sprinkle, greetAction);
319
+
320
+ expect(result.success).toBe(false);
321
+ if (!result.success) {
322
+ expect(result.error.code).toBe("VALIDATION_ERROR");
323
+ }
324
+ });
325
+ });