@sundaeswap/sprinkles 0.6.0 → 0.7.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 (158) hide show
  1. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  7. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  9. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +722 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  11. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +138 -0
  12. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +713 -0
  14. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  15. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  16. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  17. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  18. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  19. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  20. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  21. package/dist/cjs/Sprinkle/actions/builtin/index.js +117 -0
  22. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  23. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  29. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  30. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  31. package/dist/cjs/Sprinkle/actions/cli-adapter.js +372 -0
  32. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  33. package/dist/cjs/Sprinkle/actions/index.js +127 -0
  34. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  35. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +415 -0
  36. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  37. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  38. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  39. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  40. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  41. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  42. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  43. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  44. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  45. package/dist/cjs/Sprinkle/index.js +451 -4
  46. package/dist/cjs/Sprinkle/index.js.map +1 -1
  47. package/dist/cjs/Sprinkle/prompts.js +12 -7
  48. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  49. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  50. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  51. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  52. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  53. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  54. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  55. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  56. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  57. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  58. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  59. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +720 -0
  60. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  61. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +138 -0
  62. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  63. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +712 -0
  64. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  65. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  66. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  67. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  68. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  69. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  70. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  71. package/dist/esm/Sprinkle/actions/builtin/index.js +32 -0
  72. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  73. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  74. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  75. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  76. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  77. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  78. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  79. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  80. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  81. package/dist/esm/Sprinkle/actions/cli-adapter.js +361 -0
  82. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  83. package/dist/esm/Sprinkle/actions/index.js +12 -0
  84. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  85. package/dist/esm/Sprinkle/actions/mcp-adapter.js +407 -0
  86. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  87. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  88. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  89. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  90. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  91. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  92. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  93. package/dist/esm/Sprinkle/actions/types.js +61 -0
  94. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  95. package/dist/esm/Sprinkle/index.js +299 -4
  96. package/dist/esm/Sprinkle/index.js.map +1 -1
  97. package/dist/esm/Sprinkle/prompts.js +12 -7
  98. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  99. package/dist/esm/Sprinkle/type-guards.js +3 -0
  100. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  101. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  102. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  103. package/dist/types/Sprinkle/actions/builtin/index.d.ts +26 -0
  104. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  105. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  106. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  107. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  108. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  109. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  110. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  111. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  112. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  113. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  114. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  115. package/dist/types/Sprinkle/actions/index.d.ts +12 -0
  116. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  117. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +92 -0
  118. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  119. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  120. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  121. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  122. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  123. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  124. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  125. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  126. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  127. package/dist/types/Sprinkle/index.d.ts +81 -1
  128. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  129. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  130. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  131. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  132. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  133. package/package.json +9 -2
  134. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  135. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  136. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  137. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  138. package/src/Sprinkle/__tests__/cli-adapter.test.ts +715 -0
  139. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +144 -0
  140. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +718 -0
  141. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  142. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  143. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  144. package/src/Sprinkle/actions/builtin/index.ts +86 -0
  145. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  146. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  147. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  148. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  149. package/src/Sprinkle/actions/cli-adapter.ts +430 -0
  150. package/src/Sprinkle/actions/index.ts +32 -0
  151. package/src/Sprinkle/actions/mcp-adapter.ts +463 -0
  152. package/src/Sprinkle/actions/registry.ts +97 -0
  153. package/src/Sprinkle/actions/runner.ts +200 -0
  154. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  155. package/src/Sprinkle/actions/types.ts +91 -0
  156. package/src/Sprinkle/index.ts +395 -3
  157. package/src/Sprinkle/prompts.ts +118 -72
  158. package/src/Sprinkle/type-guards.ts +9 -0
