@framers/agentos-ext-cloud-railway 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,89 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview RailwayDeployServiceTool — deploy a service on Railway from a Git repo.
4
- */
5
-
6
- import type { RailwayService, RailwayDeployment, RailwayService_ } from '../RailwayService.js';
7
-
8
- export interface RailwayDeployServiceInput {
9
- projectId?: string;
10
- projectName?: string;
11
- repo?: string;
12
- serviceName?: string;
13
- serviceId?: string;
14
- environmentId?: string;
15
- variables?: Record<string, string>;
16
- }
17
-
18
- export class RailwayDeployServiceTool {
19
- readonly id = 'railwayDeployService';
20
- readonly name = 'railwayDeployService';
21
- readonly displayName = 'Deploy Railway Service';
22
- readonly description = 'Deploy a service on Railway. Either create a new service from a GitHub repo in a project, or trigger a redeployment of an existing service. Can set environment variables before deploying.';
23
- readonly category = 'cloud';
24
- readonly version = '0.1.0';
25
- readonly hasSideEffects = true;
26
-
27
- readonly inputSchema = {
28
- type: 'object' as const,
29
- properties: {
30
- projectId: { type: 'string', description: 'Railway project ID to deploy into. If omitted and projectName is given, a new project is created.' },
31
- projectName: { type: 'string', description: 'Name for a new project (used only when projectId is omitted)' },
32
- repo: { type: 'string', description: 'GitHub repository to deploy from (e.g. "user/repo"). Required for new services.' },
33
- serviceName: { type: 'string', description: 'Name for the service (optional)' },
34
- serviceId: { type: 'string', description: 'Existing service ID to redeploy (triggers rebuild of latest commit)' },
35
- environmentId: { type: 'string', description: 'Environment ID for redeployment (required with serviceId, defaults to production)' },
36
- variables: { type: 'object', description: 'Environment variables to set (key-value pairs)' },
37
- },
38
- required: [] as string[],
39
- };
40
-
41
- constructor(private service: RailwayService) {}
42
-
43
- async execute(args: RailwayDeployServiceInput): Promise<{ success: boolean; data?: { service?: RailwayService_; deployment?: RailwayDeployment; projectId: string }; error?: string }> {
44
- try {
45
- // Case 1: Redeploy existing service
46
- if (args.serviceId && args.environmentId) {
47
- // Set variables if provided
48
- if (args.variables && Object.keys(args.variables).length > 0) {
49
- await this.service.upsertVariables(args.serviceId, args.environmentId, args.variables);
50
- }
51
-
52
- const deployment = await this.service.redeployService(args.serviceId, args.environmentId);
53
- return {
54
- success: true,
55
- data: {
56
- deployment,
57
- projectId: args.projectId ?? '',
58
- },
59
- };
60
- }
61
-
62
- // Case 2: Create new service (and project if needed)
63
- let projectId = args.projectId;
64
-
65
- if (!projectId) {
66
- const project = await this.service.createProject(
67
- args.projectName ?? `project-${Date.now()}`,
68
- );
69
- projectId = project.id;
70
- }
71
-
72
- const svc = await this.service.createService(projectId, {
73
- name: args.serviceName,
74
- source: args.repo ? { repo: args.repo } : undefined,
75
- variables: args.variables,
76
- });
77
-
78
- return {
79
- success: true,
80
- data: {
81
- service: svc,
82
- projectId,
83
- },
84
- };
85
- } catch (err: any) {
86
- return { success: false, error: err.message };
87
- }
88
- }
89
- }
@@ -1,50 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview RailwayGetLogsTool — retrieve deployment or build logs from Railway.
4
- */
5
-
6
- import type { RailwayService, RailwayLogEntry } from '../RailwayService.js';
7
-
8
- export interface RailwayGetLogsInput {
9
- deploymentId: string;
10
- type?: 'deploy' | 'build';
11
- }
12
-
13
- export class RailwayGetLogsTool {
14
- readonly id = 'railwayGetLogs';
15
- readonly name = 'railwayGetLogs';
16
- readonly displayName = 'Get Railway Logs';
17
- readonly description = 'Retrieve logs for a Railway deployment. Can fetch either deployment (runtime) logs or build logs. Returns timestamped log entries with severity levels.';
18
- readonly category = 'cloud';
19
- readonly version = '0.1.0';
20
- readonly hasSideEffects = false;
21
-
22
- readonly inputSchema = {
23
- type: 'object' as const,
24
- properties: {
25
- deploymentId: { type: 'string', description: 'Deployment ID to retrieve logs for' },
26
- type: {
27
- type: 'string',
28
- enum: ['deploy', 'build'],
29
- description: 'Type of logs to retrieve: "deploy" for runtime logs, "build" for build logs (default: deploy)',
30
- },
31
- },
32
- required: ['deploymentId'],
33
- };
34
-
35
- constructor(private service: RailwayService) {}
36
-
37
- async execute(args: RailwayGetLogsInput): Promise<{ success: boolean; data?: RailwayLogEntry[]; error?: string }> {
38
- try {
39
- const logType = args.type ?? 'deploy';
40
-
41
- const logs = logType === 'build'
42
- ? await this.service.getBuildLogs(args.deploymentId)
43
- : await this.service.getDeploymentLogs(args.deploymentId);
44
-
45
- return { success: true, data: logs };
46
- } catch (err: any) {
47
- return { success: false, error: err.message };
48
- }
49
- }
50
- }
@@ -1,44 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview RailwayListServicesTool — list all projects and services on Railway.
4
- */
5
-
6
- import type { RailwayService, RailwayProject } from '../RailwayService.js';
7
-
8
- export interface RailwayListServicesInput {
9
- projectId?: string;
10
- }
11
-
12
- export class RailwayListServicesTool {
13
- readonly id = 'railwayListServices';
14
- readonly name = 'railwayListServices';
15
- readonly displayName = 'List Railway Services';
16
- readonly description = 'List all Railway projects and their services. Optionally filter to a single project by ID. Shows project name, environments, and all services with their status.';
17
- readonly category = 'cloud';
18
- readonly version = '0.1.0';
19
- readonly hasSideEffects = false;
20
-
21
- readonly inputSchema = {
22
- type: 'object' as const,
23
- properties: {
24
- projectId: { type: 'string', description: 'Filter to a specific project ID (optional — lists all projects if omitted)' },
25
- },
26
- required: [] as string[],
27
- };
28
-
29
- constructor(private service: RailwayService) {}
30
-
31
- async execute(args: RailwayListServicesInput): Promise<{ success: boolean; data?: RailwayProject[]; error?: string }> {
32
- try {
33
- if (args.projectId) {
34
- const project = await this.service.getProject(args.projectId);
35
- return { success: true, data: [project] };
36
- }
37
-
38
- const projects = await this.service.listProjects();
39
- return { success: true, data: projects };
40
- } catch (err: any) {
41
- return { success: false, error: err.message };
42
- }
43
- }
44
- }
@@ -1,439 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
- import { RailwayService } from '../src/RailwayService';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Helpers
7
- // ---------------------------------------------------------------------------
8
-
9
- function mockResponse(data: unknown, status = 200): Response {
10
- const body = typeof data === 'string' ? data : JSON.stringify(data);
11
- return {
12
- ok: status >= 200 && status < 300,
13
- status,
14
- statusText: status === 200 ? 'OK' : 'Error',
15
- headers: new Headers(),
16
- text: vi.fn().mockResolvedValue(body),
17
- json: vi.fn().mockResolvedValue(typeof data === 'string' ? {} : data),
18
- clone: vi.fn(),
19
- } as unknown as Response;
20
- }
21
-
22
- function gqlResponse(data: unknown): Response {
23
- return mockResponse({ data });
24
- }
25
-
26
- function gqlErrorResponse(messages: string[]): Response {
27
- return mockResponse({
28
- errors: messages.map((m) => ({ message: m })),
29
- });
30
- }
31
-
32
- // ---------------------------------------------------------------------------
33
- // Tests
34
- // ---------------------------------------------------------------------------
35
-
36
- describe('RailwayService', () => {
37
- let service: RailwayService;
38
- let mockFetch: ReturnType<typeof vi.fn>;
39
-
40
- beforeEach(() => {
41
- mockFetch = vi.fn();
42
- vi.stubGlobal('fetch', mockFetch);
43
- service = new RailwayService({
44
- token: 'railway-test-token-abc',
45
- baseUrl: 'https://backboard.railway.com/graphql/v2',
46
- });
47
- });
48
-
49
- afterEach(() => {
50
- vi.restoreAllMocks();
51
- });
52
-
53
- // ── Initialize / Shutdown ───────────────────────────────────────────────
54
-
55
- describe('initialize', () => {
56
- it('should succeed when me query returns a user id', async () => {
57
- mockFetch.mockResolvedValueOnce(gqlResponse({ me: { id: 'user-1', name: 'Test', email: 'test@test.com' } }));
58
- await service.initialize();
59
- expect(service.isRunning).toBe(true);
60
- });
61
-
62
- it('should throw when me query returns no id', async () => {
63
- mockFetch.mockResolvedValueOnce(gqlResponse({ me: { id: null } }));
64
- await expect(service.initialize()).rejects.toThrow('Railway auth failed: could not retrieve user');
65
- expect(service.isRunning).toBe(false);
66
- });
67
-
68
- it('should throw when API returns non-OK status', async () => {
69
- mockFetch.mockResolvedValueOnce(mockResponse('Unauthorized', 401));
70
- await expect(service.initialize()).rejects.toThrow('Railway API 401');
71
- });
72
-
73
- it('should throw on GraphQL errors', async () => {
74
- mockFetch.mockResolvedValueOnce(gqlErrorResponse(['Invalid token']));
75
- await expect(service.initialize()).rejects.toThrow('Railway GraphQL: Invalid token');
76
- });
77
- });
78
-
79
- describe('shutdown', () => {
80
- it('should set isRunning to false', async () => {
81
- mockFetch.mockResolvedValueOnce(gqlResponse({ me: { id: 'u1' } }));
82
- await service.initialize();
83
- await service.shutdown();
84
- expect(service.isRunning).toBe(false);
85
- });
86
- });
87
-
88
- // ── Projects ────────────────────────────────────────────────────────────
89
-
90
- describe('listProjects', () => {
91
- it('should return mapped projects with environments and services', async () => {
92
- mockFetch.mockResolvedValueOnce(gqlResponse({
93
- projects: {
94
- edges: [
95
- {
96
- node: {
97
- id: 'proj-1',
98
- name: 'my-project',
99
- description: 'A test project',
100
- createdAt: '2024-01-01',
101
- updatedAt: '2024-01-02',
102
- environments: {
103
- edges: [
104
- { node: { id: 'env-prod', name: 'production', isEphemeral: false } },
105
- { node: { id: 'env-pr', name: 'pr-123', isEphemeral: true } },
106
- ],
107
- },
108
- services: {
109
- edges: [
110
- { node: { id: 'svc-1', name: 'web', projectId: 'proj-1', createdAt: '', updatedAt: '' } },
111
- ],
112
- },
113
- },
114
- },
115
- ],
116
- },
117
- }));
118
- const projects = await service.listProjects();
119
- expect(projects).toHaveLength(1);
120
- expect(projects[0].name).toBe('my-project');
121
- expect(projects[0].environments).toHaveLength(2);
122
- expect(projects[0].environments[0].name).toBe('production');
123
- expect(projects[0].environments[1].isEphemeral).toBe(true);
124
- expect(projects[0].services).toHaveLength(1);
125
- expect(projects[0].services[0].name).toBe('web');
126
- });
127
-
128
- it('should return empty array when no projects exist', async () => {
129
- mockFetch.mockResolvedValueOnce(gqlResponse({ projects: { edges: [] } }));
130
- const projects = await service.listProjects();
131
- expect(projects).toHaveLength(0);
132
- });
133
- });
134
-
135
- describe('getProject', () => {
136
- it('should return a single project by ID', async () => {
137
- mockFetch.mockResolvedValueOnce(gqlResponse({
138
- project: {
139
- id: 'proj-1',
140
- name: 'my-project',
141
- description: 'desc',
142
- createdAt: '2024-01-01',
143
- updatedAt: '2024-01-02',
144
- environments: { edges: [] },
145
- services: { edges: [] },
146
- },
147
- }));
148
- const project = await service.getProject('proj-1');
149
- expect(project.id).toBe('proj-1');
150
- expect(project.name).toBe('my-project');
151
- expect(project.environments).toHaveLength(0);
152
- });
153
-
154
- it('should throw on GraphQL errors', async () => {
155
- mockFetch.mockResolvedValueOnce(gqlErrorResponse(['Project not found']));
156
- await expect(service.getProject('bad-id')).rejects.toThrow('Railway GraphQL: Project not found');
157
- });
158
- });
159
-
160
- describe('createProject', () => {
161
- it('should create a project and return it', async () => {
162
- mockFetch.mockResolvedValueOnce(gqlResponse({
163
- projectCreate: {
164
- id: 'proj-new',
165
- name: 'new-project',
166
- description: 'Fresh project',
167
- createdAt: '2024-06-01',
168
- updatedAt: '2024-06-01',
169
- environments: {
170
- edges: [{ node: { id: 'env-prod', name: 'production', isEphemeral: false } }],
171
- },
172
- services: { edges: [] },
173
- },
174
- }));
175
- const project = await service.createProject('new-project', 'Fresh project');
176
- expect(project.id).toBe('proj-new');
177
- expect(project.name).toBe('new-project');
178
- expect(project.description).toBe('Fresh project');
179
- expect(project.environments).toHaveLength(1);
180
- });
181
- });
182
-
183
- // ── Services ────────────────────────────────────────────────────────────
184
-
185
- describe('createService', () => {
186
- it('should create a service and return it', async () => {
187
- mockFetch.mockResolvedValueOnce(gqlResponse({
188
- serviceCreate: {
189
- id: 'svc-new',
190
- name: 'api-server',
191
- projectId: 'proj-1',
192
- createdAt: '2024-06-01',
193
- updatedAt: '2024-06-01',
194
- },
195
- }));
196
- const svc = await service.createService('proj-1', {
197
- name: 'api-server',
198
- source: { repo: 'user/repo' },
199
- });
200
- expect(svc.id).toBe('svc-new');
201
- expect(svc.name).toBe('api-server');
202
- expect(svc.projectId).toBe('proj-1');
203
- });
204
-
205
- it('should set variables when provided by fetching the project', async () => {
206
- // serviceCreate
207
- mockFetch.mockResolvedValueOnce(gqlResponse({
208
- serviceCreate: {
209
- id: 'svc-new',
210
- name: 'web',
211
- projectId: 'proj-1',
212
- createdAt: '',
213
- updatedAt: '',
214
- },
215
- }));
216
- // getProject (to find prod env)
217
- mockFetch.mockResolvedValueOnce(gqlResponse({
218
- project: {
219
- id: 'proj-1',
220
- name: 'my-project',
221
- description: '',
222
- createdAt: '',
223
- updatedAt: '',
224
- environments: {
225
- edges: [{ node: { id: 'env-prod', name: 'production', isEphemeral: false } }],
226
- },
227
- services: { edges: [] },
228
- },
229
- }));
230
- // upsertVariables
231
- mockFetch.mockResolvedValueOnce(gqlResponse({ variableCollectionUpsert: true }));
232
-
233
- const svc = await service.createService('proj-1', {
234
- name: 'web',
235
- variables: { NODE_ENV: 'production', PORT: '3000' },
236
- });
237
- expect(svc.name).toBe('web');
238
- expect(mockFetch).toHaveBeenCalledTimes(3);
239
- });
240
- });
241
-
242
- describe('redeployService', () => {
243
- it('should trigger redeploy and fetch latest deployment', async () => {
244
- // redeploy mutation
245
- mockFetch.mockResolvedValueOnce(gqlResponse({ serviceInstanceRedeploy: true }));
246
- // getLatestDeployment query
247
- mockFetch.mockResolvedValueOnce(gqlResponse({
248
- deployments: {
249
- edges: [
250
- {
251
- node: {
252
- id: 'dep-abc',
253
- serviceId: 'svc-1',
254
- environmentId: 'env-prod',
255
- status: 'DEPLOYING',
256
- createdAt: '2024-06-01',
257
- staticUrl: 'my-app.up.railway.app',
258
- },
259
- },
260
- ],
261
- },
262
- }));
263
- const dep = await service.redeployService('svc-1', 'env-prod');
264
- expect(dep.id).toBe('dep-abc');
265
- expect(dep.status).toBe('DEPLOYING');
266
- expect(dep.staticUrl).toBe('my-app.up.railway.app');
267
- });
268
- });
269
-
270
- describe('upsertVariables', () => {
271
- it('should return true on success', async () => {
272
- mockFetch.mockResolvedValueOnce(gqlResponse({ variableCollectionUpsert: true }));
273
- const result = await service.upsertVariables('svc-1', 'env-prod', { KEY: 'value' });
274
- expect(result).toBe(true);
275
- });
276
-
277
- it('should return false when mutation returns false', async () => {
278
- mockFetch.mockResolvedValueOnce(gqlResponse({ variableCollectionUpsert: false }));
279
- const result = await service.upsertVariables('svc-1', 'env-prod', { KEY: 'value' });
280
- expect(result).toBe(false);
281
- });
282
- });
283
-
284
- // ── Plugins (Databases) ────────────────────────────────────────────────
285
-
286
- describe('createPlugin', () => {
287
- it('should create a database plugin', async () => {
288
- mockFetch.mockResolvedValueOnce(gqlResponse({
289
- pluginCreate: {
290
- id: 'plugin-pg',
291
- name: 'postgresql',
292
- status: 'provisioning',
293
- friendlyName: 'PostgreSQL',
294
- },
295
- }));
296
- const plugin = await service.createPlugin('proj-1', 'postgresql');
297
- expect(plugin.id).toBe('plugin-pg');
298
- expect(plugin.name).toBe('postgresql');
299
- expect(plugin.status).toBe('provisioning');
300
- expect(plugin.friendlyName).toBe('PostgreSQL');
301
- });
302
-
303
- it('should use plugin type as fallback for missing fields', async () => {
304
- mockFetch.mockResolvedValueOnce(gqlResponse({
305
- pluginCreate: { id: 'plugin-redis' },
306
- }));
307
- const plugin = await service.createPlugin('proj-1', 'redis');
308
- expect(plugin.name).toBe('redis');
309
- expect(plugin.friendlyName).toBe('redis');
310
- expect(plugin.status).toBe('provisioning');
311
- });
312
- });
313
-
314
- // ── Deployments ─────────────────────────────────────────────────────────
315
-
316
- describe('getLatestDeployment', () => {
317
- it('should return the latest deployment', async () => {
318
- mockFetch.mockResolvedValueOnce(gqlResponse({
319
- deployments: {
320
- edges: [
321
- {
322
- node: {
323
- id: 'dep-1',
324
- serviceId: 'svc-1',
325
- environmentId: 'env-prod',
326
- status: 'SUCCESS',
327
- createdAt: '2024-06-01',
328
- staticUrl: 'app.up.railway.app',
329
- },
330
- },
331
- ],
332
- },
333
- }));
334
- const dep = await service.getLatestDeployment('svc-1', 'env-prod');
335
- expect(dep.id).toBe('dep-1');
336
- expect(dep.status).toBe('SUCCESS');
337
- });
338
-
339
- it('should return a placeholder when no deployments exist', async () => {
340
- mockFetch.mockResolvedValueOnce(gqlResponse({ deployments: { edges: [] } }));
341
- const dep = await service.getLatestDeployment('svc-1', 'env-prod');
342
- expect(dep.id).toBe('');
343
- expect(dep.status).toBe('unknown');
344
- expect(dep.serviceId).toBe('svc-1');
345
- expect(dep.environmentId).toBe('env-prod');
346
- });
347
- });
348
-
349
- // ── Logs ────────────────────────────────────────────────────────────────
350
-
351
- describe('getDeploymentLogs', () => {
352
- it('should return deployment log entries', async () => {
353
- mockFetch.mockResolvedValueOnce(gqlResponse({
354
- deploymentLogs: [
355
- { timestamp: '2024-06-01T00:00:00Z', message: 'Server started', severity: 'info' },
356
- { timestamp: '2024-06-01T00:00:01Z', message: 'Listening on port 3000', severity: 'info' },
357
- ],
358
- }));
359
- const logs = await service.getDeploymentLogs('dep-1');
360
- expect(logs).toHaveLength(2);
361
- expect(logs[0].message).toBe('Server started');
362
- expect(logs[1].severity).toBe('info');
363
- });
364
-
365
- it('should return empty array when no logs exist', async () => {
366
- mockFetch.mockResolvedValueOnce(gqlResponse({ deploymentLogs: [] }));
367
- const logs = await service.getDeploymentLogs('dep-1');
368
- expect(logs).toHaveLength(0);
369
- });
370
-
371
- it('should default severity to info when missing', async () => {
372
- mockFetch.mockResolvedValueOnce(gqlResponse({
373
- deploymentLogs: [{ timestamp: '2024-06-01', message: 'hello' }],
374
- }));
375
- const logs = await service.getDeploymentLogs('dep-1');
376
- expect(logs[0].severity).toBe('info');
377
- });
378
- });
379
-
380
- describe('getBuildLogs', () => {
381
- it('should return build log entries', async () => {
382
- mockFetch.mockResolvedValueOnce(gqlResponse({
383
- buildLogs: [
384
- { timestamp: '2024-06-01T00:00:00Z', message: 'Installing dependencies...', severity: 'info' },
385
- { timestamp: '2024-06-01T00:00:05Z', message: 'Build succeeded', severity: 'info' },
386
- ],
387
- }));
388
- const logs = await service.getBuildLogs('dep-1');
389
- expect(logs).toHaveLength(2);
390
- expect(logs[0].message).toBe('Installing dependencies...');
391
- });
392
-
393
- it('should return empty array when no build logs exist', async () => {
394
- mockFetch.mockResolvedValueOnce(gqlResponse({ buildLogs: null }));
395
- const logs = await service.getBuildLogs('dep-1');
396
- expect(logs).toHaveLength(0);
397
- });
398
- });
399
-
400
- // ── Request Details ─────────────────────────────────────────────────────
401
-
402
- describe('GraphQL request format', () => {
403
- it('should send Bearer token and correct content type', async () => {
404
- mockFetch.mockResolvedValueOnce(gqlResponse({ projects: { edges: [] } }));
405
- await service.listProjects();
406
- const [url, init] = mockFetch.mock.calls[0];
407
- expect(url).toBe('https://backboard.railway.com/graphql/v2');
408
- expect(init.method).toBe('POST');
409
- expect(init.headers['Authorization']).toBe('Bearer railway-test-token-abc');
410
- expect(init.headers['Content-Type']).toBe('application/json');
411
- });
412
-
413
- it('should include variables in the request body', async () => {
414
- mockFetch.mockResolvedValueOnce(gqlResponse({
415
- project: {
416
- id: 'proj-1', name: 'p', description: '', createdAt: '', updatedAt: '',
417
- environments: { edges: [] }, services: { edges: [] },
418
- },
419
- }));
420
- await service.getProject('proj-1');
421
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
422
- expect(body.variables).toEqual({ id: 'proj-1' });
423
- });
424
- });
425
-
426
- // ── Error Handling ──────────────────────────────────────────────────────
427
-
428
- describe('error handling', () => {
429
- it('should include all GraphQL error messages joined by semicolons', async () => {
430
- mockFetch.mockResolvedValueOnce(gqlErrorResponse(['Error one', 'Error two']));
431
- await expect(service.listProjects()).rejects.toThrow('Railway GraphQL: Error one; Error two');
432
- });
433
-
434
- it('should handle HTTP-level errors', async () => {
435
- mockFetch.mockResolvedValueOnce(mockResponse('Service Unavailable', 503));
436
- await expect(service.listProjects()).rejects.toThrow('Railway API 503');
437
- });
438
- });
439
- });
@@ -1,49 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { createExtensionPack } from '../src/index.js';
4
-
5
- const mockFetch = vi.fn();
6
- vi.stubGlobal('fetch', mockFetch);
7
-
8
- function okResponse(data: any = {}) {
9
- return { ok: true, status: 200, text: async () => JSON.stringify(data), json: async () => data } as Response;
10
- }
11
-
12
- describe('createExtensionPack (Railway)', () => {
13
- beforeEach(() => {
14
- vi.clearAllMocks();
15
- mockFetch.mockResolvedValue(okResponse({ data: { me: { id: 'u1' } } }));
16
- });
17
-
18
- it('should return a valid ExtensionPack', () => {
19
- const pack = createExtensionPack({ secrets: { 'railway.token': 'tok' } });
20
- expect(pack.name).toBe('@framers/agentos-ext-cloud-railway');
21
- expect(pack.version).toBe('0.1.0');
22
- expect(pack.descriptors).toHaveLength(4);
23
- });
24
-
25
- it('should register correct tool IDs', () => {
26
- const pack = createExtensionPack({ secrets: { 'railway.token': 'tok' } });
27
- const ids = pack.descriptors.map((d) => d.id);
28
- expect(ids).toContain('railwayDeployService');
29
- expect(ids).toContain('railwayListServices');
30
- expect(ids).toContain('railwayAddDatabase');
31
- expect(ids).toContain('railwayGetLogs');
32
- });
33
-
34
- it('onActivate should throw without token', async () => {
35
- const pack = createExtensionPack({ secrets: {} });
36
- await expect(pack.onActivate!()).rejects.toThrow();
37
- });
38
-
39
- it('onActivate should succeed with valid token', async () => {
40
- const pack = createExtensionPack({ secrets: { 'railway.token': 'valid' } });
41
- await expect(pack.onActivate!()).resolves.toBeUndefined();
42
- });
43
-
44
- it('onDeactivate should complete', async () => {
45
- const pack = createExtensionPack({ secrets: { 'railway.token': 'valid' } });
46
- await pack.onActivate!();
47
- await expect(pack.onDeactivate!()).resolves.toBeUndefined();
48
- });
49
- });