@remix-gg/mcp 0.4.3

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 (139) hide show
  1. package/README.md +81 -0
  2. package/dist/client-helpers/index.d.ts +2 -0
  3. package/dist/client-helpers/index.d.ts.map +1 -0
  4. package/dist/client-helpers/index.js +2 -0
  5. package/dist/client-helpers/index.js.map +1 -0
  6. package/dist/config.d.ts +20 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +58 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/api-client.d.ts +4 -0
  11. package/dist/core/api-client.d.ts.map +1 -0
  12. package/dist/core/api-client.js +12 -0
  13. package/dist/core/api-client.js.map +1 -0
  14. package/dist/core/config.d.ts +6 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +19 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/index.d.ts +5 -0
  19. package/dist/core/index.d.ts.map +1 -0
  20. package/dist/core/index.js +4 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/core/skills.d.ts +22 -0
  23. package/dist/core/skills.d.ts.map +1 -0
  24. package/dist/core/skills.js +49 -0
  25. package/dist/core/skills.js.map +1 -0
  26. package/dist/core/tool-defs.d.ts +12 -0
  27. package/dist/core/tool-defs.d.ts.map +1 -0
  28. package/dist/core/tool-defs.js +356 -0
  29. package/dist/core/tool-defs.js.map +1 -0
  30. package/dist/core/tools/create-game.d.ts +9 -0
  31. package/dist/core/tools/create-game.d.ts.map +1 -0
  32. package/dist/core/tools/create-game.js +21 -0
  33. package/dist/core/tools/create-game.js.map +1 -0
  34. package/dist/core/tools/create-shop-item.d.ts +14 -0
  35. package/dist/core/tools/create-shop-item.d.ts.map +1 -0
  36. package/dist/core/tools/create-shop-item.js +78 -0
  37. package/dist/core/tools/create-shop-item.js.map +1 -0
  38. package/dist/core/tools/delete-shop-item.d.ts +9 -0
  39. package/dist/core/tools/delete-shop-item.d.ts.map +1 -0
  40. package/dist/core/tools/delete-shop-item.js +19 -0
  41. package/dist/core/tools/delete-shop-item.js.map +1 -0
  42. package/dist/core/tools/generate-image.d.ts +8 -0
  43. package/dist/core/tools/generate-image.d.ts.map +1 -0
  44. package/dist/core/tools/generate-image.js +32 -0
  45. package/dist/core/tools/generate-image.js.map +1 -0
  46. package/dist/core/tools/generate-sprite-sheet.d.ts +14 -0
  47. package/dist/core/tools/generate-sprite-sheet.d.ts.map +1 -0
  48. package/dist/core/tools/generate-sprite-sheet.js +29 -0
  49. package/dist/core/tools/generate-sprite-sheet.js.map +1 -0
  50. package/dist/core/tools/helpers.d.ts +60 -0
  51. package/dist/core/tools/helpers.d.ts.map +1 -0
  52. package/dist/core/tools/helpers.js +68 -0
  53. package/dist/core/tools/helpers.js.map +1 -0
  54. package/dist/core/tools/index.d.ts +13 -0
  55. package/dist/core/tools/index.d.ts.map +1 -0
  56. package/dist/core/tools/index.js +13 -0
  57. package/dist/core/tools/index.js.map +1 -0
  58. package/dist/core/tools/list-shop-items.d.ts +8 -0
  59. package/dist/core/tools/list-shop-items.d.ts.map +1 -0
  60. package/dist/core/tools/list-shop-items.js +17 -0
  61. package/dist/core/tools/list-shop-items.js.map +1 -0
  62. package/dist/core/tools/update-game.d.ts +11 -0
  63. package/dist/core/tools/update-game.d.ts.map +1 -0
  64. package/dist/core/tools/update-game.js +22 -0
  65. package/dist/core/tools/update-game.js.map +1 -0
  66. package/dist/core/tools/update-shop-item.d.ts +15 -0
  67. package/dist/core/tools/update-shop-item.d.ts.map +1 -0
  68. package/dist/core/tools/update-shop-item.js +24 -0
  69. package/dist/core/tools/update-shop-item.js.map +1 -0
  70. package/dist/core/tools/upload-game-asset.d.ts +8 -0
  71. package/dist/core/tools/upload-game-asset.d.ts.map +1 -0
  72. package/dist/core/tools/upload-game-asset.js +43 -0
  73. package/dist/core/tools/upload-game-asset.js.map +1 -0
  74. package/dist/core/tools/upload-version.d.ts +8 -0
  75. package/dist/core/tools/upload-version.d.ts.map +1 -0
  76. package/dist/core/tools/upload-version.js +20 -0
  77. package/dist/core/tools/upload-version.js.map +1 -0
  78. package/dist/core/tools/validate-game.d.ts +6 -0
  79. package/dist/core/tools/validate-game.d.ts.map +1 -0
  80. package/dist/core/tools/validate-game.js +41 -0
  81. package/dist/core/tools/validate-game.js.map +1 -0
  82. package/dist/core/tools.test.d.ts +2 -0
  83. package/dist/core/tools.test.d.ts.map +1 -0
  84. package/dist/core/tools.test.js +825 -0
  85. package/dist/core/tools.test.js.map +1 -0
  86. package/dist/generated/server-api.d.ts +3673 -0
  87. package/dist/generated/server-api.d.ts.map +1 -0
  88. package/dist/generated/server-api.js +2 -0
  89. package/dist/generated/server-api.js.map +1 -0
  90. package/dist/index.d.ts +3 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +365 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/server/create-server.d.ts +12 -0
  95. package/dist/server/create-server.d.ts.map +1 -0
  96. package/dist/server/create-server.js +29 -0
  97. package/dist/server/create-server.js.map +1 -0
  98. package/dist/server/create-server.test.d.ts +2 -0
  99. package/dist/server/create-server.test.d.ts.map +1 -0
  100. package/dist/server/create-server.test.js +37 -0
  101. package/dist/server/create-server.test.js.map +1 -0
  102. package/dist/server/index.d.ts +3 -0
  103. package/dist/server/index.d.ts.map +1 -0
  104. package/dist/server/index.js +8 -0
  105. package/dist/server/index.js.map +1 -0
  106. package/dist/server/lib/config.d.ts +2 -0
  107. package/dist/server/lib/config.d.ts.map +1 -0
  108. package/dist/server/lib/config.js +2 -0
  109. package/dist/server/lib/config.js.map +1 -0
  110. package/dist/server/resources/skills.d.ts +3 -0
  111. package/dist/server/resources/skills.d.ts.map +1 -0
  112. package/dist/server/resources/skills.js +60 -0
  113. package/dist/server/resources/skills.js.map +1 -0
  114. package/dist/server/resources/skills.test.d.ts +2 -0
  115. package/dist/server/resources/skills.test.d.ts.map +1 -0
  116. package/dist/server/resources/skills.test.js +44 -0
  117. package/dist/server/resources/skills.test.js.map +1 -0
  118. package/dist/server/tools/register.d.ts +3 -0
  119. package/dist/server/tools/register.d.ts.map +1 -0
  120. package/dist/server/tools/register.js +26 -0
  121. package/dist/server/tools/register.js.map +1 -0
  122. package/dist/server/tools/register.test.d.ts +2 -0
  123. package/dist/server/tools/register.test.d.ts.map +1 -0
  124. package/dist/server/tools/register.test.js +85 -0
  125. package/dist/server/tools/register.test.js.map +1 -0
  126. package/dist/types.d.ts +73 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +31 -0
  129. package/dist/types.js.map +1 -0
  130. package/package.json +38 -0
  131. package/skills/SKILL.md +82 -0
  132. package/skills/actions/open-game.md +18 -0
  133. package/skills/workflows/add-image-to-game.md +121 -0
  134. package/skills/workflows/add-sprite-to-game.md +127 -0
  135. package/skills/workflows/game-creation.md +124 -0
  136. package/skills/workflows/implement-multiplayer.md +355 -0
  137. package/skills/workflows/integrate-save-game.md +135 -0
  138. package/skills/workflows/manage-shop-items.md +246 -0
  139. package/skills/workflows/upload-game.md +74 -0