@@ -0,0 +1,187 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { ActionRegistry, ActionError } from "../actions/index.js";
4
+
5
+ // Minimal valid action factory for test convenience
6
+ function makeAction(overrides: Record<string, unknown> = {}) {
7
+ return {
8
+ name: "my-action",
9
+ description: "A test action",
10
+ inputSchema: Type.Object({ value: Type.String() }),
11
+ outputSchema: Type.Object({ result: Type.String() }),
12
+ execute: async (input: { value: string }) => ({ result: input.value }),
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe("ActionRegistry", () => {
18
+ describe("register()", () => {
19
+ test("registers a valid action", () => {
20
+ const registry = new ActionRegistry();
21
+ const action = makeAction();
22
+ registry.register(action as any);
23
+ expect(registry.has("my-action")).toBe(true);
24
+ });
25
+
26
+ test("accepts single-word kebab-case names", () => {
27
+ const registry = new ActionRegistry();
28
+ registry.register(makeAction({ name: "transfer" }) as any);
29
+ expect(registry.has("transfer")).toBe(true);
30
+ });
31
+
32
+ test("accepts multi-segment kebab-case names", () => {
33
+ const registry = new ActionRegistry();
34
+ registry.register(makeAction({ name: "get-wallet-balance" }) as any);
35
+ expect(registry.has("get-wallet-balance")).toBe(true);
36
+ });
37
+
38
+ test("accepts names with digits", () => {
39
+ const registry = new ActionRegistry();
40
+ registry.register(makeAction({ name: "action2-v3" }) as any);
41
+ expect(registry.has("action2-v3")).toBe(true);
42
+ });
43
+
44
+ test("throws for UpperCase names", () => {
45
+ const registry = new ActionRegistry();
46
+ expect(() =>
47
+ registry.register(makeAction({ name: "MyAction" }) as any),
48
+ ).toThrow(/Invalid action name/);
49
+ });
50
+
51
+ test("throws for names with spaces", () => {
52
+ const registry = new ActionRegistry();
53
+ expect(() =>
54
+ registry.register(makeAction({ name: "my action" }) as any),
55
+ ).toThrow(/Invalid action name/);
56
+ });
57
+
58
+ test("throws for names with leading hyphens", () => {
59
+ const registry = new ActionRegistry();
60
+ expect(() =>
61
+ registry.register(makeAction({ name: "-my-action" }) as any),
62
+ ).toThrow(/Invalid action name/);
63
+ });
64
+
65
+ test("throws for names with trailing hyphens", () => {
66
+ const registry = new ActionRegistry();
67
+ expect(() =>
68
+ registry.register(makeAction({ name: "my-action-" }) as any),
69
+ ).toThrow(/Invalid action name/);
70
+ });
71
+
72
+ test("throws for names with consecutive hyphens", () => {
73
+ const registry = new ActionRegistry();
74
+ expect(() =>
75
+ registry.register(makeAction({ name: "my--action" }) as any),
76
+ ).toThrow(/Invalid action name/);
77
+ });
78
+
79
+ test("throws for duplicate action names", () => {
80
+ const registry = new ActionRegistry();
81
+ registry.register(makeAction() as any);
82
+ expect(() => registry.register(makeAction() as any)).toThrow(
83
+ /already registered/,
84
+ );
85
+ });
86
+
87
+ test("throws when inputSchema is missing", () => {
88
+ const registry = new ActionRegistry();
89
+ const action = makeAction({ inputSchema: undefined });
90
+ expect(() => registry.register(action as any)).toThrow(/inputSchema/);
91
+ });
92
+
93
+ test("throws when outputSchema is missing", () => {
94
+ const registry = new ActionRegistry();
95
+ const action = makeAction({ outputSchema: undefined });
96
+ expect(() => registry.register(action as any)).toThrow(/outputSchema/);
97
+ });
98
+ });
99
+
100
+ describe("get()", () => {
101
+ test("returns the registered action by name", () => {
102
+ const registry = new ActionRegistry();
103
+ const action = makeAction();
104
+ registry.register(action as any);
105
+ expect(registry.get("my-action")).toBe(action);
106
+ });
107
+
108
+ test("returns undefined for an unknown name", () => {
109
+ const registry = new ActionRegistry();
110
+ expect(registry.get("unknown")).toBeUndefined();
111
+ });
112
+ });
113
+
114
+ describe("has()", () => {
115
+ test("returns true for a registered action", () => {
116
+ const registry = new ActionRegistry();
117
+ registry.register(makeAction() as any);
118
+ expect(registry.has("my-action")).toBe(true);
119
+ });
120
+
121
+ test("returns false for an unregistered name", () => {
122
+ const registry = new ActionRegistry();
123
+ expect(registry.has("missing")).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe("list()", () => {
128
+ test("returns empty array when no actions registered", () => {
129
+ const registry = new ActionRegistry();
130
+ expect(registry.list()).toEqual([]);
131
+ });
132
+
133
+ test("returns all registered actions", () => {
134
+ const registry = new ActionRegistry();
135
+ const a1 = makeAction({ name: "action-one" });
136
+ const a2 = makeAction({ name: "action-two" });
137
+ registry.register(a1 as any);
138
+ registry.register(a2 as any);
139
+ const list = registry.list();
140
+ expect(list).toHaveLength(2);
141
+ expect(list).toContain(a1);
142
+ expect(list).toContain(a2);
143
+ });
144
+ });
145
+
146
+ describe("listByCategory()", () => {
147
+ test("groups uncategorized actions under 'default'", () => {
148
+ const registry = new ActionRegistry();
149
+ registry.register(makeAction({ name: "action-a" }) as any);
150
+ const map = registry.listByCategory();
151
+ expect(map.has("default")).toBe(true);
152
+ expect(map.get("default")).toHaveLength(1);
153
+ });
154
+
155
+ test("groups actions by category string", () => {
156
+ const registry = new ActionRegistry();
157
+ registry.register(
158
+ makeAction({ name: "send-tx", category: "wallet" }) as any,
159
+ );
160
+ registry.register(
161
+ makeAction({ name: "get-balance", category: "wallet" }) as any,
162
+ );
163
+ registry.register(
164
+ makeAction({ name: "list-pools", category: "pool" }) as any,
165
+ );
166
+ const map = registry.listByCategory();
167
+ expect(map.get("wallet")).toHaveLength(2);
168
+ expect(map.get("pool")).toHaveLength(1);
169
+ });
170
+
171
+ test("mixes categorized and uncategorized actions", () => {
172
+ const registry = new ActionRegistry();
173
+ registry.register(
174
+ makeAction({ name: "action-cat", category: "info" }) as any,
175
+ );
176
+ registry.register(makeAction({ name: "action-nocat" }) as any);
177
+ const map = registry.listByCategory();
178
+ expect(map.get("info")).toHaveLength(1);
179
+ expect(map.get("default")).toHaveLength(1);
180
+ });
181
+
182
+ test("returns an empty map when no actions registered", () => {
183
+ const registry = new ActionRegistry();
184
+ expect(registry.listByCategory().size).toBe(0);
185
+ });
186
+ });
187
+ });
@@ -0,0 +1,324 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Type } from "@sinclair/typebox";
3
+ import {
4
+ executeAction,
5
+ detectMode,
6
+ parseCliArgs,
7
+ ActionError,
8
+ } from "../actions/index.js";
9
+ import type { IActionContext } from "../actions/index.js";
10
+
11
+ // Lightweight stub context – the runner only forwards it to action.execute
12
+ const fakeContext = {} as IActionContext<any>;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // executeAction
16
+ // ---------------------------------------------------------------------------
17
+
18
+ describe("executeAction", () => {
19
+ const echoAction = {
20
+ name: "echo",
21
+ description: "Returns its input as output",
22
+ inputSchema: Type.Object({ message: Type.String() }),
23
+ outputSchema: Type.Object({ message: Type.String() }),
24
+ execute: async (input: { message: string }) => ({ message: input.message }),
25
+ };
26
+
27
+ test("returns success result with data on valid input", async () => {
28
+ const result = await executeAction(
29
+ echoAction,
30
+ { message: "hello" },
31
+ fakeContext,
32
+ );
33
+ expect(result.success).toBe(true);
34
+ if (result.success) {
35
+ expect(result.data).toEqual({ message: "hello" });
36
+ }
37
+ });
38
+
39
+ test("returns failure result on invalid input", async () => {
40
+ const result = await executeAction(echoAction, { message: 42 }, fakeContext);
41
+ expect(result.success).toBe(false);
42
+ if (!result.success) {
43
+ expect(result.error.code).toBe("VALIDATION_ERROR");
44
+ expect(result.error.details).toBeDefined();
45
+ }
46
+ });
47
+
48
+ test("returns failure result when required field is missing", async () => {
49
+ const result = await executeAction(echoAction, {}, fakeContext);
50
+ expect(result.success).toBe(false);
51
+ if (!result.success) {
52
+ expect(result.error.code).toBe("VALIDATION_ERROR");
53
+ }
54
+ });
55
+
56
+ test("returns failure result when input is not an object", async () => {
57
+ const result = await executeAction(echoAction, "not-an-object", fakeContext);
58
+ expect(result.success).toBe(false);
59
+ if (!result.success) {
60
+ expect(result.error.code).toBe("VALIDATION_ERROR");
61
+ }
62
+ });
63
+
64
+ test("validation error includes per-field details", async () => {
65
+ const result = await executeAction(
66
+ echoAction,
67
+ { message: 99 },
68
+ fakeContext,
69
+ );
70
+ if (!result.success) {
71
+ const details = result.error.details as Array<{
72
+ path: string;
73
+ message: string;
74
+ }>;
75
+ expect(Array.isArray(details)).toBe(true);
76
+ expect(details.length).toBeGreaterThan(0);
77
+ expect(details[0]).toHaveProperty("path");
78
+ expect(details[0]).toHaveProperty("message");
79
+ }
80
+ });
81
+
82
+ test("wraps plain Error thrown by execute into EXECUTION_ERROR", async () => {
83
+ const failAction = {
84
+ ...echoAction,
85
+ execute: async () => {
86
+ throw new Error("something went wrong");
87
+ },
88
+ };
89
+ const result = await executeAction(
90
+ failAction,
91
+ { message: "x" },
92
+ fakeContext,
93
+ );
94
+ expect(result.success).toBe(false);
95
+ if (!result.success) {
96
+ expect(result.error.code).toBe("EXECUTION_ERROR");
97
+ expect(result.error.message).toBe("something went wrong");
98
+ }
99
+ });
100
+
101
+ test("re-uses ActionError thrown by execute without re-wrapping", async () => {
102
+ const myError = new ActionError("custom error", "CUSTOM_CODE", {
103
+ hint: "test",
104
+ });
105
+ const failAction = {
106
+ ...echoAction,
107
+ execute: async () => {
108
+ throw myError;
109
+ },
110
+ };
111
+ const result = await executeAction(
112
+ failAction,
113
+ { message: "x" },
114
+ fakeContext,
115
+ );
116
+ expect(result.success).toBe(false);
117
+ if (!result.success) {
118
+ expect(result.error).toBe(myError);
119
+ expect(result.error.code).toBe("CUSTOM_CODE");
120
+ }
121
+ });
122
+
123
+ test("decodes valid input and passes it to execute", async () => {
124
+ let receivedInput: any;
125
+ const actionWithOptional = {
126
+ name: "optional-test",
127
+ description: "Tests optional fields",
128
+ inputSchema: Type.Object({
129
+ name: Type.String(),
130
+ count: Type.Optional(Type.Number()),
131
+ }),
132
+ outputSchema: Type.Object({}),
133
+ execute: async (input: any) => {
134
+ receivedInput = input;
135
+ return {};
136
+ },
137
+ };
138
+ const result = await executeAction(
139
+ actionWithOptional,
140
+ { name: "alice", count: 5 },
141
+ fakeContext,
142
+ );
143
+ expect(result.success).toBe(true);
144
+ expect(receivedInput.name).toBe("alice");
145
+ expect(receivedInput.count).toBe(5);
146
+ });
147
+
148
+ test("passes context to execute function", async () => {
149
+ let receivedContext: any;
150
+ const ctxAction = {
151
+ ...echoAction,
152
+ execute: async (input: any, ctx: any) => {
153
+ receivedContext = ctx;
154
+ return { message: "ok" };
155
+ },
156
+ };
157
+ const ctx = { sprinkle: "stub", settings: {} } as any;
158
+ await executeAction(ctxAction, { message: "test" }, ctx);
159
+ expect(receivedContext).toBe(ctx);
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // detectMode
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe("detectMode", () => {
168
+ test("returns 'tui' for empty argv", () => {
169
+ expect(detectMode([])).toBe("tui");
170
+ });
171
+
172
+ test("returns 'tui' for --interactive flag", () => {
173
+ expect(detectMode(["--interactive"])).toBe("tui");
174
+ });
175
+
176
+ test("returns 'tui' for --interactive alongside other flags", () => {
177
+ expect(detectMode(["--verbose", "--interactive"])).toBe("tui");
178
+ });
179
+
180
+ test("returns 'mcp' for --mcp flag", () => {
181
+ expect(detectMode(["--mcp"])).toBe("mcp");
182
+ });
183
+
184
+ test("returns 'mcp' when --mcp appears with other flags", () => {
185
+ expect(detectMode(["--verbose", "--mcp"])).toBe("mcp");
186
+ });
187
+
188
+ test("returns 'help' for --help flag only", () => {
189
+ expect(detectMode(["--help"])).toBe("help");
190
+ });
191
+
192
+ test("returns 'help' for -h shorthand only", () => {
193
+ expect(detectMode(["-h"])).toBe("help");
194
+ });
195
+
196
+ test("returns 'cli' when a positional arg is present", () => {
197
+ expect(detectMode(["get-balance"])).toBe("cli");
198
+ });
199
+
200
+ test("returns 'cli' when positional arg is mixed with flags", () => {
201
+ expect(detectMode(["--verbose", "send-tx", "--amount", "100"])).toBe("cli");
202
+ });
203
+
204
+ test("returns 'tui' for flag-only invocation that is not mcp/help/interactive", () => {
205
+ expect(detectMode(["--verbose", "--debug"])).toBe("tui");
206
+ });
207
+ });
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // parseCliArgs
211
+ // ---------------------------------------------------------------------------
212
+
213
+ describe("parseCliArgs", () => {
214
+ test("extracts action name from first positional arg", () => {
215
+ const { actionName } = parseCliArgs(["get-balance"]);
216
+ expect(actionName).toBe("get-balance");
217
+ });
218
+
219
+ test("returns empty args when no flags follow the action name", () => {
220
+ const { args } = parseCliArgs(["get-balance"]);
221
+ expect(args).toEqual({});
222
+ });
223
+
224
+ test("parses --flag value pairs", () => {
225
+ const { args } = parseCliArgs(["send", "--amount", "100"]);
226
+ expect(args["amount"]).toBe("100");
227
+ });
228
+
229
+ test("parses --flag=value syntax", () => {
230
+ const { args } = parseCliArgs(["send", "--amount=100"]);
231
+ expect(args["amount"]).toBe("100");
232
+ });
233
+
234
+ test("parses --no-flag as boolean false", () => {
235
+ const { args } = parseCliArgs(["send", "--no-confirm"]);
236
+ expect(args["confirm"]).toBe(false);
237
+ });
238
+
239
+ test("treats lone --flag with no value as boolean true", () => {
240
+ const { args } = parseCliArgs(["send", "--dry-run"]);
241
+ expect(args["dry-run"]).toBe(true);
242
+ });
243
+
244
+ test("parses JSON object value", () => {
245
+ const { args } = parseCliArgs([
246
+ "send",
247
+ "--input",
248
+ '{"key":"value","count":3}',
249
+ ]);
250
+ expect(args["input"]).toEqual({ key: "value", count: 3 });
251
+ });
252
+
253
+ test("parses JSON array value", () => {
254
+ const { args } = parseCliArgs(["send", "--ids", '["a","b","c"]']);
255
+ expect(args["ids"]).toEqual(["a", "b", "c"]);
256
+ });
257
+
258
+ test("falls back to string when JSON parse fails", () => {
259
+ const { args } = parseCliArgs(["send", "--data", "{bad json"]);
260
+ expect(args["data"]).toBe("{bad json");
261
+ });
262
+
263
+ test("parses multiple flags", () => {
264
+ const { args } = parseCliArgs([
265
+ "transfer",
266
+ "--from",
267
+ "addr1",
268
+ "--to",
269
+ "addr2",
270
+ "--amount",
271
+ "50",
272
+ ]);
273
+ expect(args["from"]).toBe("addr1");
274
+ expect(args["to"]).toBe("addr2");
275
+ expect(args["amount"]).toBe("50");
276
+ });
277
+
278
+ test("throws when all args start with dashes (no positional)", () => {
279
+ expect(() => parseCliArgs(["--verbose", "--debug"])).toThrow(
280
+ /No action name provided/,
281
+ );
282
+ });
283
+
284
+ test("throws when argv is empty", () => {
285
+ expect(() => parseCliArgs([])).toThrow(/No action name provided/);
286
+ });
287
+
288
+ test("action name is not included in args", () => {
289
+ const { args } = parseCliArgs(["my-action", "--key", "val"]);
290
+ expect(args).not.toHaveProperty("my-action");
291
+ });
292
+ });
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // ActionError
296
+ // ---------------------------------------------------------------------------
297
+
298
+ describe("ActionError", () => {
299
+ test("sets name to 'ActionError'", () => {
300
+ const err = new ActionError("oops", "CODE");
301
+ expect(err.name).toBe("ActionError");
302
+ });
303
+
304
+ test("extends Error", () => {
305
+ const err = new ActionError("oops", "CODE");
306
+ expect(err instanceof Error).toBe(true);
307
+ });
308
+
309
+ test("stores code", () => {
310
+ const err = new ActionError("oops", "MY_CODE");
311
+ expect(err.code).toBe("MY_CODE");
312
+ });
313
+
314
+ test("stores optional details", () => {
315
+ const details = { field: "amount" };
316
+ const err = new ActionError("oops", "CODE", details);
317
+ expect(err.details).toBe(details);
318
+ });
319
+
320
+ test("details is undefined when not provided", () => {
321
+ const err = new ActionError("oops", "CODE");
322
+ expect(err.details).toBeUndefined();
323
+ });
324
+ });