@satelliteoflove/godot-mcp 1.3.0 → 2.0.2

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 (60) hide show
  1. package/README.md +8 -4
  2. package/dist/__tests__/tools/animation.test.js +319 -306
  3. package/dist/__tests__/tools/animation.test.js.map +1 -1
  4. package/dist/__tests__/tools/editor.test.d.ts +2 -0
  5. package/dist/__tests__/tools/editor.test.d.ts.map +1 -0
  6. package/dist/__tests__/tools/editor.test.js +225 -0
  7. package/dist/__tests__/tools/editor.test.js.map +1 -0
  8. package/dist/__tests__/tools/node.test.js +151 -44
  9. package/dist/__tests__/tools/node.test.js.map +1 -1
  10. package/dist/__tests__/tools/resource.test.js +32 -20
  11. package/dist/__tests__/tools/resource.test.js.map +1 -1
  12. package/dist/__tests__/tools/scene.test.js +28 -51
  13. package/dist/__tests__/tools/scene.test.js.map +1 -1
  14. package/dist/__tests__/tools/tilemap.test.js +336 -323
  15. package/dist/__tests__/tools/tilemap.test.js.map +1 -1
  16. package/dist/tools/animation.d.ts +46 -118
  17. package/dist/tools/animation.d.ts.map +1 -1
  18. package/dist/tools/animation.js +95 -150
  19. package/dist/tools/animation.js.map +1 -1
  20. package/dist/tools/editor.d.ts +27 -15
  21. package/dist/tools/editor.d.ts.map +1 -1
  22. package/dist/tools/editor.js +86 -77
  23. package/dist/tools/editor.js.map +1 -1
  24. package/dist/tools/index.d.ts +0 -2
  25. package/dist/tools/index.d.ts.map +1 -1
  26. package/dist/tools/index.js +0 -6
  27. package/dist/tools/index.js.map +1 -1
  28. package/dist/tools/node.d.ts +46 -45
  29. package/dist/tools/node.d.ts.map +1 -1
  30. package/dist/tools/node.js +135 -88
  31. package/dist/tools/node.js.map +1 -1
  32. package/dist/tools/project.d.ts +4 -25
  33. package/dist/tools/project.d.ts.map +1 -1
  34. package/dist/tools/project.js +30 -75
  35. package/dist/tools/project.js.map +1 -1
  36. package/dist/tools/resource.d.ts +17 -4
  37. package/dist/tools/resource.d.ts.map +1 -1
  38. package/dist/tools/resource.js +42 -24
  39. package/dist/tools/resource.js.map +1 -1
  40. package/dist/tools/scene.d.ts +20 -22
  41. package/dist/tools/scene.d.ts.map +1 -1
  42. package/dist/tools/scene.js +57 -62
  43. package/dist/tools/scene.js.map +1 -1
  44. package/dist/tools/tilemap.d.ts +92 -188
  45. package/dist/tools/tilemap.d.ts.map +1 -1
  46. package/dist/tools/tilemap.js +57 -96
  47. package/dist/tools/tilemap.js.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/__tests__/screenshot.test.d.ts +0 -2
  50. package/dist/__tests__/screenshot.test.d.ts.map +0 -1
  51. package/dist/__tests__/screenshot.test.js +0 -42
  52. package/dist/__tests__/screenshot.test.js.map +0 -1
  53. package/dist/tools/screenshot.d.ts +0 -21
  54. package/dist/tools/screenshot.d.ts.map +0 -1
  55. package/dist/tools/screenshot.js +0 -46
  56. package/dist/tools/screenshot.js.map +0 -1
  57. package/dist/tools/script.d.ts +0 -51
  58. package/dist/tools/script.d.ts.map +0 -1
  59. package/dist/tools/script.js +0 -86
  60. package/dist/tools/script.js.map +0 -1
package/README.md CHANGED
@@ -4,12 +4,16 @@ MCP (Model Context Protocol) server for Godot Engine integration. Enables AI ass
4
4
 
5
5
  ## Features
6
6
 
7
- - **33 MCP tools** across 8 categories (scene, node, script, editor, project, screenshot, animation, tilemap)
7
+ <!-- NPM_FEATURES_START -->
8
+ - **8 MCP tools** for scene, node, editor, project, animation, tilemap, resource operations
8
9
  - **3 MCP resources** for reading scene trees, scripts, and project files
