@masonator/coolify-mcp 0.7.0 → 0.8.1
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.
- package/README.md +26 -13
- package/dist/__tests__/coolify-client.test.js +568 -0
- package/dist/__tests__/integration/diagnostics.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/diagnostics.integration.test.js +140 -0
- package/dist/__tests__/mcp-server.test.js +166 -0
- package/dist/lib/coolify-client.d.ts +37 -2
- package/dist/lib/coolify-client.js +388 -2
- package/dist/lib/mcp-server.js +8 -2
- package/dist/types/coolify.d.ts +81 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
[](https://mseep.ai/app/stumason-coolify-mcp)
|
|
2
|
-
|
|
3
1
|
# Coolify MCP Server
|
|
4
2
|
|
|
3
|
+
[](https://mseep.ai/app/stumason-coolify-mcp)
|
|
4
|
+
|
|
5
5
|
A Model Context Protocol (MCP) server for [Coolify](https://coolify.io/), enabling AI assistants to manage and debug your Coolify instances through natural language.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
This MCP server provides **
|
|
9
|
+
This MCP server provides **61 tools** focused on **debugging, management, and deployment**:
|
|
10
10
|
|
|
11
11
|
| Category | Tools |
|
|
12
12
|
| ------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| **Infrastructure** | overview (all resources at once) |
|
|
14
|
+
| **Diagnostics** | diagnose_app, diagnose_server, find_issues (smart lookup by name/domain/IP) |
|
|
14
15
|
| **Servers** | list, get, validate, resources, domains |
|
|
15
16
|
| **Projects** | list, get, create, update, delete |
|
|
16
17
|
| **Environments** | list, get, create, delete |
|
|
@@ -82,11 +83,12 @@ The Coolify API returns extremely verbose responses - a single application can c
|
|
|
82
83
|
|
|
83
84
|
### Response Size Comparison
|
|
84
85
|
|
|
85
|
-
| Endpoint
|
|
86
|
-
|
|
|
87
|
-
| list_applications
|
|
88
|
-
| list_services
|
|
89
|
-
| list_servers
|
|
86
|
+
| Endpoint | Full Response | Summary Response | Reduction |
|
|
87
|
+
| --------------------- | ------------- | ---------------- | --------- |
|
|
88
|
+
| list_applications | ~170KB | ~4.4KB | **97%** |
|
|
89
|
+
| list_services | ~367KB | ~1.2KB | **99%** |
|
|
90
|
+
| list_servers | ~4KB | ~0.4KB | **90%** |
|
|
91
|
+
| list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
|
|
90
92
|
|
|
91
93
|
### Recommended Workflow
|
|
92
94
|
|
|
@@ -108,7 +110,7 @@ list_applications(page=2, per_page=10)
|
|
|
108
110
|
|
|
109
111
|
### Getting Started
|
|
110
112
|
|
|
111
|
-
```
|
|
113
|
+
```text
|
|
112
114
|
Give me an overview of my infrastructure
|
|
113
115
|
Show me all my applications
|
|
114
116
|
What's running on my servers?
|
|
@@ -116,9 +118,12 @@ What's running on my servers?
|
|
|
116
118
|
|
|
117
119
|
### Debugging & Monitoring
|
|
118
120
|
|
|
119
|
-
```
|
|
121
|
+
```text
|
|
122
|
+
Diagnose my stuartmason.co.uk app
|
|
123
|
+
What's wrong with my-api application?
|
|
124
|
+
Check the status of server 192.168.1.100
|
|
125
|
+
Find any issues in my infrastructure
|
|
120
126
|
Get the logs for application {uuid}
|
|
121
|
-
What's the status of application {uuid}?
|
|
122
127
|
What environment variables are set for application {uuid}?
|
|
123
128
|
Show me recent deployments for application {uuid}
|
|
124
129
|
What resources are running on server {uuid}?
|
|
@@ -126,7 +131,7 @@ What resources are running on server {uuid}?
|
|
|
126
131
|
|
|
127
132
|
### Application Management
|
|
128
133
|
|
|
129
|
-
```
|
|
134
|
+
```text
|
|
130
135
|
Restart application {uuid}
|
|
131
136
|
Stop the database {uuid}
|
|
132
137
|
Start service {uuid}
|
|
@@ -136,7 +141,7 @@ Update the DATABASE_URL env var for application {uuid}
|
|
|
136
141
|
|
|
137
142
|
### Project Setup
|
|
138
143
|
|
|
139
|
-
```
|
|
144
|
+
```text
|
|
140
145
|
Create a new project called "my-app"
|
|
141
146
|
Create a staging environment in project {uuid}
|
|
142
147
|
Deploy my app from private GitHub repo org/repo on branch main
|
|
@@ -176,6 +181,14 @@ node dist/index.js
|
|
|
176
181
|
- `get_version` - Get Coolify API version
|
|
177
182
|
- `get_infrastructure_overview` - Get a high-level overview of all infrastructure (servers, projects, applications, databases, services)
|
|
178
183
|
|
|
184
|
+
### Diagnostics (Smart Lookup)
|
|
185
|
+
|
|
186
|
+
These tools accept human-friendly identifiers instead of just UUIDs:
|
|
187
|
+
|
|
188
|
+
- `diagnose_app` - Get comprehensive app diagnostics (status, logs, env vars, deployments). Accepts UUID, name, or domain (e.g., "stuartmason.co.uk" or "my-app")
|
|
189
|
+
- `diagnose_server` - Get server diagnostics (status, resources, domains, validation). Accepts UUID, name, or IP address (e.g., "coolify-apps" or "192.168.1.100")
|
|
190
|
+
- `find_issues` - Scan entire infrastructure for unhealthy apps, databases, services, and unreachable servers
|
|
191
|
+
|
|
179
192
|
### Servers
|
|
180
193
|
|
|
181
194
|
- `list_servers` - List all servers (returns summary)
|
|
@@ -649,6 +649,34 @@ describe('CoolifyClient', () => {
|
|
|
649
649
|
expect(result).toEqual([mockEnvVar]);
|
|
650
650
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs', expect.any(Object));
|
|
651
651
|
});
|
|
652
|
+
it('should list application env vars with summary', async () => {
|
|
653
|
+
const fullEnvVar = {
|
|
654
|
+
id: 1,
|
|
655
|
+
uuid: 'env-var-uuid',
|
|
656
|
+
key: 'API_KEY',
|
|
657
|
+
value: 'secret123',
|
|
658
|
+
is_build_time: false,
|
|
659
|
+
is_literal: true,
|
|
660
|
+
is_multiline: false,
|
|
661
|
+
is_preview: false,
|
|
662
|
+
is_shared: false,
|
|
663
|
+
is_shown_once: false,
|
|
664
|
+
application_id: 1,
|
|
665
|
+
created_at: '2024-01-01',
|
|
666
|
+
updated_at: '2024-01-01',
|
|
667
|
+
};
|
|
668
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
669
|
+
const result = await client.listApplicationEnvVars('app-uuid', { summary: true });
|
|
670
|
+
// Summary should only include uuid, key, value, is_build_time
|
|
671
|
+
expect(result).toEqual([
|
|
672
|
+
{
|
|
673
|
+
uuid: 'env-var-uuid',
|
|
674
|
+
key: 'API_KEY',
|
|
675
|
+
value: 'secret123',
|
|
676
|
+
is_build_time: false,
|
|
677
|
+
},
|
|
678
|
+
]);
|
|
679
|
+
});
|
|
652
680
|
it('should create application env var', async () => {
|
|
653
681
|
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
|
|
654
682
|
const result = await client.createApplicationEnvVar('app-uuid', {
|
|
@@ -1101,4 +1129,544 @@ describe('CoolifyClient', () => {
|
|
|
1101
1129
|
expect(result).toEqual({ message: 'Deployment cancelled' });
|
|
1102
1130
|
});
|
|
1103
1131
|
});
|
|
1132
|
+
// ===========================================================================
|
|
1133
|
+
// Smart Lookup Tests
|
|
1134
|
+
// ===========================================================================
|
|
1135
|
+
describe('Smart Lookup', () => {
|
|
1136
|
+
describe('resolveApplicationUuid', () => {
|
|
1137
|
+
const mockApps = [
|
|
1138
|
+
{
|
|
1139
|
+
id: 1,
|
|
1140
|
+
uuid: 'app-uuid-1',
|
|
1141
|
+
name: 'tidylinker',
|
|
1142
|
+
status: 'running',
|
|
1143
|
+
fqdn: 'https://tidylinker.com',
|
|
1144
|
+
created_at: '2024-01-01',
|
|
1145
|
+
updated_at: '2024-01-01',
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
id: 2,
|
|
1149
|
+
uuid: 'app-uuid-2',
|
|
1150
|
+
name: 'my-api',
|
|
1151
|
+
status: 'running',
|
|
1152
|
+
fqdn: 'https://api.example.com',
|
|
1153
|
+
created_at: '2024-01-01',
|
|
1154
|
+
updated_at: '2024-01-01',
|
|
1155
|
+
},
|
|
1156
|
+
];
|
|
1157
|
+
it('should return UUID directly if it looks like a UUID', async () => {
|
|
1158
|
+
// UUIDs are alphanumeric, 20+ chars - no API call should be made
|
|
1159
|
+
const result = await client.resolveApplicationUuid('xs0sgs4gog044s4k4c88kgsc');
|
|
1160
|
+
expect(result).toBe('xs0sgs4gog044s4k4c88kgsc');
|
|
1161
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1162
|
+
});
|
|
1163
|
+
it('should find application by name', async () => {
|
|
1164
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1165
|
+
const result = await client.resolveApplicationUuid('tidylinker');
|
|
1166
|
+
expect(result).toBe('app-uuid-1');
|
|
1167
|
+
});
|
|
1168
|
+
it('should find application by partial name (case-insensitive)', async () => {
|
|
1169
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1170
|
+
const result = await client.resolveApplicationUuid('TidyLink');
|
|
1171
|
+
expect(result).toBe('app-uuid-1');
|
|
1172
|
+
});
|
|
1173
|
+
it('should find application by domain', async () => {
|
|
1174
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1175
|
+
const result = await client.resolveApplicationUuid('tidylinker.com');
|
|
1176
|
+
expect(result).toBe('app-uuid-1');
|
|
1177
|
+
});
|
|
1178
|
+
it('should find application by partial domain', async () => {
|
|
1179
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1180
|
+
const result = await client.resolveApplicationUuid('api.example.com');
|
|
1181
|
+
expect(result).toBe('app-uuid-2');
|
|
1182
|
+
});
|
|
1183
|
+
it('should throw error if no application found', async () => {
|
|
1184
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1185
|
+
await expect(client.resolveApplicationUuid('nonexistent')).rejects.toThrow('No application found matching "nonexistent"');
|
|
1186
|
+
});
|
|
1187
|
+
it('should throw error if multiple applications match', async () => {
|
|
1188
|
+
const multiMatchApps = [
|
|
1189
|
+
{ ...mockApps[0], name: 'test-app-1' },
|
|
1190
|
+
{ ...mockApps[1], name: 'test-app-2' },
|
|
1191
|
+
];
|
|
1192
|
+
mockFetch.mockResolvedValueOnce(mockResponse(multiMatchApps));
|
|
1193
|
+
await expect(client.resolveApplicationUuid('test-app')).rejects.toThrow('Multiple applications match');
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
describe('resolveServerUuid', () => {
|
|
1197
|
+
const mockServers = [
|
|
1198
|
+
{
|
|
1199
|
+
id: 1,
|
|
1200
|
+
uuid: 'server-uuid-1',
|
|
1201
|
+
name: 'coolify-apps',
|
|
1202
|
+
ip: '192.168.1.100',
|
|
1203
|
+
user: 'root',
|
|
1204
|
+
port: 22,
|
|
1205
|
+
created_at: '2024-01-01',
|
|
1206
|
+
updated_at: '2024-01-01',
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
id: 2,
|
|
1210
|
+
uuid: 'server-uuid-2',
|
|
1211
|
+
name: 'production-db',
|
|
1212
|
+
ip: '10.0.0.50',
|
|
1213
|
+
user: 'root',
|
|
1214
|
+
port: 22,
|
|
1215
|
+
created_at: '2024-01-01',
|
|
1216
|
+
updated_at: '2024-01-01',
|
|
1217
|
+
},
|
|
1218
|
+
];
|
|
1219
|
+
it('should return UUID directly if it looks like a UUID', async () => {
|
|
1220
|
+
const result = await client.resolveServerUuid('ggkk8w4c08gw48oowsg4g0oc');
|
|
1221
|
+
expect(result).toBe('ggkk8w4c08gw48oowsg4g0oc');
|
|
1222
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1223
|
+
});
|
|
1224
|
+
it('should find server by name', async () => {
|
|
1225
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1226
|
+
const result = await client.resolveServerUuid('coolify-apps');
|
|
1227
|
+
expect(result).toBe('server-uuid-1');
|
|
1228
|
+
});
|
|
1229
|
+
it('should find server by partial name (case-insensitive)', async () => {
|
|
1230
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1231
|
+
const result = await client.resolveServerUuid('Coolify');
|
|
1232
|
+
expect(result).toBe('server-uuid-1');
|
|
1233
|
+
});
|
|
1234
|
+
it('should find server by IP address', async () => {
|
|
1235
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1236
|
+
const result = await client.resolveServerUuid('192.168.1.100');
|
|
1237
|
+
expect(result).toBe('server-uuid-1');
|
|
1238
|
+
});
|
|
1239
|
+
it('should find server by partial IP', async () => {
|
|
1240
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1241
|
+
const result = await client.resolveServerUuid('10.0.0');
|
|
1242
|
+
expect(result).toBe('server-uuid-2');
|
|
1243
|
+
});
|
|
1244
|
+
it('should throw error if no server found', async () => {
|
|
1245
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1246
|
+
await expect(client.resolveServerUuid('nonexistent')).rejects.toThrow('No server found matching "nonexistent"');
|
|
1247
|
+
});
|
|
1248
|
+
it('should throw error if multiple servers match', async () => {
|
|
1249
|
+
const multiMatchServers = [
|
|
1250
|
+
{ ...mockServers[0], name: 'prod-server-1' },
|
|
1251
|
+
{ ...mockServers[1], name: 'prod-server-2' },
|
|
1252
|
+
];
|
|
1253
|
+
mockFetch.mockResolvedValueOnce(mockResponse(multiMatchServers));
|
|
1254
|
+
await expect(client.resolveServerUuid('prod-server')).rejects.toThrow('Multiple servers match');
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
// ===========================================================================
|
|
1259
|
+
// Diagnostic Methods Tests
|
|
1260
|
+
// ===========================================================================
|
|
1261
|
+
describe('Diagnostic Methods', () => {
|
|
1262
|
+
describe('diagnoseApplication', () => {
|
|
1263
|
+
// Use UUID-like format that matches the isLikelyUuid check
|
|
1264
|
+
const testAppUuid = 'app0uuid0test0001234567';
|
|
1265
|
+
const mockApp = {
|
|
1266
|
+
id: 1,
|
|
1267
|
+
uuid: testAppUuid,
|
|
1268
|
+
name: 'test-app',
|
|
1269
|
+
status: 'running:healthy',
|
|
1270
|
+
fqdn: 'https://test.com',
|
|
1271
|
+
git_repository: 'org/repo',
|
|
1272
|
+
git_branch: 'main',
|
|
1273
|
+
created_at: '2024-01-01',
|
|
1274
|
+
updated_at: '2024-01-01',
|
|
1275
|
+
};
|
|
1276
|
+
const mockLogs = 'Log line 1\nLog line 2\nLog line 3';
|
|
1277
|
+
const mockEnvVars = [
|
|
1278
|
+
{
|
|
1279
|
+
id: 1,
|
|
1280
|
+
uuid: 'env-1',
|
|
1281
|
+
key: 'DATABASE_URL',
|
|
1282
|
+
value: 'postgres://...',
|
|
1283
|
+
is_build_time: false,
|
|
1284
|
+
},
|
|
1285
|
+
{ id: 2, uuid: 'env-2', key: 'NODE_ENV', value: 'production', is_build_time: true },
|
|
1286
|
+
];
|
|
1287
|
+
const mockDeployments = [
|
|
1288
|
+
{
|
|
1289
|
+
id: 1,
|
|
1290
|
+
uuid: 'deploy-1',
|
|
1291
|
+
deployment_uuid: 'deploy-1',
|
|
1292
|
+
status: 'finished',
|
|
1293
|
+
force_rebuild: false,
|
|
1294
|
+
is_webhook: false,
|
|
1295
|
+
is_api: false,
|
|
1296
|
+
restart_only: false,
|
|
1297
|
+
created_at: '2024-01-01',
|
|
1298
|
+
updated_at: '2024-01-01',
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
id: 2,
|
|
1302
|
+
uuid: 'deploy-2',
|
|
1303
|
+
deployment_uuid: 'deploy-2',
|
|
1304
|
+
status: 'finished',
|
|
1305
|
+
force_rebuild: false,
|
|
1306
|
+
is_webhook: false,
|
|
1307
|
+
is_api: false,
|
|
1308
|
+
restart_only: false,
|
|
1309
|
+
created_at: '2024-01-02',
|
|
1310
|
+
updated_at: '2024-01-02',
|
|
1311
|
+
},
|
|
1312
|
+
];
|
|
1313
|
+
it('should aggregate all application data successfully', async () => {
|
|
1314
|
+
mockFetch
|
|
1315
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1316
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1317
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1318
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1319
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1320
|
+
expect(result.application).toEqual({
|
|
1321
|
+
uuid: testAppUuid,
|
|
1322
|
+
name: 'test-app',
|
|
1323
|
+
status: 'running:healthy',
|
|
1324
|
+
fqdn: 'https://test.com',
|
|
1325
|
+
git_repository: 'org/repo',
|
|
1326
|
+
git_branch: 'main',
|
|
1327
|
+
});
|
|
1328
|
+
expect(result.health.status).toBe('healthy');
|
|
1329
|
+
expect(result.logs).toBe(mockLogs);
|
|
1330
|
+
expect(result.environment_variables.count).toBe(2);
|
|
1331
|
+
expect(result.environment_variables.variables).toEqual([
|
|
1332
|
+
{ key: 'DATABASE_URL', is_build_time: false },
|
|
1333
|
+
{ key: 'NODE_ENV', is_build_time: true },
|
|
1334
|
+
]);
|
|
1335
|
+
expect(result.recent_deployments).toHaveLength(2);
|
|
1336
|
+
expect(result.errors).toBeUndefined();
|
|
1337
|
+
});
|
|
1338
|
+
it('should detect unhealthy application status', async () => {
|
|
1339
|
+
const unhealthyApp = { ...mockApp, status: 'exited:unhealthy' };
|
|
1340
|
+
mockFetch
|
|
1341
|
+
.mockResolvedValueOnce(mockResponse(unhealthyApp))
|
|
1342
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1343
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1344
|
+
.mockResolvedValueOnce(mockResponse([]));
|
|
1345
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1346
|
+
expect(result.health.status).toBe('unhealthy');
|
|
1347
|
+
expect(result.health.issues).toContain('Status: exited:unhealthy');
|
|
1348
|
+
});
|
|
1349
|
+
it('should detect failed deployments as issues', async () => {
|
|
1350
|
+
const failedDeployments = [
|
|
1351
|
+
{ ...mockDeployments[0], status: 'failed' },
|
|
1352
|
+
{ ...mockDeployments[1], status: 'failed' },
|
|
1353
|
+
];
|
|
1354
|
+
mockFetch
|
|
1355
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1356
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1357
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1358
|
+
.mockResolvedValueOnce(mockResponse(failedDeployments));
|
|
1359
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1360
|
+
expect(result.health.issues).toContain('2 failed deployment(s) in last 5');
|
|
1361
|
+
});
|
|
1362
|
+
it('should handle partial failures gracefully', async () => {
|
|
1363
|
+
mockFetch
|
|
1364
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1365
|
+
.mockRejectedValueOnce(new Error('Logs unavailable'))
|
|
1366
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1367
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1368
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1369
|
+
expect(result.application).not.toBeNull();
|
|
1370
|
+
expect(result.logs).toBeNull();
|
|
1371
|
+
expect(result.errors).toContain('logs: Logs unavailable');
|
|
1372
|
+
});
|
|
1373
|
+
it('should handle complete failure gracefully', async () => {
|
|
1374
|
+
mockFetch
|
|
1375
|
+
.mockRejectedValueOnce(new Error('App not found'))
|
|
1376
|
+
.mockRejectedValueOnce(new Error('Logs unavailable'))
|
|
1377
|
+
.mockRejectedValueOnce(new Error('Env vars unavailable'))
|
|
1378
|
+
.mockRejectedValueOnce(new Error('Deployments unavailable'));
|
|
1379
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1380
|
+
expect(result.application).toBeNull();
|
|
1381
|
+
expect(result.logs).toBeNull();
|
|
1382
|
+
expect(result.health.status).toBe('unknown');
|
|
1383
|
+
expect(result.errors).toHaveLength(4);
|
|
1384
|
+
});
|
|
1385
|
+
it('should find application by name and diagnose it', async () => {
|
|
1386
|
+
const mockApps = [{ ...mockApp, uuid: 'found-uuid', name: 'my-app' }];
|
|
1387
|
+
mockFetch
|
|
1388
|
+
.mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
|
|
1389
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1390
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1391
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1392
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1393
|
+
const result = await client.diagnoseApplication('my-app');
|
|
1394
|
+
expect(result.application).not.toBeNull();
|
|
1395
|
+
// First call should be to list apps for lookup
|
|
1396
|
+
expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/applications', expect.any(Object));
|
|
1397
|
+
});
|
|
1398
|
+
it('should find application by domain and diagnose it', async () => {
|
|
1399
|
+
const mockApps = [{ ...mockApp, uuid: 'found-uuid', fqdn: 'https://tidylinker.com' }];
|
|
1400
|
+
mockFetch
|
|
1401
|
+
.mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
|
|
1402
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1403
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1404
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1405
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1406
|
+
const result = await client.diagnoseApplication('tidylinker.com');
|
|
1407
|
+
expect(result.application).not.toBeNull();
|
|
1408
|
+
});
|
|
1409
|
+
it('should return error in result when application not found by name', async () => {
|
|
1410
|
+
mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty app list
|
|
1411
|
+
const result = await client.diagnoseApplication('nonexistent-app');
|
|
1412
|
+
expect(result.application).toBeNull();
|
|
1413
|
+
expect(result.errors).toContain('No application found matching "nonexistent-app"');
|
|
1414
|
+
});
|
|
1415
|
+
});
|
|
1416
|
+
describe('diagnoseServer', () => {
|
|
1417
|
+
// Use UUID-like format that matches the isLikelyUuid check
|
|
1418
|
+
const testServerUuid = 'srv0uuid0test0001234567';
|
|
1419
|
+
const mockServer = {
|
|
1420
|
+
id: 1,
|
|
1421
|
+
uuid: testServerUuid,
|
|
1422
|
+
name: 'test-server',
|
|
1423
|
+
ip: '192.168.1.1',
|
|
1424
|
+
user: 'root',
|
|
1425
|
+
port: 22,
|
|
1426
|
+
status: 'running',
|
|
1427
|
+
is_reachable: true,
|
|
1428
|
+
is_usable: true,
|
|
1429
|
+
created_at: '2024-01-01',
|
|
1430
|
+
updated_at: '2024-01-01',
|
|
1431
|
+
};
|
|
1432
|
+
const mockResources = [
|
|
1433
|
+
{
|
|
1434
|
+
id: 1,
|
|
1435
|
+
uuid: 'res-1',
|
|
1436
|
+
name: 'app-1',
|
|
1437
|
+
type: 'application',
|
|
1438
|
+
status: 'running:healthy',
|
|
1439
|
+
created_at: '2024-01-01',
|
|
1440
|
+
updated_at: '2024-01-01',
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
id: 2,
|
|
1444
|
+
uuid: 'res-2',
|
|
1445
|
+
name: 'db-1',
|
|
1446
|
+
type: 'database',
|
|
1447
|
+
status: 'running:healthy',
|
|
1448
|
+
created_at: '2024-01-01',
|
|
1449
|
+
updated_at: '2024-01-01',
|
|
1450
|
+
},
|
|
1451
|
+
];
|
|
1452
|
+
const mockDomains = [{ ip: '192.168.1.1', domains: ['example.com', 'api.example.com'] }];
|
|
1453
|
+
const mockValidation = { message: 'Server is reachable and validated' };
|
|
1454
|
+
it('should aggregate all server data successfully', async () => {
|
|
1455
|
+
mockFetch
|
|
1456
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1457
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1458
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1459
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1460
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1461
|
+
expect(result.server).toEqual({
|
|
1462
|
+
uuid: testServerUuid,
|
|
1463
|
+
name: 'test-server',
|
|
1464
|
+
ip: '192.168.1.1',
|
|
1465
|
+
status: 'running',
|
|
1466
|
+
is_reachable: true,
|
|
1467
|
+
});
|
|
1468
|
+
expect(result.health.status).toBe('healthy');
|
|
1469
|
+
expect(result.resources).toHaveLength(2);
|
|
1470
|
+
expect(result.domains).toHaveLength(1);
|
|
1471
|
+
expect(result.validation?.message).toBe('Server is reachable and validated');
|
|
1472
|
+
expect(result.errors).toBeUndefined();
|
|
1473
|
+
});
|
|
1474
|
+
it('should detect unreachable server', async () => {
|
|
1475
|
+
const unreachableServer = { ...mockServer, is_reachable: false };
|
|
1476
|
+
mockFetch
|
|
1477
|
+
.mockResolvedValueOnce(mockResponse(unreachableServer))
|
|
1478
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1479
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1480
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1481
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1482
|
+
expect(result.health.status).toBe('unhealthy');
|
|
1483
|
+
expect(result.health.issues).toContain('Server is not reachable');
|
|
1484
|
+
});
|
|
1485
|
+
it('should detect unhealthy resources', async () => {
|
|
1486
|
+
const unhealthyResources = [
|
|
1487
|
+
{ ...mockResources[0], status: 'exited:unhealthy' },
|
|
1488
|
+
{ ...mockResources[1], status: 'running:healthy' },
|
|
1489
|
+
];
|
|
1490
|
+
mockFetch
|
|
1491
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1492
|
+
.mockResolvedValueOnce(mockResponse(unhealthyResources))
|
|
1493
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1494
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1495
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1496
|
+
expect(result.health.issues).toContain('1 unhealthy resource(s)');
|
|
1497
|
+
});
|
|
1498
|
+
it('should handle partial failures gracefully', async () => {
|
|
1499
|
+
mockFetch
|
|
1500
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1501
|
+
.mockRejectedValueOnce(new Error('Resources unavailable'))
|
|
1502
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1503
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1504
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1505
|
+
expect(result.server).not.toBeNull();
|
|
1506
|
+
expect(result.resources).toEqual([]);
|
|
1507
|
+
expect(result.errors).toContain('resources: Resources unavailable');
|
|
1508
|
+
});
|
|
1509
|
+
it('should find server by name and diagnose it', async () => {
|
|
1510
|
+
const mockServers = [{ ...mockServer, uuid: 'found-uuid', name: 'coolify-apps' }];
|
|
1511
|
+
mockFetch
|
|
1512
|
+
.mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
|
|
1513
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1514
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1515
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1516
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1517
|
+
const result = await client.diagnoseServer('coolify-apps');
|
|
1518
|
+
expect(result.server).not.toBeNull();
|
|
1519
|
+
// First call should be to list servers for lookup
|
|
1520
|
+
expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/servers', expect.any(Object));
|
|
1521
|
+
});
|
|
1522
|
+
it('should find server by IP and diagnose it', async () => {
|
|
1523
|
+
const mockServers = [{ ...mockServer, uuid: 'found-uuid', ip: '10.0.0.5' }];
|
|
1524
|
+
mockFetch
|
|
1525
|
+
.mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
|
|
1526
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1527
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1528
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1529
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1530
|
+
const result = await client.diagnoseServer('10.0.0.5');
|
|
1531
|
+
expect(result.server).not.toBeNull();
|
|
1532
|
+
});
|
|
1533
|
+
it('should return error in result when server not found by name', async () => {
|
|
1534
|
+
mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty server list
|
|
1535
|
+
const result = await client.diagnoseServer('nonexistent-server');
|
|
1536
|
+
expect(result.server).toBeNull();
|
|
1537
|
+
expect(result.errors).toContain('No server found matching "nonexistent-server"');
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
describe('findInfrastructureIssues', () => {
|
|
1541
|
+
const mockServers = [
|
|
1542
|
+
{
|
|
1543
|
+
id: 1,
|
|
1544
|
+
uuid: 'server-1',
|
|
1545
|
+
name: 'healthy-server',
|
|
1546
|
+
ip: '1.1.1.1',
|
|
1547
|
+
user: 'root',
|
|
1548
|
+
port: 22,
|
|
1549
|
+
is_reachable: true,
|
|
1550
|
+
created_at: '2024-01-01',
|
|
1551
|
+
updated_at: '2024-01-01',
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
id: 2,
|
|
1555
|
+
uuid: 'server-2',
|
|
1556
|
+
name: 'unreachable-server',
|
|
1557
|
+
ip: '2.2.2.2',
|
|
1558
|
+
user: 'root',
|
|
1559
|
+
port: 22,
|
|
1560
|
+
is_reachable: false,
|
|
1561
|
+
status: 'error',
|
|
1562
|
+
created_at: '2024-01-01',
|
|
1563
|
+
updated_at: '2024-01-01',
|
|
1564
|
+
},
|
|
1565
|
+
];
|
|
1566
|
+
const mockApplications = [
|
|
1567
|
+
{
|
|
1568
|
+
id: 1,
|
|
1569
|
+
uuid: 'app-1',
|
|
1570
|
+
name: 'healthy-app',
|
|
1571
|
+
status: 'running:healthy',
|
|
1572
|
+
created_at: '2024-01-01',
|
|
1573
|
+
updated_at: '2024-01-01',
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
id: 2,
|
|
1577
|
+
uuid: 'app-2',
|
|
1578
|
+
name: 'unhealthy-app',
|
|
1579
|
+
status: 'exited:unhealthy',
|
|
1580
|
+
created_at: '2024-01-01',
|
|
1581
|
+
updated_at: '2024-01-01',
|
|
1582
|
+
},
|
|
1583
|
+
];
|
|
1584
|
+
const mockDatabases = [
|
|
1585
|
+
{
|
|
1586
|
+
id: 1,
|
|
1587
|
+
uuid: 'db-1',
|
|
1588
|
+
name: 'healthy-db',
|
|
1589
|
+
type: 'postgresql',
|
|
1590
|
+
status: 'running:healthy',
|
|
1591
|
+
is_public: false,
|
|
1592
|
+
image: 'postgres:16',
|
|
1593
|
+
created_at: '2024-01-01',
|
|
1594
|
+
updated_at: '2024-01-01',
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
id: 2,
|
|
1598
|
+
uuid: 'db-2',
|
|
1599
|
+
name: 'stopped-db',
|
|
1600
|
+
type: 'redis',
|
|
1601
|
+
status: 'exited:unhealthy',
|
|
1602
|
+
is_public: false,
|
|
1603
|
+
image: 'redis:7',
|
|
1604
|
+
created_at: '2024-01-01',
|
|
1605
|
+
updated_at: '2024-01-01',
|
|
1606
|
+
},
|
|
1607
|
+
];
|
|
1608
|
+
const mockServices = [
|
|
1609
|
+
{
|
|
1610
|
+
id: 1,
|
|
1611
|
+
uuid: 'svc-1',
|
|
1612
|
+
name: 'healthy-service',
|
|
1613
|
+
type: 'pocketbase',
|
|
1614
|
+
status: 'running:healthy',
|
|
1615
|
+
created_at: '2024-01-01',
|
|
1616
|
+
updated_at: '2024-01-01',
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
id: 2,
|
|
1620
|
+
uuid: 'svc-2',
|
|
1621
|
+
name: 'exited-service',
|
|
1622
|
+
type: 'n8n',
|
|
1623
|
+
status: 'exited',
|
|
1624
|
+
created_at: '2024-01-01',
|
|
1625
|
+
updated_at: '2024-01-01',
|
|
1626
|
+
},
|
|
1627
|
+
];
|
|
1628
|
+
it('should find all infrastructure issues', async () => {
|
|
1629
|
+
mockFetch
|
|
1630
|
+
.mockResolvedValueOnce(mockResponse(mockServers))
|
|
1631
|
+
.mockResolvedValueOnce(mockResponse(mockApplications))
|
|
1632
|
+
.mockResolvedValueOnce(mockResponse(mockDatabases))
|
|
1633
|
+
.mockResolvedValueOnce(mockResponse(mockServices));
|
|
1634
|
+
const result = await client.findInfrastructureIssues();
|
|
1635
|
+
expect(result.summary.total_issues).toBe(4);
|
|
1636
|
+
expect(result.summary.unreachable_servers).toBe(1);
|
|
1637
|
+
expect(result.summary.unhealthy_applications).toBe(1);
|
|
1638
|
+
expect(result.summary.unhealthy_databases).toBe(1);
|
|
1639
|
+
expect(result.summary.unhealthy_services).toBe(1);
|
|
1640
|
+
expect(result.issues).toHaveLength(4);
|
|
1641
|
+
expect(result.errors).toBeUndefined();
|
|
1642
|
+
});
|
|
1643
|
+
it('should return empty issues when everything is healthy', async () => {
|
|
1644
|
+
const healthyServers = [mockServers[0]];
|
|
1645
|
+
const healthyApps = [mockApplications[0]];
|
|
1646
|
+
const healthyDbs = [mockDatabases[0]];
|
|
1647
|
+
const healthySvcs = [mockServices[0]];
|
|
1648
|
+
mockFetch
|
|
1649
|
+
.mockResolvedValueOnce(mockResponse(healthyServers))
|
|
1650
|
+
.mockResolvedValueOnce(mockResponse(healthyApps))
|
|
1651
|
+
.mockResolvedValueOnce(mockResponse(healthyDbs))
|
|
1652
|
+
.mockResolvedValueOnce(mockResponse(healthySvcs));
|
|
1653
|
+
const result = await client.findInfrastructureIssues();
|
|
1654
|
+
expect(result.summary.total_issues).toBe(0);
|
|
1655
|
+
expect(result.issues).toHaveLength(0);
|
|
1656
|
+
});
|
|
1657
|
+
it('should handle partial failures and still report issues', async () => {
|
|
1658
|
+
mockFetch
|
|
1659
|
+
.mockResolvedValueOnce(mockResponse(mockServers))
|
|
1660
|
+
.mockRejectedValueOnce(new Error('Applications unavailable'))
|
|
1661
|
+
.mockResolvedValueOnce(mockResponse(mockDatabases))
|
|
1662
|
+
.mockResolvedValueOnce(mockResponse(mockServices));
|
|
1663
|
+
const result = await client.findInfrastructureIssues();
|
|
1664
|
+
expect(result.summary.unreachable_servers).toBe(1);
|
|
1665
|
+
expect(result.summary.unhealthy_databases).toBe(1);
|
|
1666
|
+
expect(result.summary.unhealthy_services).toBe(1);
|
|
1667
|
+
expect(result.summary.unhealthy_applications).toBe(0); // Failed to fetch
|
|
1668
|
+
expect(result.errors).toContain('applications: Applications unavailable');
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1104
1672
|
});
|