@@ -0,0 +1,825 @@
1
+ import { beforeEach, describe, expect, it, mock } from 'bun:test';
2
+ const mockReadFile = mock(async (..._args) => '');
3
+ const mockStat = mock(async () => ({ size: 1024 }));
4
+ const mockPost = mock(async (..._args) => ({
5
+ data: undefined,
6
+ error: undefined,
7
+ response: new Response(null, { status: 200 }),
8
+ }));
9
+ const mockGet = mock(async (..._args) => ({
10
+ data: undefined,
11
+ error: undefined,
12
+ response: new Response(null, { status: 200 }),
13
+ }));
14
+ const mockDelete = mock(async (..._args) => ({
15
+ data: undefined,
16
+ error: undefined,
17
+ response: new Response(null, { status: 200 }),
18
+ }));
19
+ const mockCreateServerApiClient = mock((..._args) => ({
20
+ DELETE: mockDelete,
21
+ GET: mockGet,
22
+ POST: mockPost,
23
+ }));
24
+ mock.module('node:fs/promises', () => ({
25
+ readFile: mockReadFile,
26
+ stat: mockStat,
27
+ }));
28
+ mock.module('./api-client.js', () => ({
29
+ createServerApiClient: mockCreateServerApiClient,
30
+ }));
31
+ const { createShopItem, createGame, deleteShopItem, updateGame, uploadVersion, generateImage, generateSpriteSheet, listShopItems, updateShopItem, uploadGameAsset, validateGame, } = await import('./tools/index.js');
32
+ describe('core tools', () => {
33
+ beforeEach(() => {
34
+ mockReadFile.mockReset();
35
+ mockStat.mockReset();
36
+ mockGet.mockReset();
37
+ mockDelete.mockReset();
38
+ mockPost.mockReset();
39
+ mockCreateServerApiClient.mockClear();
40
+ mockReadFile.mockResolvedValue('');
41
+ mockStat.mockResolvedValue({ size: 1024 });
42
+ mockGet.mockResolvedValue({
43
+ data: undefined,
44
+ error: undefined,
45
+ response: new Response(null, { status: 200 }),
46
+ });
47
+ mockDelete.mockResolvedValue({
48
+ data: undefined,
49
+ error: undefined,
50
+ response: new Response(null, { status: 200 }),
51
+ });
52
+ mockPost.mockResolvedValue({
53
+ data: undefined,
54
+ error: undefined,
55
+ response: new Response(null, { status: 200 }),
56
+ });
57
+ mockCreateServerApiClient.mockReturnValue({
58
+ DELETE: mockDelete,
59
+ GET: mockGet,
60
+ POST: mockPost,
61
+ });
62
+ });
63
+ describe('createGame()', () => {
64
+ it('creates a game via the API client and maps the response', async () => {
65
+ mockPost.mockResolvedValueOnce({
66
+ data: {
67
+ success: true,
68
+ data: {
69
+ game: {
70
+ id: 'game-1',
71
+ name: 'Test Game',
72
+ createdAt: '2026-03-03T00:00:00.000Z',
73
+ version: {
74
+ id: 'version-1',
75
+ createdAt: '2026-03-03T00:00:00.000Z',
76
+ },
77
+ },
78
+ },
79
+ },
80
+ error: undefined,
81
+ response: new Response(null, { status: 201 }),
82
+ });
83
+ const result = await createGame('Test Game', {
84
+ apiKey: 'key',
85
+ apiBaseUrl: 'https://api.remix.gg',
86
+ });
87
+ expect(mockCreateServerApiClient).toHaveBeenCalledWith({
88
+ apiKey: 'key',
89
+ apiBaseUrl: 'https://api.remix.gg',
90
+ });
91
+ expect(mockPost).toHaveBeenCalledWith('/v1/games', {
92
+ body: { name: 'Test Game' },
93
+ });
94
+ expect(result).toEqual({
95
+ gameId: 'game-1',
96
+ versionId: 'version-1',
97
+ name: 'Test Game',
98
+ createdAt: '2026-03-03T00:00:00.000Z',
99
+ });
100
+ });
101
+ it('throws a normalized API error when the request fails', async () => {
102
+ mockPost.mockResolvedValueOnce({
103
+ data: undefined,
104
+ error: { error: { message: 'Forbidden' } },
105
+ response: new Response(null, { status: 403, statusText: 'Forbidden' }),
106
+ });
107
+ await expect(createGame('Test Game')).rejects.toThrow('Remix API error (403): Forbidden');
108
+ });
109
+ });
110
+ describe('updateGame()', () => {
111
+ it('updates a game and maps the response', async () => {
112
+ mockPost.mockResolvedValueOnce({
113
+ data: {
114
+ success: true,
115
+ data: {
116
+ game: {
117
+ id: 'game-1',
118
+ name: 'Updated Name',
119
+ isMultiplayer: true,
120
+ },
121
+ },
122
+ },
123
+ error: undefined,
124
+ response: new Response(null, { status: 200 }),
125
+ });
126
+ const result = await updateGame('game-1', {
127
+ name: 'Updated Name',
128
+ isMultiplayer: true,
129
+ });
130
+ expect(mockPost).toHaveBeenCalledWith('/v1/games/{gameId}', {
131
+ params: { path: { gameId: 'game-1' } },
132
+ body: { name: 'Updated Name', isMultiplayer: true },
133
+ });
134
+ expect(result).toEqual({
135
+ gameId: 'game-1',
136
+ name: 'Updated Name',
137
+ isMultiplayer: true,
138
+ });
139
+ });
140
+ it('forwards only provided fields', async () => {
141
+ mockPost.mockResolvedValueOnce({
142
+ data: {
143
+ success: true,
144
+ data: {
145
+ game: {
146
+ id: 'game-1',
147
+ name: 'Existing Name',
148
+ isMultiplayer: false,
149
+ },
150
+ },
151
+ },
152
+ error: undefined,
153
+ response: new Response(null, { status: 200 }),
154
+ });
155
+ await updateGame('game-1', { isMultiplayer: false });
156
+ const call = mockPost.mock.calls[0];
157
+ expect(call[1].body).toEqual({ isMultiplayer: false });
158
+ expect(call[1].body).not.toHaveProperty('name');
159
+ });
160
+ });
161
+ describe('uploadVersion()', () => {
162
+ it('reads the file and uploads code to the expected endpoint', async () => {
163
+ mockReadFile.mockResolvedValueOnce('<html>game</html>');
164
+ mockPost.mockResolvedValueOnce({
165
+ data: {
166
+ success: true,
167
+ data: {
168
+ success: true,
169
+ gameId: 'game-1',
170
+ versionId: 'version-1',
171
+ },
172
+ },
173
+ error: undefined,
174
+ response: new Response(null, { status: 200 }),
175
+ });
176
+ const result = await uploadVersion('game-1', 'version-1', '/tmp/game.html');
177
+ expect(mockReadFile).toHaveBeenCalledWith('/tmp/game.html', 'utf-8');
178
+ expect(mockPost).toHaveBeenCalledWith('/v1/games/{gameId}/versions/{versionId}/code', {
179
+ params: { path: { gameId: 'game-1', versionId: 'version-1' } },
180
+ body: { code: '<html>game</html>' },
181
+ });
182
+ expect(result).toEqual({
183
+ gameId: 'game-1',
184
+ versionId: 'version-1',
185
+ threadId: undefined,
186
+ });
187
+ });
188
+ });
189
+ describe('validateGame()', () => {
190
+ it('returns no issues for valid Remix HTML', async () => {
191
+ mockReadFile.mockResolvedValueOnce(`<!DOCTYPE html>
192
+ <html>
193
+ <head>
194
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
195
+ <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
196
+ </head>
197
+ <body>
198
+ <script>
199
+ window.RemixSDK?.onToggleMute(() => {})
200
+ window.RemixSDK?.onPlayAgain(() => {})
201
+ window.RemixSDK?.singlePlayer.actions.gameOver({ score: 10 })
202
+ </script>
203
+ </body>
204
+ </html>`);
205
+ await expect(validateGame('/tmp/game.html')).resolves.toEqual({
206
+ valid: true,
207
+ issues: [],
208
+ });
209
+ });
210
+ it('accepts multiplayer.actions.gameOver as valid', async () => {
211
+ mockReadFile.mockResolvedValueOnce(`<!DOCTYPE html>
212
+ <html>
213
+ <head>
214
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
215
+ <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
216
+ </head>
217
+ <body>
218
+ <script>
219
+ window.RemixSDK?.onToggleMute(() => {})
220
+ window.RemixSDK?.onPlayAgain(() => {})
221
+ window.RemixSDK?.multiplayer.actions.gameOver({ scores: [] })
222
+ </script>
223
+ </body>
224
+ </html>`);
225
+ await expect(validateGame('/tmp/game.html')).resolves.toEqual({
226
+ valid: true,
227
+ issues: [],
228
+ });
229
+ });
230
+ it('accepts a versioned Remix SDK script tag', async () => {
231
+ mockReadFile.mockResolvedValueOnce(`<!DOCTYPE html>
232
+ <html>
233
+ <head>
234
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
235
+ <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@1.2.3/dist/index.min.js"></script>
236
+ </head>
237
+ <body>
238
+ <script>
239
+ window.RemixSDK?.onToggleMute(() => {})
240
+ window.RemixSDK?.onPlayAgain(() => {})
241
+ window.RemixSDK?.singlePlayer.actions.gameOver({ score: 10 })
242
+ </script>
243
+ </body>
244
+ </html>`);
245
+ await expect(validateGame('/tmp/game.html')).resolves.toEqual({
246
+ valid: true,
247
+ issues: [],
248
+ });
249
+ });
250
+ it('rejects both singlePlayer and multiplayer gameOver in the same game', async () => {
251
+ mockReadFile.mockResolvedValueOnce(`<!DOCTYPE html>
252
+ <html>
253
+ <head>
254
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
255
+ <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>
256
+ </head>
257
+ <body>
258
+ <script>
259
+ window.RemixSDK?.onToggleMute(() => {})
260
+ window.RemixSDK?.onPlayAgain(() => {})
261
+ window.RemixSDK?.singlePlayer.actions.gameOver({ score: 10 })
262
+ window.RemixSDK?.multiplayer.actions.gameOver({ scores: [] })
263
+ window.RemixSDK?.multiplayer.actions.gameOver({ scores: [] })
264
+ </script>
265
+ </body>
266
+ </html>`);
267
+ const result = await validateGame('/tmp/game.html');
268
+ expect(result.valid).toBe(false);
269
+ expect(result.issues).toContain('Both singlePlayer.actions.gameOver and multiplayer.actions.gameOver detected — use exactly one. Use singlePlayer for solo games, multiplayer for two-player games.');
270
+ });
271
+ it('reports legacy Farcade SDK usage with a migration-specific issue', async () => {
272
+ mockReadFile.mockResolvedValueOnce(`<!DOCTYPE html>
273
+ <html>
274
+ <head>
275
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
276
+ <script src="https://cdn.jsdelivr.net/npm/@farcade/game-sdk@latest/dist/index.min.js"></script>
277
+ </head>
278
+ <body>
279
+ <script>
280
+ window.RemixSDK?.onToggleMute(() => {})
281
+ window.RemixSDK?.onPlayAgain(() => {})
282
+ window.RemixSDK?.singlePlayer.actions.gameOver({ score: 10 })
283
+ </script>
284
+ </body>
285
+ </html>`);
286
+ const result = await validateGame('/tmp/game.html');
287
+ expect(result.valid).toBe(false);
288
+ expect(result.issues).toContain('Legacy @farcade/game-sdk script detected — replace it with <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>');
289
+ expect(result.issues).not.toContain('Missing RemixSDK script tag in <head> — add: <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>');
290
+ });
291
+ it('reports missing required integrations and unsafe patterns', async () => {
292
+ mockReadFile.mockResolvedValueOnce('<html><body><button onclick="play()"></button></body></html>');
293
+ const result = await validateGame('/tmp/game.html');
294
+ expect(result.valid).toBe(false);
295
+ expect(result.issues).toContain('Missing <!DOCTYPE html> declaration');
296
+ expect(result.issues).toContain('Missing viewport meta tag — needed for mobile play');
297
+ expect(result.issues).toContain('Missing RemixSDK script tag in <head> — add: <script src="https://cdn.jsdelivr.net/npm/@remix-gg/sdk@latest/dist/index.min.js"></script>');
298
+ expect(result.issues).toContain('Missing gameOver call — use singlePlayer.actions.gameOver({ score }) for solo games or multiplayer.actions.gameOver({ scores }) for two-player games');
299
+ expect(result.issues).toContain('Missing onToggleMute(callback) call — must register a callback to handle mute/unmute');
300
+ expect(result.issues).toContain('Missing onPlayAgain(callback) call — must register a callback to restart the game');
301
+ expect(result.issues).toContain('Inline event handlers detected — use addEventListener instead');
302
+ });
303
+ });
304
+ describe('generateImage()', () => {
305
+ it('generates an image, uploads it as an asset, and returns the hosted URL', async () => {
306
+ mockPost
307
+ .mockResolvedValueOnce({
308
+ data: {
309
+ success: true,
310
+ data: {
311
+ image: {
312
+ data: Buffer.from('png-bytes').toString('base64'),
313
+ contentType: 'image/png',
314
+ },
315
+ prompt: 'hero sprite',
316
+ },
317
+ },
318
+ error: undefined,
319
+ response: new Response(null, { status: 200 }),
320
+ })
321
+ .mockResolvedValueOnce({
322
+ data: {
323
+ success: true,
324
+ data: {
325
+ gameId: 'game-1',
326
+ asset: { url: 'https://cdn.remix.gg/hero-sprite.png' },
327
+ },
328
+ },
329
+ error: undefined,
330
+ response: new Response(null, { status: 200 }),
331
+ });
332
+ const result = await generateImage('game-1', 'Hero Sprite');
333
+ expect(mockPost).toHaveBeenNthCalledWith(1, '/v1/games/{gameId}/images/generate', {
334
+ params: { path: { gameId: 'game-1' } },
335
+ body: { prompt: 'Hero Sprite' },
336
+ });
337
+ const uploadCall = mockPost.mock.calls[1];
338
+ expect(uploadCall[0]).toBe('/v1/games/{gameId}/assets');
339
+ expect(uploadCall[1].params).toEqual({ path: { gameId: 'game-1' } });
340
+ expect(uploadCall[1].body).toBeInstanceOf(FormData);
341
+ expect(result).toEqual({
342
+ url: 'https://cdn.remix.gg/hero-sprite.png',
343
+ fileName: 'hero-sprite.png',
344
+ prompt: 'Hero Sprite',
345
+ });
346
+ });
347
+ });
348
+ describe('generateSpriteSheet()', () => {
349
+ it('passes prompt options through and maps the sprite response', async () => {
350
+ mockPost.mockResolvedValueOnce({
351
+ data: {
352
+ success: true,
353
+ data: {
354
+ spriteUrl: 'https://cdn.remix.gg/sprite.png',
355
+ transparentSpriteUrl: 'https://cdn.remix.gg/sprite-transparent.png',
356
+ promptOriginal: 'knight attack',
357
+ promptRewritten: 'pixel knight attack animation',
358
+ metadata: {
359
+ gridSize: 6,
360
+ gridDimensions: '6x6',
361
+ },
362
+ warnings: ['large sprite sheet'],
363
+ },
364
+ },
365
+ error: undefined,
366
+ response: new Response(null, { status: 200 }),
367
+ });
368
+ const result = await generateSpriteSheet('game-1', 'knight attack', 6, 'https://example.com/reference.png');
369
+ expect(mockPost).toHaveBeenCalledWith('/v1/games/{gameId}/sprites/generate', {
370
+ params: { path: { gameId: 'game-1' } },
371
+ body: {
372
+ prompt: 'knight attack',
373
+ gridSize: 6,
374
+ imageUrl: 'https://example.com/reference.png',
375
+ },
376
+ });
377
+ expect(result).toEqual({
378
+ spriteUrl: 'https://cdn.remix.gg/sprite.png',
379
+ transparentSpriteUrl: 'https://cdn.remix.gg/sprite-transparent.png',
380
+ promptOriginal: 'knight attack',
381
+ promptRewritten: 'pixel knight attack animation',
382
+ metadata: {
383
+ gridSize: 6,
384
+ gridDimensions: '6x6',
385
+ },
386
+ warnings: ['large sprite sheet'],
387
+ });
388
+ });
389
+ });
390
+ describe('uploadGameAsset()', () => {
391
+ it('rejects unsupported file types before calling the API', async () => {
392
+ await expect(uploadGameAsset('game-1', '/tmp/asset.txt')).rejects.toThrow('Unsupported file type ".txt"');
393
+ expect(mockStat).not.toHaveBeenCalled();
394
+ expect(mockPost).not.toHaveBeenCalled();
395
+ });
396
+ it('rejects oversized files before reading them', async () => {
397
+ mockStat.mockResolvedValueOnce({ size: 6 * 1024 * 1024 });
398
+ await expect(uploadGameAsset('game-1', '/tmp/asset.png')).rejects.toThrow('File is too large (6.0 MB). Maximum allowed size is 5 MB.');
399
+ expect(mockReadFile).not.toHaveBeenCalled();
400
+ expect(mockPost).not.toHaveBeenCalled();
401
+ });
402
+ it('uploads supported files with form data and returns the raw response', async () => {
403
+ mockReadFile.mockResolvedValueOnce(Buffer.from('asset-bytes'));
404
+ mockPost.mockResolvedValueOnce({
405
+ data: {
406
+ success: true,
407
+ data: {
408
+ gameId: 'game-1',
409
+ asset: { id: 'asset-1', url: 'https://cdn.remix.gg/asset.png' },
410
+ },
411
+ },
412
+ error: undefined,
413
+ response: new Response(null, { status: 200 }),
414
+ });
415
+ const result = await uploadGameAsset('game-1', '/tmp/asset.png', 'hero-asset');
416
+ expect(mockStat).toHaveBeenCalledWith('/tmp/asset.png');
417
+ expect(mockReadFile).toHaveBeenCalledWith('/tmp/asset.png');
418
+ const uploadCall = mockPost.mock.calls[0];
419
+ expect(uploadCall[0]).toBe('/v1/games/{gameId}/assets');
420
+ expect(uploadCall[1].params).toEqual({ path: { gameId: 'game-1' } });
421
+ expect(uploadCall[1].body).toBeInstanceOf(FormData);
422
+ expect(result).toEqual({
423
+ fileName: 'asset.png',
424
+ gameId: 'game-1',
425
+ response: {
426
+ success: true,
427
+ data: {
428
+ gameId: 'game-1',
429
+ asset: { id: 'asset-1', url: 'https://cdn.remix.gg/asset.png' },
430
+ },
431
+ },
432
+ });
433
+ });
434
+ });
435
+ describe('createShopItem()', () => {
436
+ it('creates a shop item and normalizes numeric fields', async () => {
437
+ mockPost.mockResolvedValueOnce({
438
+ data: {
439
+ success: true,
440
+ data: {
441
+ item: {
442
+ id: 'item-1',
443
+ gameId: 'game-1',
444
+ name: 'Extra Life',
445
+ slug: 'extra-life',
446
+ status: 'ACTIVE',
447
+ itemType: 'CONSUMABLE',
448
+ bitsCost: '50',
449
+ description: 'Spend one to restore a life.',
450
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
451
+ tier: null,
452
+ createdAt: '2026-03-03T00:00:00.000Z',
453
+ updatedAt: '2026-03-03T00:00:00.000Z',
454
+ },
455
+ },
456
+ },
457
+ error: undefined,
458
+ response: new Response(null, { status: 201 }),
459
+ });
460
+ const result = await createShopItem({
461
+ gameId: 'game-1',
462
+ name: 'Extra Life',
463
+ slug: 'extra-life',
464
+ itemType: 'CONSUMABLE',
465
+ bitsCost: 50,
466
+ description: 'Spend one to restore a life.',
467
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
468
+ });
469
+ expect(mockPost).toHaveBeenCalledWith('/v1/games/{gameId}/items', {
470
+ params: {
471
+ path: {
472
+ gameId: 'game-1',
473
+ },
474
+ },
475
+ body: {
476
+ name: 'Extra Life',
477
+ slug: 'extra-life',
478
+ itemType: 'CONSUMABLE',
479
+ bitsCost: 50,
480
+ description: 'Spend one to restore a life.',
481
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
482
+ },
483
+ });
484
+ expect(result).toEqual({
485
+ id: 'item-1',
486
+ gameId: 'game-1',
487
+ name: 'Extra Life',
488
+ slug: 'extra-life',
489
+ status: 'ACTIVE',
490
+ itemType: 'CONSUMABLE',
491
+ bitsCost: 50,
492
+ description: 'Spend one to restore a life.',
493
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
494
+ tier: null,
495
+ createdAt: '2026-03-03T00:00:00.000Z',
496
+ updatedAt: '2026-03-03T00:00:00.000Z',
497
+ });
498
+ });
499
+ it('auto-generates and uploads a shop icon when iconUrl is omitted', async () => {
500
+ mockPost
501
+ .mockResolvedValueOnce({
502
+ data: {
503
+ success: true,
504
+ data: {
505
+ image: {
506
+ data: Buffer.from('png-bytes').toString('base64'),
507
+ contentType: 'image/png',
508
+ },
509
+ prompt: 'generated icon prompt',
510
+ },
511
+ },
512
+ error: undefined,
513
+ response: new Response(null, { status: 200 }),
514
+ })
515
+ .mockResolvedValueOnce({
516
+ data: {
517
+ success: true,
518
+ data: {
519
+ gameId: 'game-1',
520
+ asset: { id: 'asset-1', url: 'https://cdn.remix.gg/shop-item-extra-life.png' },
521
+ },
522
+ },
523
+ error: undefined,
524
+ response: new Response(null, { status: 200 }),
525
+ })
526
+ .mockResolvedValueOnce({
527
+ data: {
528
+ success: true,
529
+ data: {
530
+ item: {
531
+ id: 'item-1',
532
+ gameId: 'game-1',
533
+ name: 'Extra Life',
534
+ slug: 'extra-life',
535
+ status: 'PENDING',
536
+ itemType: 'CONSUMABLE',
537
+ bitsCost: 50,
538
+ description: 'Spend one to restore a life.',
539
+ iconUrl: 'https://cdn.remix.gg/shop-item-extra-life.png',
540
+ tier: null,
541
+ createdAt: '2026-03-03T00:00:00.000Z',
542
+ updatedAt: '2026-03-03T00:00:00.000Z',
543
+ },
544
+ },
545
+ },
546
+ error: undefined,
547
+ response: new Response(null, { status: 201 }),
548
+ });
549
+ const result = await createShopItem({
550
+ gameId: 'game-1',
551
+ name: 'Extra Life',
552
+ slug: 'extra-life',
553
+ itemType: 'CONSUMABLE',
554
+ bitsCost: 50,
555
+ description: 'Spend one to restore a life.',
556
+ });
557
+ expect(mockPost).toHaveBeenNthCalledWith(1, '/v1/games/{gameId}/images/generate', {
558
+ params: { path: { gameId: 'game-1' } },
559
+ body: expect.objectContaining({
560
+ prompt: expect.stringContaining('Create a polished game shop icon'),
561
+ }),
562
+ });
563
+ expect(mockPost).toHaveBeenNthCalledWith(2, '/v1/games/{gameId}/assets', expect.objectContaining({
564
+ params: { path: { gameId: 'game-1' } },
565
+ body: expect.any(FormData),
566
+ }));
567
+ expect(mockPost).toHaveBeenNthCalledWith(3, '/v1/games/{gameId}/items', {
568
+ params: {
569
+ path: {
570
+ gameId: 'game-1',
571
+ },
572
+ },
573
+ body: {
574
+ name: 'Extra Life',
575
+ slug: 'extra-life',
576
+ itemType: 'CONSUMABLE',
577
+ bitsCost: 50,
578
+ description: 'Spend one to restore a life.',
579
+ iconUrl: 'https://cdn.remix.gg/shop-item-extra-life.png',
580
+ },
581
+ });
582
+ expect(result.iconUrl).toBe('https://cdn.remix.gg/shop-item-extra-life.png');
583
+ });
584
+ it('falls back to creating the item without an icon if auto-generation fails', async () => {
585
+ const warnSpy = mock(() => { });
586
+ const originalWarn = console.warn;
587
+ console.warn = warnSpy;
588
+ mockPost
589
+ .mockResolvedValueOnce({
590
+ data: undefined,
591
+ error: { error: { message: 'Image generation failed' } },
592
+ response: new Response(null, { status: 500, statusText: 'Internal Server Error' }),
593
+ })
594
+ .mockResolvedValueOnce({
595
+ data: {
596
+ success: true,
597
+ data: {
598
+ item: {
599
+ id: 'item-1',
600
+ gameId: 'game-1',
601
+ name: 'Shield',
602
+ slug: 'shield',
603
+ status: 'PENDING',
604
+ itemType: 'ONE_TIME',
605
+ bitsCost: 100,
606
+ description: 'Protects the player once.',
607
+ iconUrl: null,
608
+ tier: null,
609
+ createdAt: '2026-03-03T00:00:00.000Z',
610
+ updatedAt: '2026-03-03T00:00:00.000Z',
611
+ },
612
+ },
613
+ },
614
+ error: undefined,
615
+ response: new Response(null, { status: 201 }),
616
+ });
617
+ try {
618
+ const result = await createShopItem({
619
+ gameId: 'game-1',
620
+ name: 'Shield',
621
+ slug: 'shield',
622
+ itemType: 'ONE_TIME',
623
+ bitsCost: 100,
624
+ description: 'Protects the player once.',
625
+ });
626
+ expect(mockPost).toHaveBeenNthCalledWith(2, '/v1/games/{gameId}/items', {
627
+ params: {
628
+ path: {
629
+ gameId: 'game-1',
630
+ },
631
+ },
632
+ body: {
633
+ name: 'Shield',
634
+ slug: 'shield',
635
+ itemType: 'ONE_TIME',
636
+ bitsCost: 100,
637
+ description: 'Protects the player once.',
638
+ },
639
+ });
640
+ expect(warnSpy).toHaveBeenCalled();
641
+ expect(result.iconUrl).toBeNull();
642
+ }
643
+ finally {
644
+ console.warn = originalWarn;
645
+ }
646
+ });
647
+ });
648
+ describe('listShopItems()', () => {
649
+ it('lists shop items and normalizes numeric fields', async () => {
650
+ mockGet.mockResolvedValueOnce({
651
+ data: {
652
+ success: true,
653
+ data: {
654
+ gameId: 'game-1',
655
+ items: [
656
+ {
657
+ id: 'item-1',
658
+ gameId: 'game-1',
659
+ name: 'Extra Life',
660
+ slug: 'extra-life',
661
+ status: 'ACTIVE',
662
+ itemType: 'CONSUMABLE',
663
+ bitsCost: '50',
664
+ description: 'Spend one to restore a life.',
665
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
666
+ tier: null,
667
+ createdAt: '2026-03-03T00:00:00.000Z',
668
+ updatedAt: '2026-03-03T00:00:00.000Z',
669
+ },
670
+ ],
671
+ },
672
+ },
673
+ error: undefined,
674
+ response: new Response(null, { status: 200 }),
675
+ });
676
+ const result = await listShopItems('game-1');
677
+ expect(mockGet).toHaveBeenCalledWith('/v1/games/{gameId}/items', {
678
+ params: {
679
+ path: {
680
+ gameId: 'game-1',
681
+ },
682
+ },
683
+ });
684
+ expect(result).toEqual({
685
+ gameId: 'game-1',
686
+ items: [
687
+ {
688
+ id: 'item-1',
689
+ gameId: 'game-1',
690
+ name: 'Extra Life',
691
+ slug: 'extra-life',
692
+ status: 'ACTIVE',
693
+ itemType: 'CONSUMABLE',
694
+ bitsCost: 50,
695
+ description: 'Spend one to restore a life.',
696
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
697
+ tier: null,
698
+ createdAt: '2026-03-03T00:00:00.000Z',
699
+ updatedAt: '2026-03-03T00:00:00.000Z',
700
+ },
701
+ ],
702
+ });
703
+ });
704
+ });
705
+ describe('updateShopItem()', () => {
706
+ it('updates a shop item and forwards only provided fields', async () => {
707
+ mockPost.mockResolvedValueOnce({
708
+ data: {
709
+ success: true,
710
+ data: {
711
+ item: {
712
+ id: 'item-1',
713
+ gameId: 'game-1',
714
+ name: 'Extra Life Pack',
715
+ slug: 'extra-life',
716
+ status: 'ACTIVE',
717
+ itemType: 'CONSUMABLE',
718
+ bitsCost: '75',
719
+ description: null,
720
+ iconUrl: null,
721
+ tier: null,
722
+ createdAt: '2026-03-03T00:00:00.000Z',
723
+ updatedAt: '2026-03-04T00:00:00.000Z',
724
+ },
725
+ },
726
+ },
727
+ error: undefined,
728
+ response: new Response(null, { status: 200 }),
729
+ });
730
+ const result = await updateShopItem({
731
+ gameId: 'game-1',
732
+ itemId: 'item-1',
733
+ name: 'Extra Life Pack',
734
+ bitsCost: 75,
735
+ description: null,
736
+ iconUrl: null,
737
+ });
738
+ expect(mockPost).toHaveBeenCalledWith('/v1/games/{gameId}/items/{itemId}', {
739
+ params: {
740
+ path: {
741
+ gameId: 'game-1',
742
+ itemId: 'item-1',
743
+ },
744
+ },
745
+ body: {
746
+ name: 'Extra Life Pack',
747
+ bitsCost: 75,
748
+ description: null,
749
+ iconUrl: null,
750
+ },
751
+ });
752
+ expect(result).toEqual({
753
+ id: 'item-1',
754
+ gameId: 'game-1',
755
+ name: 'Extra Life Pack',
756
+ slug: 'extra-life',
757
+ status: 'ACTIVE',
758
+ itemType: 'CONSUMABLE',
759
+ bitsCost: 75,
760
+ description: null,
761
+ iconUrl: null,
762
+ tier: null,
763
+ createdAt: '2026-03-03T00:00:00.000Z',
764
+ updatedAt: '2026-03-04T00:00:00.000Z',
765
+ });
766
+ });
767
+ });
768
+ describe('deleteShopItem()', () => {
769
+ it('deletes or deactivates an item and maps the optional item payload', async () => {
770
+ mockDelete.mockResolvedValueOnce({
771
+ data: {
772
+ success: true,
773
+ data: {
774
+ deleted: true,
775
+ itemId: 'item-1',
776
+ item: {
777
+ id: 'item-1',
778
+ gameId: 'game-1',
779
+ name: 'Extra Life',
780
+ slug: 'extra-life',
781
+ status: 'INACTIVE',
782
+ itemType: 'CONSUMABLE',
783
+ bitsCost: '50',
784
+ description: 'Spend one to restore a life.',
785
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
786
+ tier: null,
787
+ createdAt: '2026-03-03T00:00:00.000Z',
788
+ updatedAt: '2026-03-04T00:00:00.000Z',
789
+ },
790
+ },
791
+ },
792
+ error: undefined,
793
+ response: new Response(null, { status: 200 }),
794
+ });
795
+ const result = await deleteShopItem('game-1', 'item-1');
796
+ expect(mockDelete).toHaveBeenCalledWith('/v1/games/{gameId}/items/{itemId}', {
797
+ params: {
798
+ path: {
799
+ gameId: 'game-1',
800
+ itemId: 'item-1',
801
+ },
802
+ },
803
+ });
804
+ expect(result).toEqual({
805
+ deleted: true,
806
+ itemId: 'item-1',
807
+ item: {
808
+ id: 'item-1',
809
+ gameId: 'game-1',
810
+ name: 'Extra Life',
811
+ slug: 'extra-life',
812
+ status: 'INACTIVE',
813
+ itemType: 'CONSUMABLE',
814
+ bitsCost: 50,
815
+ description: 'Spend one to restore a life.',
816
+ iconUrl: 'https://cdn.remix.gg/extra-life.png',
817
+ tier: null,
818
+ createdAt: '2026-03-03T00:00:00.000Z',
819
+ updatedAt: '2026-03-04T00:00:00.000Z',
820
+ },
821
+ });
822
+ });
823
+ });
824
+ });
825
+ //# sourceMappingURL=tools.test.js.map