9
10
  - Real-time bidirectional communication via WebSocket
10
- - Debug output and screenshot capture from running games
11
- - Full TileMapLayer and GridMap editing support
12
- - Animation query, playback, and editing
11
+ - Screenshot capture from editor viewports and running games
12
+ - Full animation support (query, playback, editing)
13
+ - TileMapLayer and GridMap editing
14
+ - Resource inspection for SpriteFrames, TileSets, Materials, and Textures
15
+ - Debug output capture from running games
16
+ <!-- NPM_FEATURES_END -->
13
17
 
14
18
  ## Requirements
15
19
 
@@ -1,315 +1,328 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { createMockGodot, createToolContext } from '../helpers/mock-godot.js';
3
- import { animationQuery, animationPlayback, animationEdit } from '../../tools/animation.js';
4
- describe('animation_query', () => {
5
- let mock;
6
- beforeEach(() => {
7
- mock = createMockGodot();
8
- });
9
- it('list_players sends command and formats empty result', async () => {
10
- mock.mockResponse({ animation_players: [] });
11
- const ctx = createToolContext(mock);
12
- const result = await animationQuery.execute({ action: 'list_players' }, ctx);
13
- expect(mock.calls[0].command).toBe('list_animation_players');
14
- expect(result).toBe('No AnimationPlayer nodes found in scene');
15
- });
16
- it('list_players formats found players', async () => {
17
- mock.mockResponse({
18
- animation_players: [
19
- { path: '/root/Player/AnimPlayer', name: 'AnimPlayer' },
20
- { path: '/root/Enemy/AnimPlayer', name: 'AnimPlayer' },
21
- ],
3
+ import { animation, animationTools } from '../../tools/animation.js';
4
+ describe('animation tool', () => {
5
+ describe('tool definitions', () => {
6
+ it('exports one tool', () => {
7
+ expect(animationTools).toHaveLength(1);
8
+ });
9
+ it('has animation tool with all action types', () => {
10
+ expect(animation.name).toBe('animation');
11
+ expect(animation.description).toContain('Query');
12
+ expect(animation.description).toContain('Playback');
13
+ expect(animation.description).toContain('Edit');
22
14
  });
23
- const ctx = createToolContext(mock);
24
- const result = await animationQuery.execute({ action: 'list_players' }, ctx);
25
- expect(result).toContain('Found 2 AnimationPlayer(s)');
26
- expect(result).toContain('/root/Player/AnimPlayer');
27
- });
28
- it('get_info sends command and returns JSON', async () => {
29
- const info = { current_animation: 'idle', is_playing: true, current_position: 0.5 };
30
- mock.mockResponse(info);
31
- const ctx = createToolContext(mock);
32
- const result = await animationQuery.execute({
33
- action: 'get_info',
34
- node_path: '/root/AnimPlayer',
35
- }, ctx);
36
- expect(mock.calls[0].command).toBe('get_animation_player_info');
37
- expect(mock.calls[0].params.node_path).toBe('/root/AnimPlayer');
38
- expect(result).toBe(JSON.stringify(info, null, 2));
39
- });
40
- it('get_details sends command with animation name', async () => {
41
- const details = { name: 'walk', length: 1.5, track_count: 3 };
42
- mock.mockResponse(details);
43
- const ctx = createToolContext(mock);
44
- const result = await animationQuery.execute({
45
- action: 'get_details',
46
- node_path: '/root/AnimPlayer',
47
- animation_name: 'walk',
48
- }, ctx);
49
- expect(mock.calls[0].command).toBe('get_animation_details');
50
- expect(mock.calls[0].params.animation_name).toBe('walk');
51
- expect(result).toBe(JSON.stringify(details, null, 2));
52
- });
53
- it('get_keyframes sends command with track index', async () => {
54
- const keyframes = { track_path: 'Sprite:frame', keyframes: [{ time: 0, value: 0 }] };
55
- mock.mockResponse(keyframes);
56
- const ctx = createToolContext(mock);
57
- const result = await animationQuery.execute({
58
- action: 'get_keyframes',
59
- node_path: '/root/AnimPlayer',
60
- animation_name: 'walk',
61
- track_index: 0,
62
- }, ctx);
63
- expect(mock.calls[0].command).toBe('get_track_keyframes');
64
- expect(mock.calls[0].params.track_index).toBe(0);
65
- expect(result).toBe(JSON.stringify(keyframes, null, 2));
66
- });
67
- it('throws on error from Godot', async () => {
68
- mock.mockError(new Error('Node not found'));
69
- const ctx = createToolContext(mock);
70
- await expect(animationQuery.execute({
71
- action: 'get_info',
72
- node_path: '/root/Missing',
73
- }, ctx)).rejects.toThrow('Node not found');
74
- });
75
- });
76
- describe('animation_playback', () => {
77
- let mock;
78
- beforeEach(() => {
79
- mock = createMockGodot();
80
- });
81
- it('play sends command and returns confirmation', async () => {
82
- mock.mockResponse({ playing: 'run', from_position: 0 });
83
- const ctx = createToolContext(mock);
84
- const result = await animationPlayback.execute({
85
- action: 'play',
86
- node_path: '/root/AnimPlayer',
87
- animation_name: 'run',
88
- }, ctx);
89
- expect(mock.calls[0].command).toBe('play_animation');
90
- expect(mock.calls[0].params.animation_name).toBe('run');
91
- expect(result).toBe('Playing animation: run');
92
- });
93
- it('play passes optional params', async () => {
94
- mock.mockResponse({ playing: 'walk', from_position: 0 });
95
- const ctx = createToolContext(mock);
96
- await animationPlayback.execute({
97
- action: 'play',
98
- node_path: '/root/AnimPlayer',
99
- animation_name: 'walk',
100
- custom_speed: 2.0,
101
- custom_blend: 0.5,
102
- from_end: true,
103
- }, ctx);
104
- expect(mock.calls[0].params.custom_speed).toBe(2.0);
105
- expect(mock.calls[0].params.custom_blend).toBe(0.5);
106
- expect(mock.calls[0].params.from_end).toBe(true);
107
- });
108
- it('stop sends command and returns confirmation', async () => {
109
- mock.mockResponse({});
110
- const ctx = createToolContext(mock);
111
- const result = await animationPlayback.execute({
112
- action: 'stop',
113
- node_path: '/root/AnimPlayer',
114
- }, ctx);
115
- expect(mock.calls[0].command).toBe('stop_animation');
116
- expect(result).toBe('Animation stopped');
117
- });
118
- it('pause sends command with paused=true', async () => {
119
- mock.mockResponse({});
120
- const ctx = createToolContext(mock);
121
- const result = await animationPlayback.execute({
122
- action: 'pause',
123
- node_path: '/root/AnimPlayer',
124
- paused: true,
125
- }, ctx);
126
- expect(mock.calls[0].command).toBe('pause_animation');
127
- expect(mock.calls[0].params.paused).toBe(true);
128
- expect(result).toBe('Animation paused');
129
- });
130
- it('pause sends command with paused=false', async () => {
131
- mock.mockResponse({});
132
- const ctx = createToolContext(mock);
133
- const result = await animationPlayback.execute({
134
- action: 'pause',
135
- node_path: '/root/AnimPlayer',
136
- paused: false,
137
- }, ctx);
138
- expect(mock.calls[0].params.paused).toBe(false);
139
- expect(result).toBe('Animation unpaused');
140
- });
141
- it('seek sends command and returns position', async () => {
142
- mock.mockResponse({ position: 1.5 });
143
- const ctx = createToolContext(mock);
144
- const result = await animationPlayback.execute({
145
- action: 'seek',
146
- node_path: '/root/AnimPlayer',
147
- seconds: 1.5,
148
- }, ctx);
149
- expect(mock.calls[0].command).toBe('seek_animation');
150
- expect(mock.calls[0].params.seconds).toBe(1.5);
151
- expect(result).toBe('Seeked to position: 1.5');
152
- });
153
- it('queue sends command and returns queue info', async () => {
154
- mock.mockResponse({ queued: 'attack', queue_length: 3 });
155
- const ctx = createToolContext(mock);
156
- const result = await animationPlayback.execute({
157
- action: 'queue',
158
- node_path: '/root/AnimPlayer',
159
- animation_name: 'attack',
160
- }, ctx);
161
- expect(mock.calls[0].command).toBe('queue_animation');
162
- expect(result).toBe('Queued animation: attack (queue length: 3)');
163
- });
164
- it('clear_queue sends command and returns confirmation', async () => {
165
- mock.mockResponse({});
166
- const ctx = createToolContext(mock);
167
- const result = await animationPlayback.execute({
168
- action: 'clear_queue',
169
- node_path: '/root/AnimPlayer',
170
- }, ctx);
171
- expect(mock.calls[0].command).toBe('clear_animation_queue');
172
- expect(result).toBe('Animation queue cleared');
173
- });
174
- });
175
- describe('animation_edit', () => {
176
- let mock;
177
- beforeEach(() => {
178
- mock = createMockGodot();
179
- });
180
- it('create sends command and returns confirmation', async () => {
181
- mock.mockResponse({ created: 'new_anim', library: '' });
182
- const ctx = createToolContext(mock);
183
- const result = await animationEdit.execute({
184
- action: 'create',
185
- node_path: '/root/AnimPlayer',
186
- animation_name: 'new_anim',
187
- length: 2.0,
188
- }, ctx);
189
- expect(mock.calls[0].command).toBe('create_animation');
190
- expect(mock.calls[0].params.animation_name).toBe('new_anim');
191
- expect(mock.calls[0].params.length).toBe(2.0);
192
- expect(result).toBe('Created animation: new_anim');
193
- });
194
- it('create includes library in result when provided', async () => {
195
- mock.mockResponse({ created: 'walk', library: 'movement' });
196
- const ctx = createToolContext(mock);
197
- const result = await animationEdit.execute({
198
- action: 'create',
199
- node_path: '/root/AnimPlayer',
200
- animation_name: 'walk',
201
- library_name: 'movement',
202
- }, ctx);
203
- expect(result).toBe('Created animation: walk in library: movement');
204
- });
205
- it('delete sends command and returns confirmation', async () => {
206
- mock.mockResponse({ deleted: 'old_anim' });
207
- const ctx = createToolContext(mock);
208
- const result = await animationEdit.execute({
209
- action: 'delete',
210
- node_path: '/root/AnimPlayer',
211
- animation_name: 'old_anim',
212
- }, ctx);
213
- expect(mock.calls[0].command).toBe('delete_animation');
214
- expect(result).toBe('Deleted animation: old_anim');
215
- });
216
- it('rename sends command with old and new names', async () => {
217
- mock.mockResponse({ renamed: { from: 'walk', to: 'walk_slow' } });
218
- const ctx = createToolContext(mock);
219
- const result = await animationEdit.execute({
220
- action: 'rename',
221
- node_path: '/root/AnimPlayer',
222
- old_name: 'walk',
223
- new_name: 'walk_slow',
224
- }, ctx);
225
- expect(mock.calls[0].command).toBe('rename_animation');
226
- expect(mock.calls[0].params.old_name).toBe('walk');
227
- expect(mock.calls[0].params.new_name).toBe('walk_slow');
228
- expect(result).toBe('Renamed animation: walk -> walk_slow');
229
- });
230
- it('update_props sends command and returns updated properties', async () => {
231
- mock.mockResponse({ updated: 'walk', properties: { length: 2.0, loop_mode: 'linear' } });
232
- const ctx = createToolContext(mock);
233
- const result = await animationEdit.execute({
234
- action: 'update_props',
235
- node_path: '/root/AnimPlayer',
236
- animation_name: 'walk',
237
- length: 2.0,
238
- loop_mode: 'linear',
239
- }, ctx);
240
- expect(mock.calls[0].command).toBe('update_animation_properties');
241
- expect(result).toContain('Updated animation: walk');
242
- expect(result).toContain('"length":2');
243
- });
244
- it('add_track sends command and returns track info', async () => {
245
- mock.mockResponse({ track_index: 0, track_path: 'Sprite2D:frame', track_type: 'value' });
246
- const ctx = createToolContext(mock);
247
- const result = await animationEdit.execute({
248
- action: 'add_track',
249
- node_path: '/root/AnimPlayer',
250
- animation_name: 'walk',
251
- track_type: 'value',
252
- track_path: 'Sprite2D:frame',
253
- }, ctx);
254
- expect(mock.calls[0].command).toBe('add_animation_track');
255
- expect(result).toBe('Added track 0: value -> Sprite2D:frame');
256
- });
257
- it('remove_track sends command and returns confirmation', async () => {
258
- mock.mockResponse({ removed_track: 2 });
259
- const ctx = createToolContext(mock);
260
- const result = await animationEdit.execute({
261
- action: 'remove_track',
262
- node_path: '/root/AnimPlayer',
263
- animation_name: 'walk',
264
- track_index: 2,
265
- }, ctx);
266
- expect(mock.calls[0].command).toBe('remove_animation_track');
267
- expect(result).toBe('Removed track: 2');
268
15
  });
269
- it('add_keyframe sends command and returns keyframe info', async () => {
270
- mock.mockResponse({ keyframe_index: 0, time: 0.5, value: 3 });
271
- const ctx = createToolContext(mock);
272
- const result = await animationEdit.execute({
273
- action: 'add_keyframe',
274
- node_path: '/root/AnimPlayer',
275
- animation_name: 'walk',
276
- track_index: 0,
277
- time: 0.5,
278
- value: 3,
279
- }, ctx);
280
- expect(mock.calls[0].command).toBe('add_keyframe');
281
- expect(mock.calls[0].params.time).toBe(0.5);
282
- expect(mock.calls[0].params.value).toBe(3);
283
- expect(result).toBe('Added keyframe 0 at 0.5s');
16
+ describe('query actions', () => {
17
+ let mock;
18
+ beforeEach(() => {
19
+ mock = createMockGodot();
20
+ });
21
+ it('list_players sends command and formats empty result', async () => {
22
+ mock.mockResponse({ animation_players: [] });
23
+ const ctx = createToolContext(mock);
24
+ const result = await animation.execute({ action: 'list_players' }, ctx);
25
+ expect(mock.calls[0].command).toBe('list_animation_players');
26
+ expect(result).toBe('No AnimationPlayer nodes found in scene');
27
+ });
28
+ it('list_players formats found players', async () => {
29
+ mock.mockResponse({
30
+ animation_players: [
31
+ { path: '/root/Player/AnimPlayer', name: 'AnimPlayer' },
32
+ { path: '/root/Enemy/AnimPlayer', name: 'AnimPlayer' },
33
+ ],
34
+ });
35
+ const ctx = createToolContext(mock);
36
+ const result = await animation.execute({ action: 'list_players' }, ctx);
37
+ expect(result).toContain('Found 2 AnimationPlayer(s)');
38
+ expect(result).toContain('/root/Player/AnimPlayer');
39
+ });
40
+ it('get_info sends command and returns JSON', async () => {
41
+ const info = { current_animation: 'idle', is_playing: true, current_position: 0.5 };
42
+ mock.mockResponse(info);
43
+ const ctx = createToolContext(mock);
44
+ const result = await animation.execute({
45
+ action: 'get_info',
46
+ node_path: '/root/AnimPlayer',
47
+ }, ctx);
48
+ expect(mock.calls[0].command).toBe('get_animation_player_info');
49
+ expect(mock.calls[0].params.node_path).toBe('/root/AnimPlayer');
50
+ expect(result).toBe(JSON.stringify(info, null, 2));
51
+ });
52
+ it('get_details sends command with animation name', async () => {
53
+ const details = { name: 'walk', length: 1.5, track_count: 3 };
54
+ mock.mockResponse(details);
55
+ const ctx = createToolContext(mock);
56
+ const result = await animation.execute({
57
+ action: 'get_details',
58
+ node_path: '/root/AnimPlayer',
59
+ animation_name: 'walk',
60
+ }, ctx);
61
+ expect(mock.calls[0].command).toBe('get_animation_details');
62
+ expect(mock.calls[0].params.animation_name).toBe('walk');
63
+ expect(result).toBe(JSON.stringify(details, null, 2));
64
+ });
65
+ it('get_keyframes sends command with track index', async () => {
66
+ const keyframes = { track_path: 'Sprite:frame', keyframes: [{ time: 0, value: 0 }] };
67
+ mock.mockResponse(keyframes);
68
+ const ctx = createToolContext(mock);
69
+ const result = await animation.execute({
70
+ action: 'get_keyframes',
71
+ node_path: '/root/AnimPlayer',
72
+ animation_name: 'walk',
73
+ track_index: 0,
74
+ }, ctx);
75
+ expect(mock.calls[0].command).toBe('get_track_keyframes');
76
+ expect(mock.calls[0].params.track_index).toBe(0);
77
+ expect(result).toBe(JSON.stringify(keyframes, null, 2));
78
+ });
79
+ it('throws on error from Godot', async () => {
80
+ mock.mockError(new Error('Node not found'));
81
+ const ctx = createToolContext(mock);
82
+ await expect(animation.execute({
83
+ action: 'get_info',
84
+ node_path: '/root/Missing',
85
+ }, ctx)).rejects.toThrow('Node not found');
86
+ });
284
87
  });
285
- it('remove_keyframe sends command and returns confirmation', async () => {
286
- mock.mockResponse({ removed_keyframe: 1, track_index: 0 });
287
- const ctx = createToolContext(mock);
288
- const result = await animationEdit.execute({
289
- action: 'remove_keyframe',
290
- node_path: '/root/AnimPlayer',
291
- animation_name: 'walk',
292
- track_index: 0,
293
- keyframe_index: 1,
294
- }, ctx);
295
- expect(mock.calls[0].command).toBe('remove_keyframe');
296
- expect(result).toBe('Removed keyframe 1 from track 0');
88
+ describe('playback actions', () => {
89
+ let mock;
90
+ beforeEach(() => {
91
+ mock = createMockGodot();
92
+ });
93
+ it('play sends command and returns confirmation', async () => {
94
+ mock.mockResponse({ playing: 'run', from_position: 0 });
95
+ const ctx = createToolContext(mock);
96
+ const result = await animation.execute({
97
+ action: 'play',
98
+ node_path: '/root/AnimPlayer',
99
+ animation_name: 'run',
100
+ }, ctx);
101
+ expect(mock.calls[0].command).toBe('play_animation');
102
+ expect(mock.calls[0].params.animation_name).toBe('run');
103
+ expect(result).toBe('Playing animation: run');
104
+ });
105
+ it('play passes optional params', async () => {
106
+ mock.mockResponse({ playing: 'walk', from_position: 0 });
107
+ const ctx = createToolContext(mock);
108
+ await animation.execute({
109
+ action: 'play',
110
+ node_path: '/root/AnimPlayer',
111
+ animation_name: 'walk',
112
+ custom_speed: 2.0,
113
+ custom_blend: 0.5,
114
+ from_end: true,
115
+ }, ctx);
116
+ expect(mock.calls[0].params.custom_speed).toBe(2.0);
117
+ expect(mock.calls[0].params.custom_blend).toBe(0.5);
118
+ expect(mock.calls[0].params.from_end).toBe(true);
119
+ });
120
+ it('stop sends command and returns confirmation', async () => {
121
+ mock.mockResponse({});
122
+ const ctx = createToolContext(mock);
123
+ const result = await animation.execute({
124
+ action: 'stop',
125
+ node_path: '/root/AnimPlayer',
126
+ }, ctx);
127
+ expect(mock.calls[0].command).toBe('stop_animation');
128
+ expect(result).toBe('Animation stopped');
129
+ });
130
+ it('pause sends command with paused=true', async () => {
131
+ mock.mockResponse({});
132
+ const ctx = createToolContext(mock);
133
+ const result = await animation.execute({
134
+ action: 'pause',
135
+ node_path: '/root/AnimPlayer',
136
+ paused: true,
137
+ }, ctx);
138
+ expect(mock.calls[0].command).toBe('pause_animation');
139
+ expect(mock.calls[0].params.paused).toBe(true);
140
+ expect(result).toBe('Animation paused');
141
+ });
142
+ it('pause sends command with paused=false', async () => {
143
+ mock.mockResponse({});
144
+ const ctx = createToolContext(mock);
145
+ const result = await animation.execute({
146
+ action: 'pause',
147
+ node_path: '/root/AnimPlayer',
148
+ paused: false,
149
+ }, ctx);
150
+ expect(mock.calls[0].params.paused).toBe(false);
151
+ expect(result).toBe('Animation unpaused');
152
+ });
153
+ it('seek sends command and returns position', async () => {
154
+ mock.mockResponse({ position: 1.5 });
155
+ const ctx = createToolContext(mock);
156
+ const result = await animation.execute({
157
+ action: 'seek',
158
+ node_path: '/root/AnimPlayer',
159
+ seconds: 1.5,
160
+ }, ctx);
161
+ expect(mock.calls[0].command).toBe('seek_animation');
162
+ expect(mock.calls[0].params.seconds).toBe(1.5);
163
+ expect(result).toBe('Seeked to position: 1.5');
164
+ });
165
+ it('queue sends command and returns queue info', async () => {
166
+ mock.mockResponse({ queued: 'attack', queue_length: 3 });
167
+ const ctx = createToolContext(mock);
168
+ const result = await animation.execute({
169
+ action: 'queue',
170
+ node_path: '/root/AnimPlayer',
171
+ animation_name: 'attack',
172
+ }, ctx);
173
+ expect(mock.calls[0].command).toBe('queue_animation');
174
+ expect(result).toBe('Queued animation: attack (queue length: 3)');
175
+ });
176
+ it('clear_queue sends command and returns confirmation', async () => {
177
+ mock.mockResponse({});
178
+ const ctx = createToolContext(mock);
179
+ const result = await animation.execute({
180
+ action: 'clear_queue',
181
+ node_path: '/root/AnimPlayer',
182
+ }, ctx);
183
+ expect(mock.calls[0].command).toBe('clear_animation_queue');
184
+ expect(result).toBe('Animation queue cleared');
185
+ });
297
186
  });
298
- it('update_keyframe sends command and returns changes', async () => {
299
- mock.mockResponse({ updated_keyframe: 0, changes: { time: 0.75, value: 5 } });
300
- const ctx = createToolContext(mock);
301
- const result = await animationEdit.execute({
302
- action: 'update_keyframe',
303
- node_path: '/root/AnimPlayer',
304
- animation_name: 'walk',
305
- track_index: 0,
306
- keyframe_index: 0,
307
- time: 0.75,
308
- value: 5,
309
- }, ctx);
310
- expect(mock.calls[0].command).toBe('update_keyframe');
311
- expect(result).toContain('Updated keyframe 0');
312
- expect(result).toContain('"time":0.75');
187
+ describe('edit actions', () => {
188
+ let mock;
189
+ beforeEach(() => {
190
+ mock = createMockGodot();
191
+ });
192
+ it('create sends command and returns confirmation', async () => {
193
+ mock.mockResponse({ created: 'new_anim', library: '' });
194
+ const ctx = createToolContext(mock);
195
+ const result = await animation.execute({
196
+ action: 'create',
197
+ node_path: '/root/AnimPlayer',
198
+ animation_name: 'new_anim',
199
+ length: 2.0,
200
+ }, ctx);
201
+ expect(mock.calls[0].command).toBe('create_animation');
202
+ expect(mock.calls[0].params.animation_name).toBe('new_anim');
203
+ expect(mock.calls[0].params.length).toBe(2.0);
204
+ expect(result).toBe('Created animation: new_anim');
205
+ });
206
+ it('create includes library in result when provided', async () => {
207
+ mock.mockResponse({ created: 'walk', library: 'movement' });
208
+ const ctx = createToolContext(mock);
209
+ const result = await animation.execute({
210
+ action: 'create',
211
+ node_path: '/root/AnimPlayer',
212
+ animation_name: 'walk',
213
+ library_name: 'movement',
214
+ }, ctx);
215
+ expect(result).toBe('Created animation: walk in library: movement');
216
+ });
217
+ it('delete sends command and returns confirmation', async () => {
218
+ mock.mockResponse({ deleted: 'old_anim' });
219
+ const ctx = createToolContext(mock);
220
+ const result = await animation.execute({
221
+ action: 'delete',
222
+ node_path: '/root/AnimPlayer',
223
+ animation_name: 'old_anim',
224
+ }, ctx);
225
+ expect(mock.calls[0].command).toBe('delete_animation');
226
+ expect(result).toBe('Deleted animation: old_anim');
227
+ });
228
+ it('rename sends command with old and new names', async () => {
229
+ mock.mockResponse({ renamed: { from: 'walk', to: 'walk_slow' } });
230
+ const ctx = createToolContext(mock);
231
+ const result = await animation.execute({
232
+ action: 'rename',
233
+ node_path: '/root/AnimPlayer',
234
+ old_name: 'walk',
235
+ new_name: 'walk_slow',
236
+ }, ctx);
237
+ expect(mock.calls[0].command).toBe('rename_animation');
238
+ expect(mock.calls[0].params.old_name).toBe('walk');
239
+ expect(mock.calls[0].params.new_name).toBe('walk_slow');
240
+ expect(result).toBe('Renamed animation: walk -> walk_slow');
241
+ });
242
+ it('update_props sends command and returns updated properties', async () => {
243
+ mock.mockResponse({ updated: 'walk', properties: { length: 2.0, loop_mode: 'linear' } });
244
+ const ctx = createToolContext(mock);
245
+ const result = await animation.execute({
246
+ action: 'update_props',
247
+ node_path: '/root/AnimPlayer',
248
+ animation_name: 'walk',
249
+ length: 2.0,
250
+ loop_mode: 'linear',
251
+ }, ctx);
252
+ expect(mock.calls[0].command).toBe('update_animation_properties');
253
+ expect(result).toContain('Updated animation: walk');
254
+ expect(result).toContain('"length":2');
255
+ });
256
+ it('add_track sends command and returns track info', async () => {
257
+ mock.mockResponse({ track_index: 0, track_path: 'Sprite2D:frame', track_type: 'value' });
258
+ const ctx = createToolContext(mock);
259
+ const result = await animation.execute({
260
+ action: 'add_track',
261
+ node_path: '/root/AnimPlayer',
262
+ animation_name: 'walk',
263
+ track_type: 'value',
264
+ track_path: 'Sprite2D:frame',
265
+ }, ctx);
266
+ expect(mock.calls[0].command).toBe('add_animation_track');
267
+ expect(result).toBe('Added track 0: value -> Sprite2D:frame');
268
+ });
269
+ it('remove_track sends command and returns confirmation', async () => {
270
+ mock.mockResponse({ removed_track: 2 });
271
+ const ctx = createToolContext(mock);
272
+ const result = await animation.execute({
273
+ action: 'remove_track',
274
+ node_path: '/root/AnimPlayer',
275
+ animation_name: 'walk',
276
+ track_index: 2,
277
+ }, ctx);
278
+ expect(mock.calls[0].command).toBe('remove_animation_track');
279
+ expect(result).toBe('Removed track: 2');
280
+ });
281
+ it('add_keyframe sends command and returns keyframe info', async () => {
282
+ mock.mockResponse({ keyframe_index: 0, time: 0.5, value: 3 });
283
+ const ctx = createToolContext(mock);
284
+ const result = await animation.execute({
285
+ action: 'add_keyframe',
286
+ node_path: '/root/AnimPlayer',
287
+ animation_name: 'walk',
288
+ track_index: 0,
289
+ time: 0.5,
290
+ value: 3,
291
+ }, ctx);
292
+ expect(mock.calls[0].command).toBe('add_keyframe');
293
+ expect(mock.calls[0].params.time).toBe(0.5);
294
+ expect(mock.calls[0].params.value).toBe(3);
295
+ expect(result).toBe('Added keyframe 0 at 0.5s');
296
+ });
297
+ it('remove_keyframe sends command and returns confirmation', async () => {
298
+ mock.mockResponse({ removed_keyframe: 1, track_index: 0 });
299
+ const ctx = createToolContext(mock);
300
+ const result = await animation.execute({
301
+ action: 'remove_keyframe',
302
+ node_path: '/root/AnimPlayer',
303
+ animation_name: 'walk',
304
+ track_index: 0,
305
+ keyframe_index: 1,
306
+ }, ctx);
307
+ expect(mock.calls[0].command).toBe('remove_keyframe');
308
+ expect(result).toBe('Removed keyframe 1 from track 0');
309
+ });
310
+ it('update_keyframe sends command and returns changes', async () => {
311
+ mock.mockResponse({ updated_keyframe: 0, changes: { time: 0.75, value: 5 } });
312
+ const ctx = createToolContext(mock);
313
+ const result = await animation.execute({
314
+ action: 'update_keyframe',
315
+ node_path: '/root/AnimPlayer',
316
+ animation_name: 'walk',
317
+ track_index: 0,
318
+ keyframe_index: 0,
319
+ time: 0.75,
320
+ value: 5,
321
+ }, ctx);
322
+ expect(mock.calls[0].command).toBe('update_keyframe');
323
+ expect(result).toContain('Updated keyframe 0');
324
+ expect(result).toContain('"time":0.75');
325
+ });
313
326
  });
314
327
  });
315
328
  //# sourceMappingURL=animation.test.js.map