@sundaeswap/sprinkles 0.6.1 → 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 (153) 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__/mcp-adapter.test.js +713 -0
  12. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  13. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  14. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  15. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  16. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  17. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  18. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  19. package/dist/cjs/Sprinkle/actions/builtin/index.js +117 -0
  20. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  21. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  22. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  23. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  29. package/dist/cjs/Sprinkle/actions/cli-adapter.js +372 -0
  30. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  31. package/dist/cjs/Sprinkle/actions/index.js +127 -0
  32. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  33. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +415 -0
  34. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  35. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  36. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  37. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  38. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  39. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  40. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  41. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  42. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  43. package/dist/cjs/Sprinkle/index.js +412 -1
  44. package/dist/cjs/Sprinkle/index.js.map +1 -1
  45. package/dist/cjs/Sprinkle/prompts.js +12 -7
  46. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  47. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  48. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  49. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  50. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  51. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  52. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  53. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  54. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  55. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  56. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  57. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +720 -0
  58. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  59. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +712 -0
  60. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  61. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  62. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  63. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  64. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  65. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  66. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  67. package/dist/esm/Sprinkle/actions/builtin/index.js +32 -0
  68. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  69. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  70. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  71. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  72. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  73. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  74. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  75. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  76. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  77. package/dist/esm/Sprinkle/actions/cli-adapter.js +361 -0
  78. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  79. package/dist/esm/Sprinkle/actions/index.js +12 -0
  80. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  81. package/dist/esm/Sprinkle/actions/mcp-adapter.js +407 -0
  82. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  83. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  84. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  85. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  86. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  87. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  88. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  89. package/dist/esm/Sprinkle/actions/types.js +61 -0
  90. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  91. package/dist/esm/Sprinkle/index.js +260 -1
  92. package/dist/esm/Sprinkle/index.js.map +1 -1
  93. package/dist/esm/Sprinkle/prompts.js +12 -7
  94. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  95. package/dist/esm/Sprinkle/type-guards.js +3 -0
  96. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  97. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  98. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  99. package/dist/types/Sprinkle/actions/builtin/index.d.ts +26 -0
  100. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  101. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  102. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  103. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  104. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  105. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  106. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  107. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  108. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  109. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  110. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  111. package/dist/types/Sprinkle/actions/index.d.ts +12 -0
  112. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  113. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +92 -0
  114. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  115. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  116. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  117. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  118. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  119. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  120. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  121. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  122. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  123. package/dist/types/Sprinkle/index.d.ts +81 -1
  124. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  125. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  126. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  127. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  128. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  129. package/package.json +9 -2
  130. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  131. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  132. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  133. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  134. package/src/Sprinkle/__tests__/cli-adapter.test.ts +715 -0
  135. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +718 -0
  136. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  137. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  138. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  139. package/src/Sprinkle/actions/builtin/index.ts +86 -0
  140. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  141. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  142. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  143. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  144. package/src/Sprinkle/actions/cli-adapter.ts +430 -0
  145. package/src/Sprinkle/actions/index.ts +32 -0
  146. package/src/Sprinkle/actions/mcp-adapter.ts +463 -0
  147. package/src/Sprinkle/actions/registry.ts +97 -0
  148. package/src/Sprinkle/actions/runner.ts +200 -0
  149. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  150. package/src/Sprinkle/actions/types.ts +91 -0
  151. package/src/Sprinkle/index.ts +351 -0
  152. package/src/Sprinkle/prompts.ts +118 -72
  153. package/src/Sprinkle/type-guards.ts +9 -0
