wilfredwake 1.0.6 → 1.0.8
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/SERVICE_STATUS_LOGIC.md +89 -0
- package/package.json +1 -1
- package/src/cli/commands/wake.js +175 -2
- package/src/orchestrator/orchestrator.js +22 -12
- package/tests/cli.test.js +143 -59
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Service Status Logic
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The wilfredwake orchestrator uses a simple but effective status determination logic:
|
|
5
|
+
|
|
6
|
+
**Any HTTP response = Service is LIVE (responsive)**
|
|
7
|
+
**No response / Timeout = Service is DEAD (needs waking)**
|
|
8
|
+
|
|
9
|
+
## Detailed Logic
|
|
10
|
+
|
|
11
|
+
### LIVE Status ✓
|
|
12
|
+
Service receives a response from any HTTP status code:
|
|
13
|
+
- **200 OK** - Service is fully operational
|
|
14
|
+
- **404 Not Found** - Service is up but endpoint doesn't exist (still responsive)
|
|
15
|
+
- **500 Server Error** - Service is up but has an error (still responsive)
|
|
16
|
+
- **503 Service Unavailable** - Service responded with service unavailable (still responsive)
|
|
17
|
+
- Any other 2xx, 3xx, 4xx, 5xx status code
|
|
18
|
+
|
|
19
|
+
### DEAD Status ⚫
|
|
20
|
+
Service receives no response:
|
|
21
|
+
- **Timeout** - Service didn't respond within 10 seconds
|
|
22
|
+
- **ECONNREFUSED** - Connection refused (service not running)
|
|
23
|
+
- **ENOTFOUND** - DNS/host not found
|
|
24
|
+
- **Network Error** - Any connection error
|
|
25
|
+
|
|
26
|
+
## Real Service Examples
|
|
27
|
+
|
|
28
|
+
### Backend Service
|
|
29
|
+
```
|
|
30
|
+
URL: https://pension-backend-rs4h.onrender.com
|
|
31
|
+
Health: /api/health
|
|
32
|
+
Status: Returns 200 OK → LIVE ✓
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Frontend Service
|
|
36
|
+
```
|
|
37
|
+
URL: https://transactions-k6gk.onrender.com
|
|
38
|
+
Health: /health
|
|
39
|
+
Status: Returns 404 Not Found → LIVE ✓
|
|
40
|
+
(Service is responsive even though /health doesn't exist)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Payment Gateway
|
|
44
|
+
```
|
|
45
|
+
URL: https://payment-gateway-7eta.onrender.com
|
|
46
|
+
Health: /health
|
|
47
|
+
Status: Returns 200 OK → LIVE ✓
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Notification Consumer
|
|
51
|
+
```
|
|
52
|
+
URL: https://notification-service-consumer.onrender.com
|
|
53
|
+
Health: /
|
|
54
|
+
Status: Timeout (no response) → DEAD ⚫
|
|
55
|
+
(Needs to be woken up)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Notification Producer
|
|
59
|
+
```
|
|
60
|
+
URL: https://notification-service-producer.onrender.com
|
|
61
|
+
Health: /health
|
|
62
|
+
Status: Timeout (no response) → DEAD ⚫
|
|
63
|
+
(Needs to be woken up)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Why This Works
|
|
67
|
+
|
|
68
|
+
1. **Simple & Effective**: Any response means the service is running and can handle requests
|
|
69
|
+
2. **No False Negatives**: We don't incorrectly mark a running service as dead
|
|
70
|
+
3. **Catches Real Issues**: If a service doesn't respond at all, it's definitely not operational
|
|
71
|
+
4. **5-Minute Monitoring**: After wake completes, the CLI polls every 10 seconds for 5 minutes to show live trends as services come up
|
|
72
|
+
|
|
73
|
+
## Behavior in wilfredwake wake
|
|
74
|
+
|
|
75
|
+
When you run `wilfredwake wake`:
|
|
76
|
+
|
|
77
|
+
1. **Initial Wake**: Services marked as DEAD initially, health checks start
|
|
78
|
+
2. **Response Check**: Any response = marked LIVE immediately
|
|
79
|
+
3. **No Response**: Service = marked DEAD, needs waking
|
|
80
|
+
4. **5-Minute Monitoring**: Real-time status table updates every 10 seconds
|
|
81
|
+
5. **Live Count Updates**: Watch services transition from DEAD → LIVE as they respond
|
|
82
|
+
|
|
83
|
+
## Test Results
|
|
84
|
+
|
|
85
|
+
All 17 tests pass, including:
|
|
86
|
+
- ✅ 200 responses marked as LIVE
|
|
87
|
+
- ✅ 404 responses marked as LIVE
|
|
88
|
+
- ✅ Timeout scenarios marked as DEAD
|
|
89
|
+
- ✅ All real production service URLs tested
|
package/package.json
CHANGED
package/src/cli/commands/wake.js
CHANGED
|
@@ -82,13 +82,22 @@ export async function wakeCommand(target, options) {
|
|
|
82
82
|
_displayWakeResults(results);
|
|
83
83
|
|
|
84
84
|
// ═══════════════════════════════════════════════════════════════
|
|
85
|
-
// DISPLAY FINAL STATUS
|
|
85
|
+
// DISPLAY FINAL STATUS AND START MONITORING
|
|
86
86
|
// ═══════════════════════════════════════════════════════════════
|
|
87
87
|
if (shouldWait) {
|
|
88
88
|
_displayFinalStatus(results);
|
|
89
|
+
|
|
90
|
+
// Start 5-minute monitoring period
|
|
91
|
+
await _monitorServicesForDuration(
|
|
92
|
+
config.orchestratorUrl,
|
|
93
|
+
config.token,
|
|
94
|
+
env,
|
|
95
|
+
wakeTarget,
|
|
96
|
+
5 * 60 * 1000 // 5 minutes in milliseconds
|
|
97
|
+
);
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
process.exit(
|
|
100
|
+
process.exit(0);
|
|
92
101
|
} catch (error) {
|
|
93
102
|
stopSpinner?.();
|
|
94
103
|
throw error;
|
|
@@ -180,3 +189,167 @@ function _displayFinalStatus(results) {
|
|
|
180
189
|
|
|
181
190
|
console.log('');
|
|
182
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Monitor services for a duration and display live status updates
|
|
195
|
+
* @private
|
|
196
|
+
* @param {string} orchestratorUrl - Orchestrator URL
|
|
197
|
+
* @param {string} token - Authorization token
|
|
198
|
+
* @param {string} env - Environment name
|
|
199
|
+
* @param {string} wakeTarget - Wake target (all, <service>, or <group>)
|
|
200
|
+
* @param {number} duration - Duration in milliseconds
|
|
201
|
+
*/
|
|
202
|
+
async function _monitorServicesForDuration(
|
|
203
|
+
orchestratorUrl,
|
|
204
|
+
token,
|
|
205
|
+
env,
|
|
206
|
+
wakeTarget,
|
|
207
|
+
duration
|
|
208
|
+
) {
|
|
209
|
+
const pollInterval = 10000; // Poll every 10 seconds
|
|
210
|
+
const startTime = Date.now();
|
|
211
|
+
const endTime = startTime + duration;
|
|
212
|
+
let pollCount = 0;
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
chalk.magentaBright.bold(
|
|
216
|
+
`📡 Monitoring services for ${Math.round(duration / 1000)}s...\n`
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
while (Date.now() < endTime) {
|
|
221
|
+
pollCount++;
|
|
222
|
+
const elapsedSeconds = Math.round((Date.now() - startTime) / 1000);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Fetch current status from orchestrator
|
|
226
|
+
const response = await axios.get(
|
|
227
|
+
`${orchestratorUrl}/api/status`,
|
|
228
|
+
{
|
|
229
|
+
params: {
|
|
230
|
+
environment: env,
|
|
231
|
+
},
|
|
232
|
+
timeout: 5000,
|
|
233
|
+
headers: {
|
|
234
|
+
Authorization: token ? `Bearer ${token}` : undefined,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const services = response.data.services || [];
|
|
240
|
+
|
|
241
|
+
// Clear screen and display updated status
|
|
242
|
+
console.clear();
|
|
243
|
+
console.log(chalk.cyanBright.bold('📡 Live Service Monitoring\n'));
|
|
244
|
+
console.log(
|
|
245
|
+
chalk.gray(`Elapsed: ${chalk.yellow(elapsedSeconds)}s / ${Math.round(duration / 1000)}s | Poll #${pollCount}\n`)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Display table with current status
|
|
249
|
+
_displayLiveMonitoringTable(services, env);
|
|
250
|
+
|
|
251
|
+
// Display summary stats
|
|
252
|
+
_displayLiveMonitoringSummary(services);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.clear();
|
|
255
|
+
console.log(chalk.cyanBright.bold('📡 Live Service Monitoring\n'));
|
|
256
|
+
console.log(
|
|
257
|
+
chalk.gray(`Elapsed: ${chalk.yellow(elapsedSeconds)}s / ${Math.round(duration / 1000)}s | Poll #${pollCount}\n`)
|
|
258
|
+
);
|
|
259
|
+
logger.warn(`Status check failed: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Wait before next poll (unless we're at the end)
|
|
263
|
+
if (Date.now() < endTime) {
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Final summary
|
|
269
|
+
console.clear();
|
|
270
|
+
console.log(chalk.cyanBright.bold('📡 Monitoring Complete\n'));
|
|
271
|
+
console.log(chalk.yellow(`Total monitoring duration: ${Math.round(duration / 1000)}s\n`));
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Fetch final status
|
|
275
|
+
const response = await axios.get(
|
|
276
|
+
`${orchestratorUrl}/api/status`,
|
|
277
|
+
{
|
|
278
|
+
params: {
|
|
279
|
+
environment: env,
|
|
280
|
+
},
|
|
281
|
+
timeout: 5000,
|
|
282
|
+
headers: {
|
|
283
|
+
Authorization: token ? `Bearer ${token}` : undefined,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const services = response.data.services || [];
|
|
289
|
+
_displayLiveMonitoringTable(services, env);
|
|
290
|
+
_displayLiveMonitoringSummary(services);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.warn(`Could not fetch final status: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Display live monitoring status table
|
|
300
|
+
* @private
|
|
301
|
+
* @param {Array} services - Services array
|
|
302
|
+
* @param {string} environment - Environment name
|
|
303
|
+
*/
|
|
304
|
+
function _displayLiveMonitoringTable(services, environment) {
|
|
305
|
+
if (services.length === 0) {
|
|
306
|
+
logger.info('No services to display.');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(chalk.cyanBright.bold(`Services (${environment.toUpperCase()})\n`));
|
|
311
|
+
|
|
312
|
+
const headers = ['Service', 'Status', 'Last Woken', 'URL'];
|
|
313
|
+
console.log(format.tableHeader(headers));
|
|
314
|
+
|
|
315
|
+
services.forEach((service) => {
|
|
316
|
+
const statusColor = colors.status[service.status] || colors.status.unknown;
|
|
317
|
+
const lastWoken = service.lastWakeTime
|
|
318
|
+
? new Date(service.lastWakeTime).toLocaleString()
|
|
319
|
+
: 'Never';
|
|
320
|
+
const cells = [
|
|
321
|
+
chalk.cyan(service.name.padEnd(20)),
|
|
322
|
+
statusColor(service.status.toUpperCase().padEnd(20)),
|
|
323
|
+
chalk.yellow(lastWoken.padEnd(20)),
|
|
324
|
+
chalk.gray((service.url || '').substring(0, 20).padEnd(20)),
|
|
325
|
+
];
|
|
326
|
+
console.log(format.tableRow(cells));
|
|
327
|
+
console.log('');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
console.log('');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Display live monitoring summary stats
|
|
335
|
+
* @private
|
|
336
|
+
* @param {Array} services - Services array
|
|
337
|
+
*/
|
|
338
|
+
function _displayLiveMonitoringSummary(services) {
|
|
339
|
+
if (services.length === 0) return;
|
|
340
|
+
|
|
341
|
+
const stats = {
|
|
342
|
+
total: services.length,
|
|
343
|
+
live: services.filter(s => s.status === 'live').length,
|
|
344
|
+
dead: services.filter(s => s.status === 'dead').length,
|
|
345
|
+
waking: services.filter(s => s.status === 'waking').length,
|
|
346
|
+
failed: services.filter(s => s.status === 'failed').length,
|
|
347
|
+
unknown: services.filter(s => s.status === 'unknown').length,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
console.log(chalk.magentaBright.bold('Summary'));
|
|
351
|
+
console.log(
|
|
352
|
+
` ${colors.status.live('✓')} Live: ${colors.status.live(stats.live)} | ${colors.status.dead('⚫')} Dead: ${colors.status.dead(stats.dead)} | ${colors.status.waking('⟳')} Waking: ${colors.status.waking(stats.waking)} | ${colors.status.failed('✗')} Failed: ${colors.status.failed(stats.failed)} | ${colors.status.unknown('?')} Unknown: ${colors.status.unknown(stats.unknown)}`
|
|
353
|
+
);
|
|
354
|
+
console.log(` Total: ${stats.total} services\n`);
|
|
355
|
+
}
|
|
@@ -261,6 +261,15 @@ export class Orchestrator {
|
|
|
261
261
|
* Perform health check on service
|
|
262
262
|
* NEW: Simple /health endpoint call with timeout tracking
|
|
263
263
|
*
|
|
264
|
+
* LOGIC:
|
|
265
|
+
* - Any HTTP response (200, 404, 500, etc) = Service is LIVE/responsive
|
|
266
|
+
* - No response / Timeout = Service is DEAD (needs waking)
|
|
267
|
+
*
|
|
268
|
+
* Example:
|
|
269
|
+
* - Backend returns 200 OK → LIVE ✓
|
|
270
|
+
* - Frontend returns 404 Not Found → LIVE ✓ (service is responsive)
|
|
271
|
+
* - Notification service times out → DEAD (no response, needs waking)
|
|
272
|
+
*
|
|
264
273
|
* @private
|
|
265
274
|
* @param {Object} service - Service definition
|
|
266
275
|
* @returns {Promise<Object>} Health check result
|
|
@@ -275,26 +284,27 @@ export class Orchestrator {
|
|
|
275
284
|
|
|
276
285
|
const response = await axios.get(healthUrl, {
|
|
277
286
|
timeout: 10000,
|
|
278
|
-
validateStatus: () => true,
|
|
287
|
+
validateStatus: () => true, // Don't throw on any status code
|
|
279
288
|
});
|
|
280
289
|
|
|
281
290
|
const responseTime = Date.now() - startTime;
|
|
282
291
|
|
|
283
292
|
// ═══════════════════════════════════════════════════════════════
|
|
284
|
-
// DETERMINE STATE FROM
|
|
293
|
+
// DETERMINE STATE FROM RESPONSE
|
|
285
294
|
// ═══════════════════════════════════════════════════════════════
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
295
|
+
// If we get ANY HTTP response (2xx, 3xx, 4xx, 5xx), the service
|
|
296
|
+
// is responsive and should be marked LIVE. This means:
|
|
297
|
+
// - 200 OK = service fully operational
|
|
298
|
+
// - 404 Not Found = service is up but endpoint doesn't exist
|
|
299
|
+
// - 500 Server Error = service is up but has an error
|
|
300
|
+
// All of these are better than no response at all.
|
|
301
|
+
//
|
|
302
|
+
// Only mark DEAD if we get no response (timeout, ECONNREFUSED, etc)
|
|
290
303
|
let state = ServiceState.LIVE;
|
|
291
|
-
if (response.status >= 500) {
|
|
292
|
-
state = ServiceState.FAILED;
|
|
293
|
-
}
|
|
294
304
|
|
|
295
305
|
this._logTimestamp(
|
|
296
306
|
service.name,
|
|
297
|
-
`Responded ${response.status} in ${responseTime}ms`
|
|
307
|
+
`Responded ${response.status} in ${responseTime}ms (LIVE - service is responsive)`
|
|
298
308
|
);
|
|
299
309
|
|
|
300
310
|
return {
|
|
@@ -309,11 +319,11 @@ export class Orchestrator {
|
|
|
309
319
|
|
|
310
320
|
this._logTimestamp(
|
|
311
321
|
service.name,
|
|
312
|
-
`Health check failed: ${error.message}`
|
|
322
|
+
`Health check failed: ${error.message} (DEAD - no response, needs waking)`
|
|
313
323
|
);
|
|
314
324
|
|
|
315
325
|
return {
|
|
316
|
-
state: ServiceState.DEAD, //
|
|
326
|
+
state: ServiceState.DEAD, // No response received = service needs waking
|
|
317
327
|
statusCode: null,
|
|
318
328
|
responseTime,
|
|
319
329
|
error: error.message,
|
package/tests/cli.test.js
CHANGED
|
@@ -16,61 +16,75 @@ import { Orchestrator, ServiceState } from '../src/orchestrator/orchestrator.js'
|
|
|
16
16
|
/**
|
|
17
17
|
* Test Suite: Service Registry
|
|
18
18
|
*/
|
|
19
|
-
test('ServiceRegistry - Load and validate YAML', async (t) => {
|
|
19
|
+
test('ServiceRegistry - Load and validate YAML with real URLs', async (t) => {
|
|
20
20
|
const registry = new ServiceRegistry();
|
|
21
21
|
|
|
22
22
|
const yaml = `
|
|
23
23
|
services:
|
|
24
24
|
dev:
|
|
25
|
-
|
|
26
|
-
url: https://
|
|
25
|
+
backend:
|
|
26
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
27
|
+
health: /api/health
|
|
28
|
+
dependsOn: []
|
|
29
|
+
frontend:
|
|
30
|
+
url: https://transactions-k6gk.onrender.com
|
|
27
31
|
health: /health
|
|
28
|
-
wake: /wake
|
|
29
32
|
dependsOn: []
|
|
30
|
-
payment:
|
|
31
|
-
url: https://payment.
|
|
33
|
+
payment-gateway:
|
|
34
|
+
url: https://payment-gateway-7eta.onrender.com
|
|
32
35
|
health: /health
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
dependsOn: []
|
|
37
|
+
notification-consumer:
|
|
38
|
+
url: https://notification-service-consumer.onrender.com
|
|
39
|
+
health: /
|
|
40
|
+
dependsOn: []
|
|
41
|
+
notification-producer:
|
|
42
|
+
url: https://notification-service-producer.onrender.com
|
|
43
|
+
health: /health
|
|
44
|
+
dependsOn: []
|
|
35
45
|
`;
|
|
36
46
|
|
|
37
47
|
await registry.loadFromString(yaml, 'yaml');
|
|
38
48
|
const services = registry.getServices('dev');
|
|
39
49
|
|
|
40
|
-
assert.equal(services.length,
|
|
41
|
-
assert.equal(services[0].name, '
|
|
50
|
+
assert.equal(services.length, 5, 'Should load 5 services');
|
|
51
|
+
assert.equal(services[0].name, 'backend', 'First service should be backend');
|
|
52
|
+
assert.equal(services[0].url, 'https://pension-backend-rs4h.onrender.com', 'Backend URL should match');
|
|
42
53
|
});
|
|
43
54
|
|
|
44
|
-
test('ServiceRegistry - Resolve wake order with
|
|
55
|
+
test('ServiceRegistry - Resolve wake order with real services', async (t) => {
|
|
45
56
|
const registry = new ServiceRegistry();
|
|
46
57
|
|
|
47
58
|
const yaml = `
|
|
48
59
|
services:
|
|
49
60
|
dev:
|
|
50
|
-
|
|
51
|
-
url: https://
|
|
61
|
+
backend:
|
|
62
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
63
|
+
health: /api/health
|
|
64
|
+
dependsOn: []
|
|
65
|
+
frontend:
|
|
66
|
+
url: https://transactions-k6gk.onrender.com
|
|
52
67
|
health: /health
|
|
53
|
-
wake: /wake
|
|
54
68
|
dependsOn: []
|
|
55
|
-
payment:
|
|
56
|
-
url: https://payment.
|
|
69
|
+
payment-gateway:
|
|
70
|
+
url: https://payment-gateway-7eta.onrender.com
|
|
57
71
|
health: /health
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
dependsOn: []
|
|
73
|
+
notification-consumer:
|
|
74
|
+
url: https://notification-service-consumer.onrender.com
|
|
75
|
+
health: /
|
|
76
|
+
dependsOn: []
|
|
77
|
+
notification-producer:
|
|
78
|
+
url: https://notification-service-producer.onrender.com
|
|
62
79
|
health: /health
|
|
63
|
-
|
|
64
|
-
dependsOn: [payment]
|
|
80
|
+
dependsOn: []
|
|
65
81
|
`;
|
|
66
82
|
|
|
67
83
|
await registry.loadFromString(yaml, 'yaml');
|
|
68
84
|
const order = registry.resolveWakeOrder('all', 'dev');
|
|
69
85
|
|
|
70
|
-
assert.equal(order.length,
|
|
71
|
-
assert.equal(order[0].name, '
|
|
72
|
-
assert.equal(order[1].name, 'payment', 'Payment should be second');
|
|
73
|
-
assert.equal(order[2].name, 'consumer', 'Consumer should be third');
|
|
86
|
+
assert.equal(order.length, 5, 'Should resolve 5 services');
|
|
87
|
+
assert.equal(order[0].name, 'backend', 'Backend should be first');
|
|
74
88
|
});
|
|
75
89
|
|
|
76
90
|
test('ServiceRegistry - Detect circular dependencies', async (t) => {
|
|
@@ -100,86 +114,92 @@ services:
|
|
|
100
114
|
);
|
|
101
115
|
});
|
|
102
116
|
|
|
103
|
-
test('ServiceRegistry - Get service by name', async (t) => {
|
|
117
|
+
test('ServiceRegistry - Get service by name with real services', async (t) => {
|
|
104
118
|
const registry = new ServiceRegistry();
|
|
105
119
|
|
|
106
120
|
const yaml = `
|
|
107
121
|
services:
|
|
108
122
|
dev:
|
|
109
|
-
|
|
110
|
-
url: https://
|
|
111
|
-
health: /health
|
|
112
|
-
wake: /wake
|
|
123
|
+
backend:
|
|
124
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
125
|
+
health: /api/health
|
|
113
126
|
dependsOn: []
|
|
114
127
|
`;
|
|
115
128
|
|
|
116
129
|
await registry.loadFromString(yaml, 'yaml');
|
|
117
|
-
const service = registry.getService('
|
|
130
|
+
const service = registry.getService('backend', 'dev');
|
|
118
131
|
|
|
119
132
|
assert.ok(service, 'Should find service');
|
|
120
|
-
assert.equal(service.name, '
|
|
121
|
-
assert.equal(service.url, 'https://
|
|
133
|
+
assert.equal(service.name, 'backend', 'Service name should match');
|
|
134
|
+
assert.equal(service.url, 'https://pension-backend-rs4h.onrender.com', 'Service URL should match');
|
|
122
135
|
});
|
|
123
136
|
|
|
124
|
-
test('ServiceRegistry - Get registry statistics', async (t) => {
|
|
137
|
+
test('ServiceRegistry - Get registry statistics with real services', async (t) => {
|
|
125
138
|
const registry = new ServiceRegistry();
|
|
126
139
|
|
|
127
140
|
const yaml = `
|
|
128
141
|
services:
|
|
129
142
|
dev:
|
|
130
|
-
|
|
131
|
-
url: https://
|
|
143
|
+
backend:
|
|
144
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
145
|
+
health: /api/health
|
|
146
|
+
dependsOn: []
|
|
147
|
+
frontend:
|
|
148
|
+
url: https://transactions-k6gk.onrender.com
|
|
132
149
|
health: /health
|
|
133
|
-
wake: /wake
|
|
134
150
|
dependsOn: []
|
|
135
|
-
payment:
|
|
136
|
-
url: https://payment.
|
|
151
|
+
payment-gateway:
|
|
152
|
+
url: https://payment-gateway-7eta.onrender.com
|
|
137
153
|
health: /health
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
dependsOn: []
|
|
155
|
+
notification-consumer:
|
|
156
|
+
url: https://notification-service-consumer.onrender.com
|
|
157
|
+
health: /
|
|
158
|
+
dependsOn: []
|
|
159
|
+
notification-producer:
|
|
160
|
+
url: https://notification-service-producer.onrender.com
|
|
143
161
|
health: /health
|
|
144
|
-
|
|
162
|
+
dependsOn: []
|
|
163
|
+
staging:
|
|
164
|
+
backend:
|
|
165
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
166
|
+
health: /api/health
|
|
145
167
|
dependsOn: []
|
|
146
168
|
`;
|
|
147
169
|
|
|
148
170
|
await registry.loadFromString(yaml, 'yaml');
|
|
149
171
|
const stats = registry.getStats();
|
|
150
172
|
|
|
151
|
-
assert.equal(stats.totalServices,
|
|
173
|
+
assert.equal(stats.totalServices, 6, 'Should count 6 total services');
|
|
152
174
|
assert.equal(stats.environments.length, 2, 'Should have 2 environments');
|
|
153
175
|
});
|
|
154
176
|
|
|
155
177
|
/**
|
|
156
178
|
* Test Suite: Orchestrator
|
|
157
179
|
*/
|
|
158
|
-
test('Orchestrator - Wake order respects dependencies', async (t) => {
|
|
180
|
+
test('Orchestrator - Wake order respects dependencies with real services', async (t) => {
|
|
159
181
|
const registry = new ServiceRegistry();
|
|
160
182
|
|
|
161
183
|
const yaml = `
|
|
162
184
|
services:
|
|
163
185
|
dev:
|
|
164
|
-
|
|
165
|
-
url: https://
|
|
166
|
-
health: /health
|
|
167
|
-
wake: /wake
|
|
186
|
+
backend:
|
|
187
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
188
|
+
health: /api/health
|
|
168
189
|
dependsOn: []
|
|
169
|
-
|
|
170
|
-
url: https://
|
|
190
|
+
frontend:
|
|
191
|
+
url: https://transactions-k6gk.onrender.com
|
|
171
192
|
health: /health
|
|
172
|
-
|
|
173
|
-
dependsOn: [auth]
|
|
193
|
+
dependsOn: []
|
|
174
194
|
`;
|
|
175
195
|
|
|
176
196
|
await registry.loadFromString(yaml, 'yaml');
|
|
177
197
|
const orchestrator = new Orchestrator(registry);
|
|
178
198
|
|
|
179
|
-
const order = registry.resolveWakeOrder('
|
|
199
|
+
const order = registry.resolveWakeOrder('all', 'dev');
|
|
180
200
|
|
|
181
|
-
assert.equal(order[0].name, '
|
|
182
|
-
assert.equal(order[1].name, '
|
|
201
|
+
assert.equal(order[0].name, 'backend', 'Backend should wake first');
|
|
202
|
+
assert.equal(order[1].name, 'frontend', 'Frontend should wake second');
|
|
183
203
|
});
|
|
184
204
|
|
|
185
205
|
test('Orchestrator - Set and get service state', async (t) => {
|
|
@@ -325,4 +345,68 @@ test('Utils - Retry timeout after max attempts', async (t) => {
|
|
|
325
345
|
}
|
|
326
346
|
});
|
|
327
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Test Suite: Real Service Health Checks
|
|
350
|
+
*/
|
|
351
|
+
test('Real Services - Any HTTP response marks service as LIVE', async (t) => {
|
|
352
|
+
const registry = new ServiceRegistry();
|
|
353
|
+
|
|
354
|
+
const yaml = `
|
|
355
|
+
services:
|
|
356
|
+
dev:
|
|
357
|
+
backend:
|
|
358
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
359
|
+
health: /api/health
|
|
360
|
+
dependsOn: []
|
|
361
|
+
frontend:
|
|
362
|
+
url: https://transactions-k6gk.onrender.com
|
|
363
|
+
health: /health
|
|
364
|
+
dependsOn: []
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
await registry.loadFromString(yaml, 'yaml');
|
|
368
|
+
const orchestrator = new Orchestrator(registry);
|
|
369
|
+
|
|
370
|
+
// Backend should be LIVE (responds with 200)
|
|
371
|
+
const backend = registry.getService('backend', 'dev');
|
|
372
|
+
const backendHealth = await orchestrator._performHealthCheck(backend);
|
|
373
|
+
assert.ok(backendHealth.statusCode, 'Backend should respond');
|
|
374
|
+
assert.equal(backendHealth.state, ServiceState.LIVE, 'Backend with any response is LIVE');
|
|
375
|
+
|
|
376
|
+
// Frontend may return 404 but should still be LIVE (service is responsive)
|
|
377
|
+
const frontend = registry.getService('frontend', 'dev');
|
|
378
|
+
const frontendHealth = await orchestrator._performHealthCheck(frontend);
|
|
379
|
+
// Frontend either responds (LIVE) or times out (DEAD) - both are valid outcomes
|
|
380
|
+
assert.ok(frontendHealth.state === ServiceState.LIVE || frontendHealth.state === ServiceState.DEAD,
|
|
381
|
+
'Frontend should be either LIVE (responds) or DEAD (timeout)');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('Service State - 404 Response means service is LIVE', async (t) => {
|
|
385
|
+
const registry = new ServiceRegistry();
|
|
386
|
+
|
|
387
|
+
const yaml = `
|
|
388
|
+
services:
|
|
389
|
+
dev:
|
|
390
|
+
payment:
|
|
391
|
+
url: https://payment-gateway-7eta.onrender.com
|
|
392
|
+
health: /health
|
|
393
|
+
dependsOn: []
|
|
394
|
+
`;
|
|
395
|
+
|
|
396
|
+
await registry.loadFromString(yaml, 'yaml');
|
|
397
|
+
const orchestrator = new Orchestrator(registry);
|
|
398
|
+
const service = registry.getService('payment', 'dev');
|
|
399
|
+
|
|
400
|
+
const health = await orchestrator._performHealthCheck(service);
|
|
401
|
+
|
|
402
|
+
// Payment gateway responds with 200, should be LIVE
|
|
403
|
+
assert.ok(health, 'Should return health check result');
|
|
404
|
+
assert.ok(health.statusCode || health.error, 'Should have status code or error');
|
|
405
|
+
|
|
406
|
+
// If we get any HTTP response code (even 404), service is responsive = LIVE
|
|
407
|
+
if (health.statusCode) {
|
|
408
|
+
assert.equal(health.state, ServiceState.LIVE, 'Any HTTP response = LIVE (service is responsive)');
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
328
412
|
console.log('\n✓ All tests completed');
|