@masonator/coolify-mcp 0.7.0 → 0.8.0

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.
@@ -1101,4 +1101,544 @@ describe('CoolifyClient', () => {
1101
1101
  expect(result).toEqual({ message: 'Deployment cancelled' });
1102
1102
  });
1103
1103
  });
1104
+ // ===========================================================================
1105
+ // Smart Lookup Tests
1106
+ // ===========================================================================
1107
+ describe('Smart Lookup', () => {
1108
+ describe('resolveApplicationUuid', () => {
1109
+ const mockApps = [
1110
+ {
1111
+ id: 1,
1112
+ uuid: 'app-uuid-1',
1113
+ name: 'tidylinker',
1114
+ status: 'running',
1115
+ fqdn: 'https://tidylinker.com',
1116
+ created_at: '2024-01-01',
1117
+ updated_at: '2024-01-01',
1118
+ },
1119
+ {
1120
+ id: 2,
1121
+ uuid: 'app-uuid-2',
1122
+ name: 'my-api',
1123
+ status: 'running',
1124
+ fqdn: 'https://api.example.com',
1125
+ created_at: '2024-01-01',
1126
+ updated_at: '2024-01-01',
1127
+ },
1128
+ ];
1129
+ it('should return UUID directly if it looks like a UUID', async () => {
1130
+ // UUIDs are alphanumeric, 20+ chars - no API call should be made
1131
+ const result = await client.resolveApplicationUuid('xs0sgs4gog044s4k4c88kgsc');
1132
+ expect(result).toBe('xs0sgs4gog044s4k4c88kgsc');
1133
+ expect(mockFetch).not.toHaveBeenCalled();
1134
+ });
1135
+ it('should find application by name', async () => {
1136
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1137
+ const result = await client.resolveApplicationUuid('tidylinker');
1138
+ expect(result).toBe('app-uuid-1');
1139
+ });
1140
+ it('should find application by partial name (case-insensitive)', async () => {
1141
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1142
+ const result = await client.resolveApplicationUuid('TidyLink');
1143
+ expect(result).toBe('app-uuid-1');
1144
+ });
1145
+ it('should find application by domain', async () => {
1146
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1147
+ const result = await client.resolveApplicationUuid('tidylinker.com');
1148
+ expect(result).toBe('app-uuid-1');
1149
+ });
1150
+ it('should find application by partial domain', async () => {
1151
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1152
+ const result = await client.resolveApplicationUuid('api.example.com');
1153
+ expect(result).toBe('app-uuid-2');
1154
+ });
1155
+ it('should throw error if no application found', async () => {
1156
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1157
+ await expect(client.resolveApplicationUuid('nonexistent')).rejects.toThrow('No application found matching "nonexistent"');
1158
+ });
1159
+ it('should throw error if multiple applications match', async () => {
1160
+ const multiMatchApps = [
1161
+ { ...mockApps[0], name: 'test-app-1' },
1162
+ { ...mockApps[1], name: 'test-app-2' },
1163
+ ];
1164
+ mockFetch.mockResolvedValueOnce(mockResponse(multiMatchApps));
1165
+ await expect(client.resolveApplicationUuid('test-app')).rejects.toThrow('Multiple applications match');
1166
+ });
1167
+ });
1168
+ describe('resolveServerUuid', () => {
1169
+ const mockServers = [
1170
+ {
1171
+ id: 1,
1172
+ uuid: 'server-uuid-1',
1173
+ name: 'coolify-apps',
1174
+ ip: '192.168.1.100',
1175
+ user: 'root',
1176
+ port: 22,
1177
+ created_at: '2024-01-01',
1178
+ updated_at: '2024-01-01',
1179
+ },
1180
+ {
1181
+ id: 2,
1182
+ uuid: 'server-uuid-2',
1183
+ name: 'production-db',
1184
+ ip: '10.0.0.50',
1185
+ user: 'root',
1186
+ port: 22,
1187
+ created_at: '2024-01-01',
1188
+ updated_at: '2024-01-01',
1189
+ },
1190
+ ];
1191
+ it('should return UUID directly if it looks like a UUID', async () => {
1192
+ const result = await client.resolveServerUuid('ggkk8w4c08gw48oowsg4g0oc');
1193
+ expect(result).toBe('ggkk8w4c08gw48oowsg4g0oc');
1194
+ expect(mockFetch).not.toHaveBeenCalled();
1195
+ });
1196
+ it('should find server by name', async () => {
1197
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
1198
+ const result = await client.resolveServerUuid('coolify-apps');
1199
+ expect(result).toBe('server-uuid-1');
1200
+ });
1201
+ it('should find server by partial name (case-insensitive)', async () => {
1202
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
1203
+ const result = await client.resolveServerUuid('Coolify');
1204
+ expect(result).toBe('server-uuid-1');
1205
+ });
1206
+ it('should find server by IP address', async () => {
1207
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
1208
+ const result = await client.resolveServerUuid('192.168.1.100');
1209
+ expect(result).toBe('server-uuid-1');
1210
+ });
1211
+ it('should find server by partial IP', async () => {
1212
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
1213
+ const result = await client.resolveServerUuid('10.0.0');
1214
+ expect(result).toBe('server-uuid-2');
1215
+ });
1216
+ it('should throw error if no server found', async () => {
1217
+ mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
1218
+ await expect(client.resolveServerUuid('nonexistent')).rejects.toThrow('No server found matching "nonexistent"');
1219
+ });
1220
+ it('should throw error if multiple servers match', async () => {
1221
+ const multiMatchServers = [
1222
+ { ...mockServers[0], name: 'prod-server-1' },
1223
+ { ...mockServers[1], name: 'prod-server-2' },
1224
+ ];
1225
+ mockFetch.mockResolvedValueOnce(mockResponse(multiMatchServers));
1226
+ await expect(client.resolveServerUuid('prod-server')).rejects.toThrow('Multiple servers match');
1227
+ });
1228
+ });
1229
+ });
1230
+ // ===========================================================================
1231
+ // Diagnostic Methods Tests
1232
+ // ===========================================================================
1233
+ describe('Diagnostic Methods', () => {
1234
+ describe('diagnoseApplication', () => {
1235
+ // Use UUID-like format that matches the isLikelyUuid check
1236
+ const testAppUuid = 'app0uuid0test0001234567';
1237
+ const mockApp = {
1238
+ id: 1,
1239
+ uuid: testAppUuid,
1240
+ name: 'test-app',
1241
+ status: 'running:healthy',
1242
+ fqdn: 'https://test.com',
1243
+ git_repository: 'org/repo',
1244
+ git_branch: 'main',
1245
+ created_at: '2024-01-01',
1246
+ updated_at: '2024-01-01',
1247
+ };
1248
+ const mockLogs = 'Log line 1\nLog line 2\nLog line 3';
1249
+ const mockEnvVars = [
1250
+ {
1251
+ id: 1,
1252
+ uuid: 'env-1',
1253
+ key: 'DATABASE_URL',
1254
+ value: 'postgres://...',
1255
+ is_build_time: false,
1256
+ },
1257
+ { id: 2, uuid: 'env-2', key: 'NODE_ENV', value: 'production', is_build_time: true },
1258
+ ];
1259
+ const mockDeployments = [
1260
+ {
1261
+ id: 1,
1262
+ uuid: 'deploy-1',
1263
+ deployment_uuid: 'deploy-1',
1264
+ status: 'finished',
1265
+ force_rebuild: false,
1266
+ is_webhook: false,
1267
+ is_api: false,
1268
+ restart_only: false,
1269
+ created_at: '2024-01-01',
1270
+ updated_at: '2024-01-01',
1271
+ },
1272
+ {
1273
+ id: 2,
1274
+ uuid: 'deploy-2',
1275
+ deployment_uuid: 'deploy-2',
1276
+ status: 'finished',
1277
+ force_rebuild: false,
1278
+ is_webhook: false,
1279
+ is_api: false,
1280
+ restart_only: false,
1281
+ created_at: '2024-01-02',
1282
+ updated_at: '2024-01-02',
1283
+ },
1284
+ ];
1285
+ it('should aggregate all application data successfully', async () => {
1286
+ mockFetch
1287
+ .mockResolvedValueOnce(mockResponse(mockApp))
1288
+ .mockResolvedValueOnce(mockResponse(mockLogs))
1289
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1290
+ .mockResolvedValueOnce(mockResponse(mockDeployments));
1291
+ const result = await client.diagnoseApplication(testAppUuid);
1292
+ expect(result.application).toEqual({
1293
+ uuid: testAppUuid,
1294
+ name: 'test-app',
1295
+ status: 'running:healthy',
1296
+ fqdn: 'https://test.com',
1297
+ git_repository: 'org/repo',
1298
+ git_branch: 'main',
1299
+ });
1300
+ expect(result.health.status).toBe('healthy');
1301
+ expect(result.logs).toBe(mockLogs);
1302
+ expect(result.environment_variables.count).toBe(2);
1303
+ expect(result.environment_variables.variables).toEqual([
1304
+ { key: 'DATABASE_URL', is_build_time: false },
1305
+ { key: 'NODE_ENV', is_build_time: true },
1306
+ ]);
1307
+ expect(result.recent_deployments).toHaveLength(2);
1308
+ expect(result.errors).toBeUndefined();
1309
+ });
1310
+ it('should detect unhealthy application status', async () => {
1311
+ const unhealthyApp = { ...mockApp, status: 'exited:unhealthy' };
1312
+ mockFetch
1313
+ .mockResolvedValueOnce(mockResponse(unhealthyApp))
1314
+ .mockResolvedValueOnce(mockResponse(mockLogs))
1315
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1316
+ .mockResolvedValueOnce(mockResponse([]));
1317
+ const result = await client.diagnoseApplication(testAppUuid);
1318
+ expect(result.health.status).toBe('unhealthy');
1319
+ expect(result.health.issues).toContain('Status: exited:unhealthy');
1320
+ });
1321
+ it('should detect failed deployments as issues', async () => {
1322
+ const failedDeployments = [
1323
+ { ...mockDeployments[0], status: 'failed' },
1324
+ { ...mockDeployments[1], status: 'failed' },
1325
+ ];
1326
+ mockFetch
1327
+ .mockResolvedValueOnce(mockResponse(mockApp))
1328
+ .mockResolvedValueOnce(mockResponse(mockLogs))
1329
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1330
+ .mockResolvedValueOnce(mockResponse(failedDeployments));
1331
+ const result = await client.diagnoseApplication(testAppUuid);
1332
+ expect(result.health.issues).toContain('2 failed deployment(s) in last 5');
1333
+ });
1334
+ it('should handle partial failures gracefully', async () => {
1335
+ mockFetch
1336
+ .mockResolvedValueOnce(mockResponse(mockApp))
1337
+ .mockRejectedValueOnce(new Error('Logs unavailable'))
1338
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1339
+ .mockResolvedValueOnce(mockResponse(mockDeployments));
1340
+ const result = await client.diagnoseApplication(testAppUuid);
1341
+ expect(result.application).not.toBeNull();
1342
+ expect(result.logs).toBeNull();
1343
+ expect(result.errors).toContain('logs: Logs unavailable');
1344
+ });
1345
+ it('should handle complete failure gracefully', async () => {
1346
+ mockFetch
1347
+ .mockRejectedValueOnce(new Error('App not found'))
1348
+ .mockRejectedValueOnce(new Error('Logs unavailable'))
1349
+ .mockRejectedValueOnce(new Error('Env vars unavailable'))
1350
+ .mockRejectedValueOnce(new Error('Deployments unavailable'));
1351
+ const result = await client.diagnoseApplication(testAppUuid);
1352
+ expect(result.application).toBeNull();
1353
+ expect(result.logs).toBeNull();
1354
+ expect(result.health.status).toBe('unknown');
1355
+ expect(result.errors).toHaveLength(4);
1356
+ });
1357
+ it('should find application by name and diagnose it', async () => {
1358
+ const mockApps = [{ ...mockApp, uuid: 'found-uuid', name: 'my-app' }];
1359
+ mockFetch
1360
+ .mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
1361
+ .mockResolvedValueOnce(mockResponse(mockApp))
1362
+ .mockResolvedValueOnce(mockResponse(mockLogs))
1363
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1364
+ .mockResolvedValueOnce(mockResponse(mockDeployments));
1365
+ const result = await client.diagnoseApplication('my-app');
1366
+ expect(result.application).not.toBeNull();
1367
+ // First call should be to list apps for lookup
1368
+ expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/applications', expect.any(Object));
1369
+ });
1370
+ it('should find application by domain and diagnose it', async () => {
1371
+ const mockApps = [{ ...mockApp, uuid: 'found-uuid', fqdn: 'https://tidylinker.com' }];
1372
+ mockFetch
1373
+ .mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
1374
+ .mockResolvedValueOnce(mockResponse(mockApp))
1375
+ .mockResolvedValueOnce(mockResponse(mockLogs))
1376
+ .mockResolvedValueOnce(mockResponse(mockEnvVars))
1377
+ .mockResolvedValueOnce(mockResponse(mockDeployments));
1378
+ const result = await client.diagnoseApplication('tidylinker.com');
1379
+ expect(result.application).not.toBeNull();
1380
+ });
1381
+ it('should return error in result when application not found by name', async () => {
1382
+ mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty app list
1383
+ const result = await client.diagnoseApplication('nonexistent-app');
1384
+ expect(result.application).toBeNull();
1385
+ expect(result.errors).toContain('No application found matching "nonexistent-app"');
1386
+ });
1387
+ });
1388
+ describe('diagnoseServer', () => {
1389
+ // Use UUID-like format that matches the isLikelyUuid check
1390
+ const testServerUuid = 'srv0uuid0test0001234567';
1391
+ const mockServer = {
1392
+ id: 1,
1393
+ uuid: testServerUuid,
1394
+ name: 'test-server',
1395
+ ip: '192.168.1.1',
1396
+ user: 'root',
1397
+ port: 22,
1398
+ status: 'running',
1399
+ is_reachable: true,
1400
+ is_usable: true,
1401
+ created_at: '2024-01-01',
1402
+ updated_at: '2024-01-01',
1403
+ };
1404
+ const mockResources = [
1405
+ {
1406
+ id: 1,
1407
+ uuid: 'res-1',
1408
+ name: 'app-1',
1409
+ type: 'application',
1410
+ status: 'running:healthy',
1411
+ created_at: '2024-01-01',
1412
+ updated_at: '2024-01-01',
1413
+ },
1414
+ {
1415
+ id: 2,
1416
+ uuid: 'res-2',
1417
+ name: 'db-1',
1418
+ type: 'database',
1419
+ status: 'running:healthy',
1420
+ created_at: '2024-01-01',
1421
+ updated_at: '2024-01-01',
1422
+ },
1423
+ ];
1424
+ const mockDomains = [{ ip: '192.168.1.1', domains: ['example.com', 'api.example.com'] }];
1425
+ const mockValidation = { message: 'Server is reachable and validated' };
1426
+ it('should aggregate all server data successfully', async () => {
1427
+ mockFetch
1428
+ .mockResolvedValueOnce(mockResponse(mockServer))
1429
+ .mockResolvedValueOnce(mockResponse(mockResources))
1430
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1431
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1432
+ const result = await client.diagnoseServer(testServerUuid);
1433
+ expect(result.server).toEqual({
1434
+ uuid: testServerUuid,
1435
+ name: 'test-server',
1436
+ ip: '192.168.1.1',
1437
+ status: 'running',
1438
+ is_reachable: true,
1439
+ });
1440
+ expect(result.health.status).toBe('healthy');
1441
+ expect(result.resources).toHaveLength(2);
1442
+ expect(result.domains).toHaveLength(1);
1443
+ expect(result.validation?.message).toBe('Server is reachable and validated');
1444
+ expect(result.errors).toBeUndefined();
1445
+ });
1446
+ it('should detect unreachable server', async () => {
1447
+ const unreachableServer = { ...mockServer, is_reachable: false };
1448
+ mockFetch
1449
+ .mockResolvedValueOnce(mockResponse(unreachableServer))
1450
+ .mockResolvedValueOnce(mockResponse(mockResources))
1451
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1452
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1453
+ const result = await client.diagnoseServer(testServerUuid);
1454
+ expect(result.health.status).toBe('unhealthy');
1455
+ expect(result.health.issues).toContain('Server is not reachable');
1456
+ });
1457
+ it('should detect unhealthy resources', async () => {
1458
+ const unhealthyResources = [
1459
+ { ...mockResources[0], status: 'exited:unhealthy' },
1460
+ { ...mockResources[1], status: 'running:healthy' },
1461
+ ];
1462
+ mockFetch
1463
+ .mockResolvedValueOnce(mockResponse(mockServer))
1464
+ .mockResolvedValueOnce(mockResponse(unhealthyResources))
1465
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1466
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1467
+ const result = await client.diagnoseServer(testServerUuid);
1468
+ expect(result.health.issues).toContain('1 unhealthy resource(s)');
1469
+ });
1470
+ it('should handle partial failures gracefully', async () => {
1471
+ mockFetch
1472
+ .mockResolvedValueOnce(mockResponse(mockServer))
1473
+ .mockRejectedValueOnce(new Error('Resources unavailable'))
1474
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1475
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1476
+ const result = await client.diagnoseServer(testServerUuid);
1477
+ expect(result.server).not.toBeNull();
1478
+ expect(result.resources).toEqual([]);
1479
+ expect(result.errors).toContain('resources: Resources unavailable');
1480
+ });
1481
+ it('should find server by name and diagnose it', async () => {
1482
+ const mockServers = [{ ...mockServer, uuid: 'found-uuid', name: 'coolify-apps' }];
1483
+ mockFetch
1484
+ .mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
1485
+ .mockResolvedValueOnce(mockResponse(mockServer))
1486
+ .mockResolvedValueOnce(mockResponse(mockResources))
1487
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1488
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1489
+ const result = await client.diagnoseServer('coolify-apps');
1490
+ expect(result.server).not.toBeNull();
1491
+ // First call should be to list servers for lookup
1492
+ expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/servers', expect.any(Object));
1493
+ });
1494
+ it('should find server by IP and diagnose it', async () => {
1495
+ const mockServers = [{ ...mockServer, uuid: 'found-uuid', ip: '10.0.0.5' }];
1496
+ mockFetch
1497
+ .mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
1498
+ .mockResolvedValueOnce(mockResponse(mockServer))
1499
+ .mockResolvedValueOnce(mockResponse(mockResources))
1500
+ .mockResolvedValueOnce(mockResponse(mockDomains))
1501
+ .mockResolvedValueOnce(mockResponse(mockValidation));
1502
+ const result = await client.diagnoseServer('10.0.0.5');
1503
+ expect(result.server).not.toBeNull();
1504
+ });
1505
+ it('should return error in result when server not found by name', async () => {
1506
+ mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty server list
1507
+ const result = await client.diagnoseServer('nonexistent-server');
1508
+ expect(result.server).toBeNull();
1509
+ expect(result.errors).toContain('No server found matching "nonexistent-server"');
1510
+ });
1511
+ });
1512
+ describe('findInfrastructureIssues', () => {
1513
+ const mockServers = [
1514
+ {
1515
+ id: 1,
1516
+ uuid: 'server-1',
1517
+ name: 'healthy-server',
1518
+ ip: '1.1.1.1',
1519
+ user: 'root',
1520
+ port: 22,
1521
+ is_reachable: true,
1522
+ created_at: '2024-01-01',
1523
+ updated_at: '2024-01-01',
1524
+ },
1525
+ {
1526
+ id: 2,
1527
+ uuid: 'server-2',
1528
+ name: 'unreachable-server',
1529
+ ip: '2.2.2.2',
1530
+ user: 'root',
1531
+ port: 22,
1532
+ is_reachable: false,
1533
+ status: 'error',
1534
+ created_at: '2024-01-01',
1535
+ updated_at: '2024-01-01',
1536
+ },
1537
+ ];
1538
+ const mockApplications = [
1539
+ {
1540
+ id: 1,
1541
+ uuid: 'app-1',
1542
+ name: 'healthy-app',
1543
+ status: 'running:healthy',
1544
+ created_at: '2024-01-01',
1545
+ updated_at: '2024-01-01',
1546
+ },
1547
+ {
1548
+ id: 2,
1549
+ uuid: 'app-2',
1550
+ name: 'unhealthy-app',
1551
+ status: 'exited:unhealthy',
1552
+ created_at: '2024-01-01',
1553
+ updated_at: '2024-01-01',
1554
+ },
1555
+ ];
1556
+ const mockDatabases = [
1557
+ {
1558
+ id: 1,
1559
+ uuid: 'db-1',
1560
+ name: 'healthy-db',
1561
+ type: 'postgresql',
1562
+ status: 'running:healthy',
1563
+ is_public: false,
1564
+ image: 'postgres:16',
1565
+ created_at: '2024-01-01',
1566
+ updated_at: '2024-01-01',
1567
+ },
1568
+ {
1569
+ id: 2,
1570
+ uuid: 'db-2',
1571
+ name: 'stopped-db',
1572
+ type: 'redis',
1573
+ status: 'exited:unhealthy',
1574
+ is_public: false,
1575
+ image: 'redis:7',
1576
+ created_at: '2024-01-01',
1577
+ updated_at: '2024-01-01',
1578
+ },
1579
+ ];
1580
+ const mockServices = [
1581
+ {
1582
+ id: 1,
1583
+ uuid: 'svc-1',
1584
+ name: 'healthy-service',
1585
+ type: 'pocketbase',
1586
+ status: 'running:healthy',
1587
+ created_at: '2024-01-01',
1588
+ updated_at: '2024-01-01',
1589
+ },
1590
+ {
1591
+ id: 2,
1592
+ uuid: 'svc-2',
1593
+ name: 'exited-service',
1594
+ type: 'n8n',
1595
+ status: 'exited',
1596
+ created_at: '2024-01-01',
1597
+ updated_at: '2024-01-01',
1598
+ },
1599
+ ];
1600
+ it('should find all infrastructure issues', async () => {
1601
+ mockFetch
1602
+ .mockResolvedValueOnce(mockResponse(mockServers))
1603
+ .mockResolvedValueOnce(mockResponse(mockApplications))
1604
+ .mockResolvedValueOnce(mockResponse(mockDatabases))
1605
+ .mockResolvedValueOnce(mockResponse(mockServices));
1606
+ const result = await client.findInfrastructureIssues();
1607
+ expect(result.summary.total_issues).toBe(4);
1608
+ expect(result.summary.unreachable_servers).toBe(1);
1609
+ expect(result.summary.unhealthy_applications).toBe(1);
1610
+ expect(result.summary.unhealthy_databases).toBe(1);
1611
+ expect(result.summary.unhealthy_services).toBe(1);
1612
+ expect(result.issues).toHaveLength(4);
1613
+ expect(result.errors).toBeUndefined();
1614
+ });
1615
+ it('should return empty issues when everything is healthy', async () => {
1616
+ const healthyServers = [mockServers[0]];
1617
+ const healthyApps = [mockApplications[0]];
1618
+ const healthyDbs = [mockDatabases[0]];
1619
+ const healthySvcs = [mockServices[0]];
1620
+ mockFetch
1621
+ .mockResolvedValueOnce(mockResponse(healthyServers))
1622
+ .mockResolvedValueOnce(mockResponse(healthyApps))
1623
+ .mockResolvedValueOnce(mockResponse(healthyDbs))
1624
+ .mockResolvedValueOnce(mockResponse(healthySvcs));
1625
+ const result = await client.findInfrastructureIssues();
1626
+ expect(result.summary.total_issues).toBe(0);
1627
+ expect(result.issues).toHaveLength(0);
1628
+ });
1629
+ it('should handle partial failures and still report issues', async () => {
1630
+ mockFetch
1631
+ .mockResolvedValueOnce(mockResponse(mockServers))
1632
+ .mockRejectedValueOnce(new Error('Applications unavailable'))
1633
+ .mockResolvedValueOnce(mockResponse(mockDatabases))
1634
+ .mockResolvedValueOnce(mockResponse(mockServices));
1635
+ const result = await client.findInfrastructureIssues();
1636
+ expect(result.summary.unreachable_servers).toBe(1);
1637
+ expect(result.summary.unhealthy_databases).toBe(1);
1638
+ expect(result.summary.unhealthy_services).toBe(1);
1639
+ expect(result.summary.unhealthy_applications).toBe(0); // Failed to fetch
1640
+ expect(result.errors).toContain('applications: Applications unavailable');
1641
+ });
1642
+ });
1643
+ });
1104
1644
  });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Integration tests for diagnostic tools.
3
+ *
4
+ * These tests hit the real Coolify API to verify diagnostic methods work correctly.
5
+ * They are skipped in CI and should be run manually when testing against a real instance.
6
+ *
7
+ * Prerequisites:
8
+ * - COOLIFY_URL and COOLIFY_TOKEN environment variables set (from .env)
9
+ * - Access to a running Coolify instance
10
+ *
11
+ * Run with: npm run test:integration
12
+ */
13
+ export {};