@@ -0,0 +1,1107 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { Type } from "@sinclair/typebox";
6
+ import { Sprinkle, getBuiltinActions } from "../index.js";
7
+ // Helper: write a minimal profile file into a tmp storage directory
8
+ function writeTestProfile(storagePath, id, name, settings, description) {
9
+ const dir = path.join(storagePath, "profiles");
10
+ fs.mkdirSync(dir, {
11
+ recursive: true
12
+ });
13
+ const meta = {
14
+ name,
15
+ description,
16
+ createdAt: new Date().toISOString(),
17
+ updatedAt: new Date().toISOString()
18
+ };
19
+ fs.writeFileSync(path.join(dir, `${id}.json`), JSON.stringify({
20
+ meta,
21
+ settings,
22
+ defaults: {}
23
+ }, null, 2), "utf-8");
24
+ }
25
+
26
+ // Settings schema with a sensitive field for masking tests
27
+ const TestSchema = Type.Object({
28
+ name: Type.String(),
29
+ token: Type.Optional(Type.String({
30
+ sensitive: true
31
+ }))
32
+ });
33
+
34
+ // Simple schema without sensitive fields
35
+ const SimpleSchema = Type.Object({
36
+ name: Type.String()
37
+ });
38
+
39
+ // Helper: create a Sprinkle with a loaded profile ready for action execution
40
+ async function makeSprinkle(storagePath, profileId = "default", settings = {
41
+ name: "tester"
42
+ }) {
43
+ const sprinkle = new Sprinkle(SimpleSchema, storagePath);
44
+ writeTestProfile(storagePath, profileId, "Default", settings);
45
+ await sprinkle.loadProfile(profileId);
46
+ return sprinkle;
47
+ }
48
+ describe("getBuiltinActions", () => {
49
+ test("returns an array of 14 actions", () => {
50
+ const actions = getBuiltinActions();
51
+ expect(actions).toHaveLength(14);
52
+ });
53
+ test("all actions have a category", () => {
54
+ const actions = getBuiltinActions();
55
+ const validCategories = ["sprinkles", "wallet", "transaction"];
56
+ for (const action of actions) {
57
+ expect(validCategories).toContain(action.category);
58
+ }
59
+ });
60
+ test("returns actions with expected names", () => {
61
+ const names = getBuiltinActions().map(a => a.name);
62
+ expect(names).toContain("list-profiles");
63
+ expect(names).toContain("get-profile");
64
+ expect(names).toContain("set-profile");
65
+ expect(names).toContain("create-profile");
66
+ expect(names).toContain("delete-profile");
67
+ expect(names).toContain("get-settings");
68
+ expect(names).toContain("update-settings");
69
+ });
70
+ test("all actions have inputSchema and outputSchema", () => {
71
+ const actions = getBuiltinActions();
72
+ for (const action of actions) {
73
+ expect(action.inputSchema).toBeDefined();
74
+ expect(action.outputSchema).toBeDefined();
75
+ }
76
+ });
77
+ test("built-in actions can be registered on a Sprinkle instance", () => {
78
+ const sprinkle = new Sprinkle(SimpleSchema, os.tmpdir());
79
+ const actions = getBuiltinActions();
80
+ for (const action of actions) {
81
+ sprinkle.registerAction(action);
82
+ }
83
+ expect(sprinkle.listActions()).toHaveLength(14);
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // list-profiles
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe("list-profiles action", () => {
92
+ let tmpDir;
93
+ beforeEach(() => {
94
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
95
+ });
96
+ afterEach(() => {
97
+ fs.rmSync(tmpDir, {
98
+ recursive: true,
99
+ force: true
100
+ });
101
+ });
102
+ test("returns empty profiles array when no profiles exist", async () => {
103
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
104
+ for (const action of getBuiltinActions()) {
105
+ sprinkle.registerAction(action);
106
+ }
107
+ const result = await sprinkle.runAction("list-profiles", {});
108
+ expect(result.success).toBe(true);
109
+ if (result.success) {
110
+ expect(result.data.profiles).toEqual([]);
111
+ }
112
+ });
113
+ test("returns all profiles with expected fields", async () => {
114
+ writeTestProfile(tmpDir, "alice", "Alice", {
115
+ name: "alice"
116
+ });
117
+ writeTestProfile(tmpDir, "bob", "Bob", {
118
+ name: "bob"
119
+ });
120
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
121
+ await sprinkle.loadProfile("alice");
122
+ for (const action of getBuiltinActions()) {
123
+ sprinkle.registerAction(action);
124
+ }
125
+ const result = await sprinkle.runAction("list-profiles", {});
126
+ expect(result.success).toBe(true);
127
+ if (result.success) {
128
+ expect(result.data.profiles).toHaveLength(2);
129
+ const names = result.data.profiles.map(p => p.name);
130
+ expect(names).toContain("Alice");
131
+ expect(names).toContain("Bob");
132
+ }
133
+ });
134
+ test("includes activeProfileId when a profile is loaded", async () => {
135
+ writeTestProfile(tmpDir, "default", "Default", {
136
+ name: "tester"
137
+ });
138
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
139
+ await sprinkle.loadProfile("default");
140
+ for (const action of getBuiltinActions()) {
141
+ sprinkle.registerAction(action);
142
+ }
143
+ const result = await sprinkle.runAction("list-profiles", {});
144
+ expect(result.success).toBe(true);
145
+ if (result.success) {
146
+ expect(result.data.activeProfileId).toBe("default");
147
+ }
148
+ });
149
+ test("activeProfileId is undefined when no profile is loaded", async () => {
150
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
151
+ for (const action of getBuiltinActions()) {
152
+ sprinkle.registerAction(action);
153
+ }
154
+ const result = await sprinkle.runAction("list-profiles", {});
155
+ expect(result.success).toBe(true);
156
+ if (result.success) {
157
+ expect(result.data.activeProfileId).toBeUndefined();
158
+ }
159
+ });
160
+ test("each profile entry contains id, name, createdAt, updatedAt", async () => {
161
+ writeTestProfile(tmpDir, "alpha", "Alpha Profile", {
162
+ name: "alpha"
163
+ }, "A profile");
164
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
165
+ await sprinkle.loadProfile("alpha");
166
+ for (const action of getBuiltinActions()) {
167
+ sprinkle.registerAction(action);
168
+ }
169
+ const result = await sprinkle.runAction("list-profiles", {});
170
+ expect(result.success).toBe(true);
171
+ if (result.success) {
172
+ const profile = result.data.profiles[0];
173
+ expect(profile.id).toBe("alpha");
174
+ expect(profile.name).toBe("Alpha Profile");
175
+ expect(profile.description).toBe("A profile");
176
+ expect(profile.createdAt).toBeDefined();
177
+ expect(profile.updatedAt).toBeDefined();
178
+ }
179
+ });
180
+ });
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // get-profile
184
+ // ---------------------------------------------------------------------------
185
+
186
+ describe("get-profile action", () => {
187
+ let tmpDir;
188
+ beforeEach(() => {
189
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
190
+ });
191
+ afterEach(() => {
192
+ fs.rmSync(tmpDir, {
193
+ recursive: true,
194
+ force: true
195
+ });
196
+ });
197
+ test("returns profile metadata and settings for a valid id", async () => {
198
+ writeTestProfile(tmpDir, "myprofile", "My Profile", {
199
+ name: "hello"
200
+ });
201
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
202
+ await sprinkle.loadProfile("myprofile");
203
+ for (const action of getBuiltinActions()) {
204
+ sprinkle.registerAction(action);
205
+ }
206
+ const result = await sprinkle.runAction("get-profile", {
207
+ id: "myprofile"
208
+ });
209
+ expect(result.success).toBe(true);
210
+ if (result.success) {
211
+ expect(result.data.id).toBe("myprofile");
212
+ expect(result.data.name).toBe("My Profile");
213
+ expect(result.data.settings).toBeDefined();
214
+ }
215
+ });
216
+ test("returns PROFILE_NOT_FOUND for unknown profile id", async () => {
217
+ writeTestProfile(tmpDir, "existing", "Existing", {
218
+ name: "x"
219
+ });
220
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
221
+ await sprinkle.loadProfile("existing");
222
+ for (const action of getBuiltinActions()) {
223
+ sprinkle.registerAction(action);
224
+ }
225
+ const result = await sprinkle.runAction("get-profile", {
226
+ id: "no-such-profile"
227
+ });
228
+ expect(result.success).toBe(false);
229
+ if (!result.success) {
230
+ expect(result.error.code).toBe("PROFILE_NOT_FOUND");
231
+ }
232
+ });
233
+ test("settings are returned when includeSensitive is not specified (defaults to masked)", async () => {
234
+ writeTestProfile(tmpDir, "prof1", "Prof1", {
235
+ name: "value"
236
+ });
237
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
238
+ await sprinkle.loadProfile("prof1");
239
+ for (const action of getBuiltinActions()) {
240
+ sprinkle.registerAction(action);
241
+ }
242
+ const result = await sprinkle.runAction("get-profile", {
243
+ id: "prof1"
244
+ });
245
+ expect(result.success).toBe(true);
246
+ if (result.success) {
247
+ expect(result.data.settings).toBeDefined();
248
+ }
249
+ });
250
+ test("returns raw settings when includeSensitive is true", async () => {
251
+ writeTestProfile(tmpDir, "prof2", "Prof2", {
252
+ name: "raw-value"
253
+ });
254
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
255
+ await sprinkle.loadProfile("prof2");
256
+ for (const action of getBuiltinActions()) {
257
+ sprinkle.registerAction(action);
258
+ }
259
+ const result = await sprinkle.runAction("get-profile", {
260
+ id: "prof2",
261
+ includeSensitive: true
262
+ });
263
+ expect(result.success).toBe(true);
264
+ if (result.success) {
265
+ expect(result.data.settings.name).toBe("raw-value");
266
+ }
267
+ });
268
+ test("error details include the requested id", async () => {
269
+ writeTestProfile(tmpDir, "existing", "Existing", {
270
+ name: "x"
271
+ });
272
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
273
+ await sprinkle.loadProfile("existing");
274
+ for (const action of getBuiltinActions()) {
275
+ sprinkle.registerAction(action);
276
+ }
277
+ const result = await sprinkle.runAction("get-profile", {
278
+ id: "ghost"
279
+ });
280
+ expect(result.success).toBe(false);
281
+ if (!result.success) {
282
+ expect(result.error.details).toMatchObject({
283
+ id: "ghost"
284
+ });
285
+ }
286
+ });
287
+ test("returns INVALID_PROFILE_ID for path traversal attempts", async () => {
288
+ writeTestProfile(tmpDir, "existing", "Existing", {
289
+ name: "x"
290
+ });
291
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
292
+ await sprinkle.loadProfile("existing");
293
+ for (const action of getBuiltinActions()) {
294
+ sprinkle.registerAction(action);
295
+ }
296
+ const result = await sprinkle.runAction("get-profile", {
297
+ id: "../etc/passwd"
298
+ });
299
+ expect(result.success).toBe(false);
300
+ if (!result.success) {
301
+ expect(result.error.code).toBe("INVALID_PROFILE_ID");
302
+ }
303
+ });
304
+ test("masks sensitive fields by default with TestSchema", async () => {
305
+ // Write profile with a sensitive token field
306
+ writeTestProfile(tmpDir, "sensitive-test", "Sensitive Test", {
307
+ name: "user",
308
+ token: "secret-api-key-12345"
309
+ });
310
+ const sprinkle = new Sprinkle(TestSchema, tmpDir);
311
+ await sprinkle.loadProfile("sensitive-test");
312
+ for (const action of getBuiltinActions()) {
313
+ sprinkle.registerAction(action);
314
+ }
315
+ const result = await sprinkle.runAction("get-profile", {
316
+ id: "sensitive-test"
317
+ });
318
+ expect(result.success).toBe(true);
319
+ if (result.success) {
320
+ // Token should be masked
321
+ expect(result.data.settings.token).toBe("********");
322
+ // Name should not be masked
323
+ expect(result.data.settings.name).toBe("user");
324
+ }
325
+ });
326
+ test("returns raw sensitive fields when includeSensitive is true with TestSchema", async () => {
327
+ writeTestProfile(tmpDir, "sensitive-raw", "Sensitive Raw", {
328
+ name: "user",
329
+ token: "my-secret-token"
330
+ });
331
+ const sprinkle = new Sprinkle(TestSchema, tmpDir);
332
+ await sprinkle.loadProfile("sensitive-raw");
333
+ for (const action of getBuiltinActions()) {
334
+ sprinkle.registerAction(action);
335
+ }
336
+ const result = await sprinkle.runAction("get-profile", {
337
+ id: "sensitive-raw",
338
+ includeSensitive: true
339
+ });
340
+ expect(result.success).toBe(true);
341
+ if (result.success) {
342
+ // Token should NOT be masked
343
+ expect(result.data.settings.token).toBe("my-secret-token");
344
+ expect(result.data.settings.name).toBe("user");
345
+ }
346
+ });
347
+ });
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // set-profile
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe("set-profile action", () => {
354
+ let tmpDir;
355
+ beforeEach(() => {
356
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
357
+ });
358
+ afterEach(() => {
359
+ fs.rmSync(tmpDir, {
360
+ recursive: true,
361
+ force: true
362
+ });
363
+ });
364
+ test("switches active profile and returns id and name", async () => {
365
+ writeTestProfile(tmpDir, "alice", "Alice", {
366
+ name: "alice"
367
+ });
368
+ writeTestProfile(tmpDir, "bob", "Bob", {
369
+ name: "bob"
370
+ });
371
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
372
+ await sprinkle.loadProfile("alice");
373
+ for (const action of getBuiltinActions()) {
374
+ sprinkle.registerAction(action);
375
+ }
376
+ const result = await sprinkle.runAction("set-profile", {
377
+ id: "bob"
378
+ });
379
+ expect(result.success).toBe(true);
380
+ if (result.success) {
381
+ expect(result.data.id).toBe("bob");
382
+ expect(result.data.name).toBe("Bob");
383
+ }
384
+ });
385
+ test("active profile is actually changed after set-profile", async () => {
386
+ writeTestProfile(tmpDir, "alice", "Alice", {
387
+ name: "alice"
388
+ });
389
+ writeTestProfile(tmpDir, "bob", "Bob", {
390
+ name: "bob"
391
+ });
392
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
393
+ await sprinkle.loadProfile("alice");
394
+ for (const action of getBuiltinActions()) {
395
+ sprinkle.registerAction(action);
396
+ }
397
+ await sprinkle.runAction("set-profile", {
398
+ id: "bob"
399
+ });
400
+ expect(sprinkle.profileId).toBe("bob");
401
+ });
402
+ test("returns PROFILE_NOT_FOUND when switching to unknown profile", async () => {
403
+ writeTestProfile(tmpDir, "alice", "Alice", {
404
+ name: "alice"
405
+ });
406
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
407
+ await sprinkle.loadProfile("alice");
408
+ for (const action of getBuiltinActions()) {
409
+ sprinkle.registerAction(action);
410
+ }
411
+ const result = await sprinkle.runAction("set-profile", {
412
+ id: "nobody"
413
+ });
414
+ expect(result.success).toBe(false);
415
+ if (!result.success) {
416
+ expect(result.error.code).toBe("PROFILE_NOT_FOUND");
417
+ }
418
+ });
419
+ });
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // create-profile
423
+ // ---------------------------------------------------------------------------
424
+
425
+ describe("create-profile action", () => {
426
+ let tmpDir;
427
+ beforeEach(() => {
428
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
429
+ });
430
+ afterEach(() => {
431
+ fs.rmSync(tmpDir, {
432
+ recursive: true,
433
+ force: true
434
+ });
435
+ });
436
+ test("creates a new profile and returns id and name", async () => {
437
+ writeTestProfile(tmpDir, "default", "Default", {
438
+ name: "x"
439
+ });
440
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
441
+ await sprinkle.loadProfile("default");
442
+ for (const action of getBuiltinActions()) {
443
+ sprinkle.registerAction(action);
444
+ }
445
+ const result = await sprinkle.runAction("create-profile", {
446
+ name: "New Profile"
447
+ });
448
+ expect(result.success).toBe(true);
449
+ if (result.success) {
450
+ expect(result.data.name).toBe("New Profile");
451
+ expect(result.data.id).toBeDefined();
452
+ }
453
+ });
454
+ test("created profile file exists on disk", async () => {
455
+ writeTestProfile(tmpDir, "default", "Default", {
456
+ name: "x"
457
+ });
458
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
459
+ await sprinkle.loadProfile("default");
460
+ for (const action of getBuiltinActions()) {
461
+ sprinkle.registerAction(action);
462
+ }
463
+ const result = await sprinkle.runAction("create-profile", {
464
+ name: "Disk Profile"
465
+ });
466
+ expect(result.success).toBe(true);
467
+ if (result.success) {
468
+ const profilePath = path.join(tmpDir, "profiles", `${result.data.id}.json`);
469
+ expect(fs.existsSync(profilePath)).toBe(true);
470
+ }
471
+ });
472
+ test("create-profile does NOT switch the active profile", async () => {
473
+ writeTestProfile(tmpDir, "default", "Default", {
474
+ name: "x"
475
+ });
476
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
477
+ await sprinkle.loadProfile("default");
478
+ for (const action of getBuiltinActions()) {
479
+ sprinkle.registerAction(action);
480
+ }
481
+ await sprinkle.runAction("create-profile", {
482
+ name: "Another Profile"
483
+ });
484
+ expect(sprinkle.profileId).toBe("default");
485
+ });
486
+ test("create-profile with optional description stores it", async () => {
487
+ writeTestProfile(tmpDir, "default", "Default", {
488
+ name: "x"
489
+ });
490
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
491
+ await sprinkle.loadProfile("default");
492
+ for (const action of getBuiltinActions()) {
493
+ sprinkle.registerAction(action);
494
+ }
495
+ const result = await sprinkle.runAction("create-profile", {
496
+ name: "Described Profile",
497
+ description: "A detailed description"
498
+ });
499
+ expect(result.success).toBe(true);
500
+ if (result.success) {
501
+ const profilePath = path.join(tmpDir, "profiles", `${result.data.id}.json`);
502
+ const content = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
503
+ expect(content.meta.description).toBe("A detailed description");
504
+ }
505
+ });
506
+ test("returns DUPLICATE_PROFILE error when name already exists", async () => {
507
+ writeTestProfile(tmpDir, "default", "Default", {
508
+ name: "x"
509
+ });
510
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
511
+ await sprinkle.loadProfile("default");
512
+ for (const action of getBuiltinActions()) {
513
+ sprinkle.registerAction(action);
514
+ }
515
+ const result = await sprinkle.runAction("create-profile", {
516
+ name: "Default"
517
+ });
518
+ expect(result.success).toBe(false);
519
+ if (!result.success) {
520
+ expect(result.error.code).toBe("DUPLICATE_PROFILE");
521
+ }
522
+ });
523
+ test("duplicate name check is case-insensitive", async () => {
524
+ writeTestProfile(tmpDir, "default", "Default", {
525
+ name: "x"
526
+ });
527
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
528
+ await sprinkle.loadProfile("default");
529
+ for (const action of getBuiltinActions()) {
530
+ sprinkle.registerAction(action);
531
+ }
532
+ const result = await sprinkle.runAction("create-profile", {
533
+ name: "DEFAULT"
534
+ });
535
+ expect(result.success).toBe(false);
536
+ if (!result.success) {
537
+ expect(result.error.code).toBe("DUPLICATE_PROFILE");
538
+ }
539
+ });
540
+ test("create-profile with initial settings writes them to disk", async () => {
541
+ writeTestProfile(tmpDir, "default", "Default", {
542
+ name: "x"
543
+ });
544
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
545
+ await sprinkle.loadProfile("default");
546
+ for (const action of getBuiltinActions()) {
547
+ sprinkle.registerAction(action);
548
+ }
549
+ const result = await sprinkle.runAction("create-profile", {
550
+ name: "Preset Profile",
551
+ settings: {
552
+ name: "preset-name"
553
+ }
554
+ });
555
+ expect(result.success).toBe(true);
556
+ if (result.success) {
557
+ const profilePath = path.join(tmpDir, "profiles", `${result.data.id}.json`);
558
+ const content = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
559
+ expect(content.settings.name).toBe("preset-name");
560
+ }
561
+ });
562
+ });
563
+
564
+ // ---------------------------------------------------------------------------
565
+ // delete-profile
566
+ // ---------------------------------------------------------------------------
567
+
568
+ describe("delete-profile action", () => {
569
+ let tmpDir;
570
+ beforeEach(() => {
571
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
572
+ });
573
+ afterEach(() => {
574
+ fs.rmSync(tmpDir, {
575
+ recursive: true,
576
+ force: true
577
+ });
578
+ });
579
+ test("deletes a profile and returns deleted:true with id", async () => {
580
+ writeTestProfile(tmpDir, "alice", "Alice", {
581
+ name: "alice"
582
+ });
583
+ writeTestProfile(tmpDir, "bob", "Bob", {
584
+ name: "bob"
585
+ });
586
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
587
+ await sprinkle.loadProfile("alice");
588
+ for (const action of getBuiltinActions()) {
589
+ sprinkle.registerAction(action);
590
+ }
591
+ const result = await sprinkle.runAction("delete-profile", {
592
+ id: "bob"
593
+ });
594
+ expect(result.success).toBe(true);
595
+ if (result.success) {
596
+ expect(result.data.deleted).toBe(true);
597
+ expect(result.data.id).toBe("bob");
598
+ }
599
+ });
600
+ test("profile file is removed from disk after deletion", async () => {
601
+ writeTestProfile(tmpDir, "alice", "Alice", {
602
+ name: "alice"
603
+ });
604
+ writeTestProfile(tmpDir, "bob", "Bob", {
605
+ name: "bob"
606
+ });
607
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
608
+ await sprinkle.loadProfile("alice");
609
+ for (const action of getBuiltinActions()) {
610
+ sprinkle.registerAction(action);
611
+ }
612
+ await sprinkle.runAction("delete-profile", {
613
+ id: "bob"
614
+ });
615
+ expect(fs.existsSync(path.join(tmpDir, "profiles", "bob.json"))).toBe(false);
616
+ });
617
+ test("returns PROFILE_NOT_FOUND when deleting a non-existent profile", async () => {
618
+ writeTestProfile(tmpDir, "alice", "Alice", {
619
+ name: "alice"
620
+ });
621
+ writeTestProfile(tmpDir, "bob", "Bob", {
622
+ name: "bob"
623
+ });
624
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
625
+ await sprinkle.loadProfile("alice");
626
+ for (const action of getBuiltinActions()) {
627
+ sprinkle.registerAction(action);
628
+ }
629
+ const result = await sprinkle.runAction("delete-profile", {
630
+ id: "nobody"
631
+ });
632
+ expect(result.success).toBe(false);
633
+ if (!result.success) {
634
+ expect(result.error.code).toBe("PROFILE_NOT_FOUND");
635
+ }
636
+ });
637
+ test("returns CANNOT_DELETE_ONLY_PROFILE when only one profile exists", async () => {
638
+ writeTestProfile(tmpDir, "only", "Only", {
639
+ name: "x"
640
+ });
641
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
642
+ await sprinkle.loadProfile("only");
643
+ for (const action of getBuiltinActions()) {
644
+ sprinkle.registerAction(action);
645
+ }
646
+ const result = await sprinkle.runAction("delete-profile", {
647
+ id: "only"
648
+ });
649
+ expect(result.success).toBe(false);
650
+ if (!result.success) {
651
+ expect(result.error.code).toBe("CANNOT_DELETE_ONLY_PROFILE");
652
+ }
653
+ });
654
+ test("returns CANNOT_DELETE_ACTIVE_PROFILE when deleting the active profile", async () => {
655
+ writeTestProfile(tmpDir, "active", "Active", {
656
+ name: "x"
657
+ });
658
+ writeTestProfile(tmpDir, "other", "Other", {
659
+ name: "y"
660
+ });
661
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
662
+ await sprinkle.loadProfile("active");
663
+ for (const action of getBuiltinActions()) {
664
+ sprinkle.registerAction(action);
665
+ }
666
+ const result = await sprinkle.runAction("delete-profile", {
667
+ id: "active"
668
+ });
669
+ expect(result.success).toBe(false);
670
+ if (!result.success) {
671
+ expect(result.error.code).toBe("CANNOT_DELETE_ACTIVE_PROFILE");
672
+ }
673
+ });
674
+ });
675
+
676
+ // ---------------------------------------------------------------------------
677
+ // get-settings
678
+ // ---------------------------------------------------------------------------
679
+
680
+ describe("get-settings action", () => {
681
+ let tmpDir;
682
+ beforeEach(() => {
683
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
684
+ });
685
+ afterEach(() => {
686
+ fs.rmSync(tmpDir, {
687
+ recursive: true,
688
+ force: true
689
+ });
690
+ });
691
+ test("returns settings, profileId, and profileName", async () => {
692
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
693
+ name: "Alice"
694
+ });
695
+ for (const action of getBuiltinActions()) {
696
+ sprinkle.registerAction(action);
697
+ }
698
+ const result = await sprinkle.runAction("get-settings", {});
699
+ expect(result.success).toBe(true);
700
+ if (result.success) {
701
+ expect(result.data.profileId).toBe("default");
702
+ expect(result.data.profileName).toBe("Default");
703
+ expect(result.data.settings).toBeDefined();
704
+ }
705
+ });
706
+ test("returns masked settings by default", async () => {
707
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
708
+ name: "Alice"
709
+ });
710
+ for (const action of getBuiltinActions()) {
711
+ sprinkle.registerAction(action);
712
+ }
713
+ const result = await sprinkle.runAction("get-settings", {});
714
+ expect(result.success).toBe(true);
715
+ if (result.success) {
716
+ // For a schema without sensitive fields, settings pass through unchanged
717
+ expect(result.data.settings.name).toBe("Alice");
718
+ }
719
+ });
720
+ test("returns raw settings when includeSensitive is true", async () => {
721
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
722
+ name: "raw-user"
723
+ });
724
+ for (const action of getBuiltinActions()) {
725
+ sprinkle.registerAction(action);
726
+ }
727
+ const result = await sprinkle.runAction("get-settings", {
728
+ includeSensitive: true
729
+ });
730
+ expect(result.success).toBe(true);
731
+ if (result.success) {
732
+ expect(result.data.settings.name).toBe("raw-user");
733
+ }
734
+ });
735
+ test("returns same settings whether includeSensitive is false or not provided (no sensitive fields)", async () => {
736
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
737
+ name: "same-user"
738
+ });
739
+ for (const action of getBuiltinActions()) {
740
+ sprinkle.registerAction(action);
741
+ }
742
+ const [masked, raw] = await Promise.all([sprinkle.runAction("get-settings", {
743
+ includeSensitive: false
744
+ }), sprinkle.runAction("get-settings", {
745
+ includeSensitive: true
746
+ })]);
747
+ expect(masked.success).toBe(true);
748
+ expect(raw.success).toBe(true);
749
+ if (masked.success && raw.success) {
750
+ expect(masked.data.settings).toEqual(raw.data.settings);
751
+ }
752
+ });
753
+ test("settings match what was loaded from profile", async () => {
754
+ const sprinkle = await makeSprinkle(tmpDir, "prof", {
755
+ name: "expected-value"
756
+ });
757
+ for (const action of getBuiltinActions()) {
758
+ sprinkle.registerAction(action);
759
+ }
760
+ const result = await sprinkle.runAction("get-settings", {
761
+ includeSensitive: true
762
+ });
763
+ expect(result.success).toBe(true);
764
+ if (result.success) {
765
+ expect(result.data.settings.name).toBe("expected-value");
766
+ }
767
+ });
768
+ });
769
+
770
+ // ---------------------------------------------------------------------------
771
+ // update-settings
772
+ // ---------------------------------------------------------------------------
773
+
774
+ describe("update-settings action", () => {
775
+ let tmpDir;
776
+ beforeEach(() => {
777
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
778
+ });
779
+ afterEach(() => {
780
+ fs.rmSync(tmpDir, {
781
+ recursive: true,
782
+ force: true
783
+ });
784
+ });
785
+ test("updates settings and returns masked settings with profileId", async () => {
786
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
787
+ name: "old-name"
788
+ });
789
+ for (const action of getBuiltinActions()) {
790
+ sprinkle.registerAction(action);
791
+ }
792
+ const result = await sprinkle.runAction("update-settings", {
793
+ settings: {
794
+ name: "new-name"
795
+ }
796
+ });
797
+ expect(result.success).toBe(true);
798
+ if (result.success) {
799
+ expect(result.data.settings.name).toBe("new-name");
800
+ expect(result.data.profileId).toBe("default");
801
+ }
802
+ });
803
+ test("update persists settings in memory", async () => {
804
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
805
+ name: "before"
806
+ });
807
+ for (const action of getBuiltinActions()) {
808
+ sprinkle.registerAction(action);
809
+ }
810
+ await sprinkle.runAction("update-settings", {
811
+ settings: {
812
+ name: "after"
813
+ }
814
+ });
815
+ expect(sprinkle.settings.name).toBe("after");
816
+ });
817
+ test("update persists settings to disk", async () => {
818
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
819
+ name: "before"
820
+ });
821
+ for (const action of getBuiltinActions()) {
822
+ sprinkle.registerAction(action);
823
+ }
824
+ await sprinkle.runAction("update-settings", {
825
+ settings: {
826
+ name: "persisted"
827
+ }
828
+ });
829
+
830
+ // Read the saved file to verify persistence
831
+ const profilePath = path.join(tmpDir, "profiles", "default.json");
832
+ const content = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
833
+ expect(content.settings.name).toBe("persisted");
834
+ });
835
+ test("returns VALIDATION_ERROR for settings that violate schema", async () => {
836
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
837
+ name: "valid"
838
+ });
839
+ for (const action of getBuiltinActions()) {
840
+ sprinkle.registerAction(action);
841
+ }
842
+
843
+ // Removing required 'name' field should fail schema validation
844
+ const result = await sprinkle.runAction("update-settings", {
845
+ settings: {
846
+ name: 12345
847
+ } // name must be a string
848
+ });
849
+ expect(result.success).toBe(false);
850
+ if (!result.success) {
851
+ expect(result.error.code).toBe("VALIDATION_ERROR");
852
+ }
853
+ });
854
+ test("settings are not saved when validation fails", async () => {
855
+ const sprinkle = await makeSprinkle(tmpDir, "default", {
856
+ name: "original"
857
+ });
858
+ for (const action of getBuiltinActions()) {
859
+ sprinkle.registerAction(action);
860
+ }
861
+ await sprinkle.runAction("update-settings", {
862
+ settings: {
863
+ name: 99
864
+ } // invalid: not a string
865
+ });
866
+
867
+ // Settings in memory should remain unchanged
868
+ expect(sprinkle.settings.name).toBe("original");
869
+ });
870
+ test("shallow merges settings - existing keys not in update are preserved", async () => {
871
+ // Use a schema with two optional keys to test merge behaviour
872
+ const TwoKeySchema = Type.Object({
873
+ name: Type.String(),
874
+ label: Type.Optional(Type.String())
875
+ });
876
+ writeTestProfile(tmpDir, "twokey", "TwoKey", {
877
+ name: "base",
878
+ label: "keep-me"
879
+ });
880
+ const sprinkle = new Sprinkle(TwoKeySchema, tmpDir);
881
+ await sprinkle.loadProfile("twokey");
882
+ for (const action of getBuiltinActions()) {
883
+ sprinkle.registerAction(action);
884
+ }
885
+ const result = await sprinkle.runAction("update-settings", {
886
+ settings: {
887
+ name: "updated"
888
+ }
889
+ });
890
+ expect(result.success).toBe(true);
891
+ if (result.success) {
892
+ expect(result.data.settings.name).toBe("updated");
893
+ expect(result.data.settings.label).toBe("keep-me");
894
+ }
895
+ });
896
+ });
897
+
898
+ // ---------------------------------------------------------------------------
899
+ // Sprinkle.getProfileById direct method tests
900
+ // ---------------------------------------------------------------------------
901
+
902
+ describe("Sprinkle.getProfileById", () => {
903
+ let tmpDir;
904
+ beforeEach(() => {
905
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
906
+ });
907
+ afterEach(() => {
908
+ fs.rmSync(tmpDir, {
909
+ recursive: true,
910
+ force: true
911
+ });
912
+ });
913
+ test("returns profile entry when id exists", () => {
914
+ writeTestProfile(tmpDir, "myid", "My Name", {
915
+ name: "x"
916
+ });
917
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
918
+ const entry = sprinkle.getProfileById("myid");
919
+ expect(entry).toBeDefined();
920
+ expect(entry.id).toBe("myid");
921
+ expect(entry.meta.name).toBe("My Name");
922
+ });
923
+ test("returns undefined when id does not exist", () => {
924
+ writeTestProfile(tmpDir, "real", "Real", {
925
+ name: "x"
926
+ });
927
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
928
+ expect(sprinkle.getProfileById("ghost")).toBeUndefined();
929
+ });
930
+ test("returns undefined when no profiles exist", () => {
931
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
932
+ expect(sprinkle.getProfileById("anything")).toBeUndefined();
933
+ });
934
+ });
935
+
936
+ // ---------------------------------------------------------------------------
937
+ // Sprinkle.createProfileNonInteractive direct method tests
938
+ // ---------------------------------------------------------------------------
939
+
940
+ describe("Sprinkle.createProfileNonInteractive", () => {
941
+ let tmpDir;
942
+ beforeEach(() => {
943
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
944
+ });
945
+ afterEach(() => {
946
+ fs.rmSync(tmpDir, {
947
+ recursive: true,
948
+ force: true
949
+ });
950
+ });
951
+ test("creates a profile file and returns IProfileEntry", async () => {
952
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
953
+ const entry = await sprinkle.createProfileNonInteractive("Test Profile");
954
+ expect(entry.id).toBeDefined();
955
+ expect(entry.meta.name).toBe("Test Profile");
956
+ const profilePath = path.join(tmpDir, "profiles", `${entry.id}.json`);
957
+ expect(fs.existsSync(profilePath)).toBe(true);
958
+ });
959
+ test("sanitizes name to form a valid id", async () => {
960
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
961
+ const entry = await sprinkle.createProfileNonInteractive("My New Profile!");
962
+ // sanitized: "my-new-profile"
963
+ expect(entry.id).toBe("my-new-profile");
964
+ });
965
+ test("uses findAvailableId to avoid collisions", async () => {
966
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
967
+ // "My Profile" sanitizes to "my-profile". Pre-create that file so
968
+ // findAvailableId must fall back to "my-profile-2".
969
+ writeTestProfile(tmpDir, "my-profile", "My Profile First", {
970
+ name: "x"
971
+ });
972
+ const entry = await sprinkle.createProfileNonInteractive("My Profile");
973
+ // "my-profile" is taken, so the entry id should become "my-profile-2"
974
+ expect(entry.id).toBe("my-profile-2");
975
+ });
976
+ test("throws DUPLICATE_PROFILE for same name (case-insensitive)", async () => {
977
+ writeTestProfile(tmpDir, "existing", "Existing Profile", {
978
+ name: "x"
979
+ });
980
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
981
+ await expect(sprinkle.createProfileNonInteractive("existing profile")).rejects.toMatchObject({
982
+ code: "DUPLICATE_PROFILE"
983
+ });
984
+ });
985
+ test("stores optional description in meta", async () => {
986
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
987
+ const entry = await sprinkle.createProfileNonInteractive("Described", "My description");
988
+ expect(entry.meta.description).toBe("My description");
989
+ const profilePath = path.join(tmpDir, "profiles", `${entry.id}.json`);
990
+ const content = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
991
+ expect(content.meta.description).toBe("My description");
992
+ });
993
+ test("stores initial settings when provided", async () => {
994
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
995
+ const entry = await sprinkle.createProfileNonInteractive("With Settings", undefined, {
996
+ name: "preset"
997
+ });
998
+ const profilePath = path.join(tmpDir, "profiles", `${entry.id}.json`);
999
+ const content = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
1000
+ expect(content.settings.name).toBe("preset");
1001
+ });
1002
+ test("does not change the active profile", async () => {
1003
+ writeTestProfile(tmpDir, "active", "Active", {
1004
+ name: "x"
1005
+ });
1006
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
1007
+ await sprinkle.loadProfile("active");
1008
+ await sprinkle.createProfileNonInteractive("New One");
1009
+ expect(sprinkle.profileId).toBe("active");
1010
+ });
1011
+ });
1012
+
1013
+ // ---------------------------------------------------------------------------
1014
+ // Sprinkle.deleteProfileById direct method tests
1015
+ // ---------------------------------------------------------------------------
1016
+
1017
+ describe("Sprinkle.deleteProfileById", () => {
1018
+ let tmpDir;
1019
+ beforeEach(() => {
1020
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sprinkles-builtin-test-"));
1021
+ });
1022
+ afterEach(() => {
1023
+ fs.rmSync(tmpDir, {
1024
+ recursive: true,
1025
+ force: true
1026
+ });
1027
+ });
1028
+ test("removes the profile file from disk", () => {
1029
+ writeTestProfile(tmpDir, "keep", "Keep", {
1030
+ name: "x"
1031
+ });
1032
+ writeTestProfile(tmpDir, "remove", "Remove", {
1033
+ name: "y"
1034
+ });
1035
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
1036
+ sprinkle.profileId = "keep";
1037
+ sprinkle.profileMeta = {
1038
+ name: "Keep",
1039
+ createdAt: "",
1040
+ updatedAt: ""
1041
+ };
1042
+ sprinkle.deleteProfileById("remove");
1043
+ expect(fs.existsSync(path.join(tmpDir, "profiles", "remove.json"))).toBe(false);
1044
+ expect(fs.existsSync(path.join(tmpDir, "profiles", "keep.json"))).toBe(true);
1045
+ });
1046
+ test("throws PROFILE_NOT_FOUND for unknown id", () => {
1047
+ writeTestProfile(tmpDir, "alpha", "Alpha", {
1048
+ name: "x"
1049
+ });
1050
+ writeTestProfile(tmpDir, "beta", "Beta", {
1051
+ name: "y"
1052
+ });
1053
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
1054
+ sprinkle.profileId = "alpha";
1055
+ sprinkle.profileMeta = {
1056
+ name: "Alpha",
1057
+ createdAt: "",
1058
+ updatedAt: ""
1059
+ };
1060
+ expect(() => sprinkle.deleteProfileById("ghost")).toThrow();
1061
+ try {
1062
+ sprinkle.deleteProfileById("ghost");
1063
+ } catch (e) {
1064
+ expect(e.code).toBe("PROFILE_NOT_FOUND");
1065
+ }
1066
+ });
1067
+ test("throws CANNOT_DELETE_ONLY_PROFILE when only one profile exists", () => {
1068
+ writeTestProfile(tmpDir, "solo", "Solo", {
1069
+ name: "x"
1070
+ });
1071
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
1072
+ sprinkle.profileId = "solo";
1073
+ sprinkle.profileMeta = {
1074
+ name: "Solo",
1075
+ createdAt: "",
1076
+ updatedAt: ""
1077
+ };
1078
+ expect(() => sprinkle.deleteProfileById("solo")).toThrow();
1079
+ try {
1080
+ sprinkle.deleteProfileById("solo");
1081
+ } catch (e) {
1082
+ expect(e.code).toBe("CANNOT_DELETE_ONLY_PROFILE");
1083
+ }
1084
+ });
1085
+ test("throws CANNOT_DELETE_ACTIVE_PROFILE when deleting the active profile", () => {
1086
+ writeTestProfile(tmpDir, "active", "Active", {
1087
+ name: "x"
1088
+ });
1089
+ writeTestProfile(tmpDir, "other", "Other", {
1090
+ name: "y"
1091
+ });
1092
+ const sprinkle = new Sprinkle(SimpleSchema, tmpDir);
1093
+ sprinkle.profileId = "active";
1094
+ sprinkle.profileMeta = {
1095
+ name: "Active",
1096
+ createdAt: "",
1097
+ updatedAt: ""
1098
+ };
1099
+ expect(() => sprinkle.deleteProfileById("active")).toThrow();
1100
+ try {
1101
+ sprinkle.deleteProfileById("active");
1102
+ } catch (e) {
1103
+ expect(e.code).toBe("CANNOT_DELETE_ACTIVE_PROFILE");
1104
+ }
1105
+ });
1106
+ });
1107
+ //# sourceMappingURL=builtin-actions.test.js.map