@framers/agentos-ext-cloud-flyio 0.1.0 → 0.1.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.
@@ -1,407 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { FlyCreateVolumeTool } from '../src/tools/createVolume';
4
- import { FlyDeployAppTool } from '../src/tools/deployApp';
5
- import { FlyListAppsTool } from '../src/tools/listApps';
6
- import { FlyScaleAppTool } from '../src/tools/scaleApp';
7
-
8
- // ---------------------------------------------------------------------------
9
- // FlyCreateVolumeTool
10
- // ---------------------------------------------------------------------------
11
-
12
- describe('FlyCreateVolumeTool', () => {
13
- let service: any;
14
- let tool: FlyCreateVolumeTool;
15
-
16
- beforeEach(() => {
17
- service = {
18
- createVolume: vi.fn(),
19
- };
20
- tool = new FlyCreateVolumeTool(service);
21
- });
22
-
23
- it('should have correct metadata', () => {
24
- expect(tool.id).toBe('flyCreateVolume');
25
- expect(tool.name).toBe('flyCreateVolume');
26
- expect(tool.displayName).toBe('Create Fly.io Volume');
27
- expect(tool.inputSchema.type).toBe('object');
28
- expect(tool.inputSchema.required).toEqual(
29
- expect.arrayContaining(['appName', 'name', 'region', 'sizeGb']),
30
- );
31
- expect(tool.category).toBe('cloud');
32
- });
33
-
34
- it('should create a volume successfully', async () => {
35
- service.createVolume.mockResolvedValue({
36
- id: 'vol_abc123',
37
- name: 'data',
38
- region: 'iad',
39
- sizeGb: 10,
40
- encrypted: true,
41
- state: 'created',
42
- });
43
-
44
- const result = await tool.execute({
45
- appName: 'my-app',
46
- name: 'data',
47
- region: 'iad',
48
- sizeGb: 10,
49
- });
50
-
51
- expect(result.success).toBe(true);
52
- expect(result.data).toHaveProperty('id', 'vol_abc123');
53
- expect(result.data).toHaveProperty('sizeGb', 10);
54
- expect(service.createVolume).toHaveBeenCalledWith('my-app', {
55
- name: 'data',
56
- region: 'iad',
57
- sizeGb: 10,
58
- encrypted: undefined,
59
- });
60
- });
61
-
62
- it('should pass encrypted flag', async () => {
63
- service.createVolume.mockResolvedValue({ id: 'vol_1', encrypted: false });
64
-
65
- await tool.execute({
66
- appName: 'my-app',
67
- name: 'data',
68
- region: 'lax',
69
- sizeGb: 5,
70
- encrypted: false,
71
- });
72
-
73
- expect(service.createVolume).toHaveBeenCalledWith('my-app', expect.objectContaining({
74
- encrypted: false,
75
- }));
76
- });
77
-
78
- it('should handle errors gracefully', async () => {
79
- service.createVolume.mockRejectedValue(new Error('Insufficient capacity'));
80
-
81
- const result = await tool.execute({
82
- appName: 'my-app',
83
- name: 'data',
84
- region: 'iad',
85
- sizeGb: 500,
86
- });
87
-
88
- expect(result.success).toBe(false);
89
- expect(result.error).toContain('Insufficient capacity');
90
- });
91
- });
92
-
93
- // ---------------------------------------------------------------------------
94
- // FlyDeployAppTool
95
- // ---------------------------------------------------------------------------
96
-
97
- describe('FlyDeployAppTool', () => {
98
- let service: any;
99
- let tool: FlyDeployAppTool;
100
-
101
- beforeEach(() => {
102
- service = {
103
- getApp: vi.fn(),
104
- createApp: vi.fn(),
105
- createMachine: vi.fn(),
106
- };
107
- tool = new FlyDeployAppTool(service);
108
- });
109
-
110
- it('should have correct metadata', () => {
111
- expect(tool.id).toBe('flyDeployApp');
112
- expect(tool.name).toBe('flyDeployApp');
113
- expect(tool.inputSchema.type).toBe('object');
114
- expect(tool.inputSchema.required).toContain('appName');
115
- expect(tool.inputSchema.required).toContain('image');
116
- });
117
-
118
- it('should deploy to an existing app', async () => {
119
- service.getApp.mockResolvedValue({
120
- name: 'my-app',
121
- status: 'deployed',
122
- organization: { slug: 'personal' },
123
- });
124
- service.createMachine.mockResolvedValue({
125
- id: 'machine-1',
126
- state: 'started',
127
- region: 'iad',
128
- });
129
-
130
- const result = await tool.execute({
131
- appName: 'my-app',
132
- image: 'nginx:alpine',
133
- });
134
-
135
- expect(result.success).toBe(true);
136
- expect(result.data?.app.name).toBe('my-app');
137
- expect(result.data?.machine.id).toBe('machine-1');
138
- expect(service.getApp).toHaveBeenCalledWith('my-app');
139
- expect(service.createApp).not.toHaveBeenCalled();
140
- });
141
-
142
- it('should create app if it does not exist', async () => {
143
- service.getApp.mockRejectedValue(new Error('Not found'));
144
- service.createApp.mockResolvedValue({ name: 'new-app', status: 'pending' });
145
- service.createMachine.mockResolvedValue({ id: 'machine-2', state: 'started' });
146
-
147
- const result = await tool.execute({
148
- appName: 'new-app',
149
- image: 'node:20-alpine',
150
- org: 'my-org',
151
- });
152
-
153
- expect(result.success).toBe(true);
154
- expect(service.createApp).toHaveBeenCalledWith('new-app', 'my-org');
155
- });
156
-
157
- it('should fail if app does not exist and createApp is false', async () => {
158
- service.getApp.mockRejectedValue(new Error('Not found'));
159
-
160
- const result = await tool.execute({
161
- appName: 'nonexistent',
162
- image: 'nginx:alpine',
163
- createApp: false,
164
- });
165
-
166
- expect(result.success).toBe(false);
167
- expect(result.error).toContain('not found');
168
- });
169
-
170
- it('should configure machine with custom resources', async () => {
171
- service.getApp.mockResolvedValue({ name: 'my-app' });
172
- service.createMachine.mockResolvedValue({ id: 'machine-3' });
173
-
174
- await tool.execute({
175
- appName: 'my-app',
176
- image: 'nginx:alpine',
177
- cpus: 2,
178
- cpuKind: 'performance',
179
- memoryMb: 512,
180
- internalPort: 3000,
181
- env: { NODE_ENV: 'production' },
182
- region: 'cdg',
183
- });
184
-
185
- expect(service.createMachine).toHaveBeenCalledWith(
186
- 'my-app',
187
- expect.objectContaining({
188
- image: 'nginx:alpine',
189
- env: { NODE_ENV: 'production' },
190
- guest: expect.objectContaining({
191
- cpus: 2,
192
- cpu_kind: 'performance',
193
- memory_mb: 512,
194
- }),
195
- }),
196
- { region: 'cdg' },
197
- );
198
- });
199
-
200
- it('should handle errors gracefully', async () => {
201
- service.getApp.mockResolvedValue({ name: 'my-app' });
202
- service.createMachine.mockRejectedValue(new Error('Image not found'));
203
-
204
- const result = await tool.execute({
205
- appName: 'my-app',
206
- image: 'nonexistent:latest',
207
- });
208
-
209
- expect(result.success).toBe(false);
210
- expect(result.error).toContain('Image not found');
211
- });
212
- });
213
-
214
- // ---------------------------------------------------------------------------
215
- // FlyListAppsTool
216
- // ---------------------------------------------------------------------------
217
-
218
- describe('FlyListAppsTool', () => {
219
- let service: any;
220
- let tool: FlyListAppsTool;
221
-
222
- beforeEach(() => {
223
- service = {
224
- listApps: vi.fn(),
225
- getApp: vi.fn(),
226
- listMachines: vi.fn(),
227
- };
228
- tool = new FlyListAppsTool(service);
229
- });
230
-
231
- it('should have correct metadata', () => {
232
- expect(tool.id).toBe('flyListApps');
233
- expect(tool.name).toBe('flyListApps');
234
- expect(tool.inputSchema.type).toBe('object');
235
- expect(tool.inputSchema.required).toEqual([]);
236
- expect(tool.hasSideEffects).toBe(false);
237
- });
238
-
239
- it('should list all apps with machines', async () => {
240
- service.listApps.mockResolvedValue([
241
- { name: 'app-1', status: 'deployed', machines: undefined },
242
- { name: 'app-2', status: 'deployed', machines: undefined },
243
- ]);
244
- service.listMachines.mockResolvedValue([
245
- { id: 'm-1', state: 'started', region: 'iad' },
246
- ]);
247
-
248
- const result = await tool.execute({});
249
-
250
- expect(result.success).toBe(true);
251
- expect(result.data).toHaveLength(2);
252
- expect(service.listMachines).toHaveBeenCalledTimes(2);
253
- });
254
-
255
- it('should get a single app by name', async () => {
256
- service.getApp.mockResolvedValue({ name: 'my-app', status: 'deployed' });
257
- service.listMachines.mockResolvedValue([{ id: 'm-1' }]);
258
-
259
- const result = await tool.execute({ appName: 'my-app' });
260
-
261
- expect(result.success).toBe(true);
262
- expect(result.data).toHaveLength(1);
263
- expect(result.data![0].name).toBe('my-app');
264
- expect(service.getApp).toHaveBeenCalledWith('my-app');
265
- });
266
-
267
- it('should skip machines when includeMachines is false', async () => {
268
- service.listApps.mockResolvedValue([{ name: 'app-1' }]);
269
-
270
- const result = await tool.execute({ includeMachines: false });
271
-
272
- expect(result.success).toBe(true);
273
- expect(service.listMachines).not.toHaveBeenCalled();
274
- });
275
-
276
- it('should handle machine listing errors gracefully per-app', async () => {
277
- service.listApps.mockResolvedValue([
278
- { name: 'app-1', machines: undefined },
279
- ]);
280
- service.listMachines.mockRejectedValue(new Error('Forbidden'));
281
-
282
- const result = await tool.execute({});
283
-
284
- expect(result.success).toBe(true);
285
- expect(result.data![0].machines).toEqual([]);
286
- });
287
-
288
- it('should handle errors gracefully', async () => {
289
- service.listApps.mockRejectedValue(new Error('Unauthorized'));
290
-
291
- const result = await tool.execute({});
292
-
293
- expect(result.success).toBe(false);
294
- expect(result.error).toContain('Unauthorized');
295
- });
296
- });
297
-
298
- // ---------------------------------------------------------------------------
299
- // FlyScaleAppTool
300
- // ---------------------------------------------------------------------------
301
-
302
- describe('FlyScaleAppTool', () => {
303
- let service: any;
304
- let tool: FlyScaleAppTool;
305
-
306
- beforeEach(() => {
307
- service = {
308
- listMachines: vi.fn(),
309
- createMachine: vi.fn(),
310
- destroyMachine: vi.fn(),
311
- updateMachine: vi.fn(),
312
- };
313
- tool = new FlyScaleAppTool(service);
314
- });
315
-
316
- it('should have correct metadata', () => {
317
- expect(tool.id).toBe('flyScaleApp');
318
- expect(tool.name).toBe('flyScaleApp');
319
- expect(tool.inputSchema.type).toBe('object');
320
- expect(tool.inputSchema.required).toContain('appName');
321
- expect(tool.inputSchema.required).toContain('count');
322
- });
323
-
324
- it('should scale up by creating new machines', async () => {
325
- service.listMachines
326
- .mockResolvedValueOnce([
327
- { id: 'm-1', config: { image: 'nginx', guest: { cpus: 1, cpu_kind: 'shared', memory_mb: 256 } } },
328
- ])
329
- .mockResolvedValueOnce([
330
- { id: 'm-1', config: {} },
331
- { id: 'm-2', config: {} },
332
- { id: 'm-3', config: {} },
333
- ]);
334
- service.createMachine.mockResolvedValue({ id: 'm-new' });
335
-
336
- const result = await tool.execute({ appName: 'my-app', count: 3 });
337
-
338
- expect(result.success).toBe(true);
339
- expect(result.data?.created).toBe(2);
340
- expect(result.data?.destroyed).toBe(0);
341
- expect(service.createMachine).toHaveBeenCalledTimes(2);
342
- });
343
-
344
- it('should scale down by destroying excess machines', async () => {
345
- service.listMachines
346
- .mockResolvedValueOnce([
347
- { id: 'm-1', config: {} },
348
- { id: 'm-2', config: {} },
349
- { id: 'm-3', config: {} },
350
- ])
351
- .mockResolvedValueOnce([{ id: 'm-1', config: {} }]);
352
- service.destroyMachine.mockResolvedValue(undefined);
353
-
354
- const result = await tool.execute({ appName: 'my-app', count: 1 });
355
-
356
- expect(result.success).toBe(true);
357
- expect(result.data?.destroyed).toBe(2);
358
- expect(service.destroyMachine).toHaveBeenCalledTimes(2);
359
- });
360
-
361
- it('should resize existing machines when resource params change', async () => {
362
- service.listMachines
363
- .mockResolvedValueOnce([
364
- { id: 'm-1', config: { guest: { cpus: 1, cpu_kind: 'shared', memory_mb: 256 } } },
365
- ])
366
- .mockResolvedValueOnce([{ id: 'm-1' }]);
367
- service.updateMachine.mockResolvedValue({});
368
-
369
- const result = await tool.execute({
370
- appName: 'my-app',
371
- count: 1,
372
- cpus: 2,
373
- memoryMb: 512,
374
- });
375
-
376
- expect(result.success).toBe(true);
377
- expect(result.data?.resized).toBe(1);
378
- expect(service.updateMachine).toHaveBeenCalledWith('my-app', 'm-1', {
379
- guest: expect.objectContaining({ cpus: 2, memory_mb: 512 }),
380
- });
381
- });
382
-
383
- it('should scale to zero by destroying all machines', async () => {
384
- service.listMachines
385
- .mockResolvedValueOnce([
386
- { id: 'm-1', config: {} },
387
- { id: 'm-2', config: {} },
388
- ])
389
- .mockResolvedValueOnce([]);
390
- service.destroyMachine.mockResolvedValue(undefined);
391
-
392
- const result = await tool.execute({ appName: 'my-app', count: 0 });
393
-
394
- expect(result.success).toBe(true);
395
- expect(result.data?.destroyed).toBe(2);
396
- expect(result.data?.machines).toEqual([]);
397
- });
398
-
399
- it('should handle errors gracefully', async () => {
400
- service.listMachines.mockRejectedValue(new Error('App not found'));
401
-
402
- const result = await tool.execute({ appName: 'nonexistent', count: 2 });
403
-
404
- expect(result.success).toBe(false);
405
- expect(result.error).toContain('App not found');
406
- });
407
- });
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "lib": ["ES2022"],
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "noEmitOnError": false,
12
- "forceConsistentCasingInFileNames": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true,
16
- "resolveJsonModule": true,
17
- "moduleResolution": "Bundler",
18
- "types": ["node"]
19
- },
20
- "include": ["src/**/*"],
21
- "exclude": ["node_modules", "dist", "test"]
22
- }