@platformatic/watt-extra 0.1.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.
Files changed (95) hide show
  1. package/README.md +87 -0
  2. package/app.js +124 -0
  3. package/cli.js +141 -0
  4. package/clients/compliance/compliance-types.d.ts +887 -0
  5. package/clients/compliance/compliance.mjs +1049 -0
  6. package/clients/compliance/compliance.openapi.json +6127 -0
  7. package/clients/control-plane/control-plane-types.d.ts +2696 -0
  8. package/clients/control-plane/control-plane.mjs +3051 -0
  9. package/clients/control-plane/control-plane.openapi.json +13693 -0
  10. package/clients/cron/cron-types.d.ts +1479 -0
  11. package/clients/cron/cron.mjs +872 -0
  12. package/clients/cron/cron.openapi.json +9330 -0
  13. package/compliance/index.js +21 -0
  14. package/compliance/rules/dependencies.js +76 -0
  15. package/compliance/rules/utils.js +12 -0
  16. package/eslint.config.js +11 -0
  17. package/help/start.txt +12 -0
  18. package/help/watt-extra.txt +12 -0
  19. package/index.js +45 -0
  20. package/lib/banner.js +22 -0
  21. package/lib/errors.js +34 -0
  22. package/lib/utils.js +34 -0
  23. package/lib/wattpro.js +580 -0
  24. package/package.json +50 -0
  25. package/plugins/alerts.js +115 -0
  26. package/plugins/auth.js +89 -0
  27. package/plugins/compliancy.js +70 -0
  28. package/plugins/env.js +58 -0
  29. package/plugins/flamegraphs.js +100 -0
  30. package/plugins/init.js +70 -0
  31. package/plugins/metadata.js +84 -0
  32. package/plugins/scheduler.js +48 -0
  33. package/plugins/update.js +128 -0
  34. package/renovate.json +6 -0
  35. package/test/alerts.test.js +607 -0
  36. package/test/auth.test.js +128 -0
  37. package/test/auto-cache.test.js +401 -0
  38. package/test/cli.test.js +75 -0
  39. package/test/compliancy.test.js +87 -0
  40. package/test/fixtures/runtime-domains/alpha/package.json +5 -0
  41. package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
  42. package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
  43. package/test/fixtures/runtime-domains/beta/package.json +5 -0
  44. package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
  45. package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
  46. package/test/fixtures/runtime-domains/composer/package.json +5 -0
  47. package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
  48. package/test/fixtures/runtime-domains/package.json +1 -0
  49. package/test/fixtures/runtime-domains/platformatic.json +27 -0
  50. package/test/fixtures/runtime-health/package.json +20 -0
  51. package/test/fixtures/runtime-health/platformatic.json +16 -0
  52. package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
  53. package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
  54. package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
  55. package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
  56. package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
  57. package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
  58. package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
  59. package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
  60. package/test/fixtures/runtime-next/package.json +5 -0
  61. package/test/fixtures/runtime-next/platformatic.json +9 -0
  62. package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
  63. package/test/fixtures/runtime-next/web/next/package.json +7 -0
  64. package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
  65. package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
  66. package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
  67. package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
  68. package/test/fixtures/runtime-scheduler/main/package.json +5 -0
  69. package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
  70. package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
  71. package/test/fixtures/runtime-scheduler/package.json +1 -0
  72. package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
  73. package/test/fixtures/runtime-service/main/package.json +5 -0
  74. package/test/fixtures/runtime-service/main/platformatic.json +12 -0
  75. package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
  76. package/test/fixtures/runtime-service/package.json +1 -0
  77. package/test/fixtures/runtime-service/platformatic.json +19 -0
  78. package/test/fixtures/service-1/package.json +7 -0
  79. package/test/fixtures/service-1/platformatic.json +18 -0
  80. package/test/fixtures/service-1/routes/root.cjs +48 -0
  81. package/test/fixtures/service-2/platformatic.json +21 -0
  82. package/test/fixtures/service-2/routes/root.cjs +5 -0
  83. package/test/fixtures/service-3/package.json +5 -0
  84. package/test/fixtures/service-3/platformatic.json +21 -0
  85. package/test/fixtures/service-3/routes/root.cjs +8 -0
  86. package/test/health.test.js +44 -0
  87. package/test/helper.js +274 -0
  88. package/test/init.test.js +243 -0
  89. package/test/patch-config.test.js +434 -0
  90. package/test/scheduler.test.js +71 -0
  91. package/test/send-to-icc-retry.test.js +138 -0
  92. package/test/shared-context.test.js +82 -0
  93. package/test/spawn.test.js +110 -0
  94. package/test/trigger-flamegraphs.test.js +226 -0
  95. package/test/update.test.js +519 -0
@@ -0,0 +1,434 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { randomUUID } from 'node:crypto'
7
+ import { request } from 'undici'
8
+ import { setUpEnvironment, startICC, installDeps } from './helper.js'
9
+ import { start } from '../index.js'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = dirname(__filename)
13
+
14
+ test('should spawn a service app settings labels for metrics', async (t) => {
15
+ const applicationName = 'test-application'
16
+ const applicationId = randomUUID()
17
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
18
+
19
+ const icc = await startICC(t, {
20
+ applicationId,
21
+ applicationName,
22
+ enableOpenTelemetry: true,
23
+ })
24
+
25
+ setUpEnvironment({
26
+ PLT_APP_NAME: applicationName,
27
+ PLT_APP_DIR: applicationPath,
28
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
29
+ })
30
+
31
+ const app = await start()
32
+
33
+ t.after(async () => {
34
+ await app.close()
35
+ await icc.close()
36
+ })
37
+
38
+ const mainConfig = app.wattpro.runtime.getRuntimeConfig(true)
39
+
40
+ const { metrics, telemetry } = mainConfig
41
+
42
+ const expectedTelemetry = {
43
+ enabled: true,
44
+ applicationName: 'test-application',
45
+ skip: [
46
+ {
47
+ method: 'GET',
48
+ path: '/documentation',
49
+ },
50
+ {
51
+ method: 'GET',
52
+ path: '/documentation/json',
53
+ },
54
+ ],
55
+ exporter: {
56
+ type: 'otlp',
57
+ options: {
58
+ url: 'http://127.0.0.1:3000/risk-service/v1/traces',
59
+ headers: {
60
+ 'x-platformatic-application-id': applicationId,
61
+ },
62
+ keepAlive: true,
63
+ httpAgentOptions: {
64
+ rejectUnauthorized: false,
65
+ },
66
+ },
67
+ },
68
+ }
69
+ assert.deepStrictEqual(telemetry, expectedTelemetry)
70
+
71
+ const expectedMetrics = {
72
+ server: 'hide',
73
+ defaultMetrics: {
74
+ enabled: true,
75
+ },
76
+ hostname: '127.0.0.1',
77
+ port: 9090,
78
+ labels: {
79
+ serviceId: 'main',
80
+ instanceId: app.instanceId,
81
+ applicationId,
82
+ },
83
+ }
84
+ assert.deepStrictEqual(metrics, expectedMetrics)
85
+ })
86
+
87
+ test('should configure system resources', async (t) => {
88
+ const applicationName = 'test-next'
89
+ const applicationId = randomUUID()
90
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-next')
91
+
92
+ await installDeps(t, applicationPath, ['@platformatic/next', 'next'])
93
+ const { execa } = await import('execa')
94
+ await execa(join(__dirname, '../node_modules/.bin/plt'), ['build'], {
95
+ cwd: applicationPath,
96
+ })
97
+
98
+ const iccConfig = {
99
+ resources: {
100
+ threads: 1,
101
+ heap: 256,
102
+ services: [
103
+ {
104
+ name: 'next',
105
+ threads: 3,
106
+ heap: 200,
107
+ },
108
+ ],
109
+ },
110
+ }
111
+ const icc = await startICC(t, { applicationId, applicationName, iccConfig })
112
+
113
+ setUpEnvironment({
114
+ PLT_APP_NAME: applicationName,
115
+ PLT_APP_DIR: applicationPath,
116
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
117
+ })
118
+
119
+ const app = await start()
120
+
121
+ t.after(async () => {
122
+ await app.close()
123
+ await icc.close()
124
+ })
125
+ const config = await app.wattpro.runtime.getRuntimeConfig(true)
126
+
127
+ // Check generic resources
128
+ assert.strictEqual(config.workers, 1)
129
+ assert.strictEqual(config.health.maxHeapTotal, 256 * Math.pow(1024, 2))
130
+
131
+ // Check system resources
132
+ assert.strictEqual(config.applications[0].workers, 3)
133
+ assert.strictEqual(
134
+ config.applications[0].health.maxHeapTotal,
135
+ 200 * Math.pow(1024, 2)
136
+ )
137
+ })
138
+
139
+ test('should remove server https configs', async (t) => {
140
+ const appName = 'test-app'
141
+ const applicationId = randomUUID()
142
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
143
+
144
+ await installDeps(t, applicationPath)
145
+
146
+ const icc = await startICC(t, {
147
+ applicationId,
148
+ })
149
+
150
+ setUpEnvironment({
151
+ PLT_APP_NAME: appName,
152
+ PLT_APP_DIR: applicationPath,
153
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
154
+ PLT_CONTROL_PLANE_URL: 'http://127.0.0.1:3002',
155
+ })
156
+
157
+ const app = await start()
158
+
159
+ t.after(async () => {
160
+ await app.close()
161
+ await icc.close()
162
+ })
163
+
164
+ {
165
+ const mainConfig = app.wattpro.runtime.getRuntimeConfig(true)
166
+ const { server } = mainConfig
167
+ assert.strictEqual(server.https, undefined)
168
+ }
169
+
170
+ {
171
+ const runtimeConfig = await app.wattpro.runtime.getRuntimeConfig(true)
172
+
173
+ const { server } = runtimeConfig
174
+ assert.strictEqual(server.https, undefined)
175
+ }
176
+ })
177
+
178
+ test('should configure health options', async (t) => {
179
+ const appName = 'test-app'
180
+ const applicationId = randomUUID()
181
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
182
+
183
+ await installDeps(t, applicationPath)
184
+
185
+ const icc = await startICC(t, {
186
+ applicationId,
187
+ })
188
+
189
+ setUpEnvironment({
190
+ PLT_APP_NAME: appName,
191
+ PLT_APP_DIR: applicationPath,
192
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
193
+ })
194
+
195
+ const app = await start()
196
+
197
+ t.after(async () => {
198
+ await app.close()
199
+ await icc.close()
200
+ })
201
+
202
+ const runtimeConfig = await app.wattpro.runtime.getRuntimeConfig(true)
203
+
204
+ const { health } = runtimeConfig
205
+ assert.strictEqual(health.enabled, true)
206
+ assert.strictEqual(health.interval, 1000)
207
+ assert.strictEqual(health.maxUnhealthyChecks, 30)
208
+ })
209
+
210
+ test('should call updateServicesResources with maxHeapTotal', async (t) => {
211
+ const appName = 'test-update-resources'
212
+ const applicationId = randomUUID()
213
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
214
+
215
+ await installDeps(t, applicationPath)
216
+
217
+ const icc = await startICC(t, {
218
+ applicationId,
219
+ })
220
+
221
+ setUpEnvironment({
222
+ PLT_APP_NAME: appName,
223
+ PLT_APP_DIR: applicationPath,
224
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
225
+ })
226
+
227
+ const app = await start()
228
+
229
+ t.after(async () => {
230
+ await app.close()
231
+ await icc.close()
232
+ })
233
+
234
+ const updateCalls = []
235
+ const originalUpdateServicesResources =
236
+ app.wattpro.runtime.updateServicesResources
237
+ app.wattpro.runtime.updateServicesResources = async (resourceUpdates) => {
238
+ updateCalls.push(resourceUpdates)
239
+ if (originalUpdateServicesResources) {
240
+ return originalUpdateServicesResources.call(
241
+ app.wattpro.runtime,
242
+ resourceUpdates
243
+ )
244
+ }
245
+ }
246
+
247
+ const config = {
248
+ resources: {
249
+ services: [
250
+ { name: 'main', threads: 2, heap: 512 },
251
+ { name: 'service-1', threads: 1, heap: 256 },
252
+ ],
253
+ },
254
+ }
255
+
256
+ await app.wattpro.applyIccConfigUpdates(config)
257
+
258
+ assert.strictEqual(
259
+ updateCalls.length,
260
+ 1,
261
+ 'updateServicesResources should be called once'
262
+ )
263
+
264
+ const resourceUpdates = updateCalls[0]
265
+ assert.strictEqual(
266
+ resourceUpdates.length,
267
+ 2,
268
+ 'Should have updates for 2 services'
269
+ )
270
+
271
+ assert.strictEqual(resourceUpdates[0].service, 'main')
272
+ assert.strictEqual(resourceUpdates[0].workers, 2)
273
+ assert.strictEqual(resourceUpdates[0].health.maxHeapTotal, '512MB')
274
+
275
+ assert.strictEqual(resourceUpdates[1].service, 'service-1')
276
+ assert.strictEqual(resourceUpdates[1].workers, 1)
277
+ assert.strictEqual(resourceUpdates[1].health.maxHeapTotal, '256MB')
278
+ })
279
+
280
+ test('should handle updateServicesResources with different heap sizes', async (t) => {
281
+ const appName = 'test-heap-sizes'
282
+ const applicationId = randomUUID()
283
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
284
+
285
+ await installDeps(t, applicationPath)
286
+
287
+ const icc = await startICC(t, {
288
+ applicationId,
289
+ })
290
+
291
+ setUpEnvironment({
292
+ PLT_APP_NAME: appName,
293
+ PLT_APP_DIR: applicationPath,
294
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
295
+ })
296
+
297
+ const app = await start()
298
+
299
+ t.after(async () => {
300
+ await app.close()
301
+ await icc.close()
302
+ })
303
+
304
+ const updateCalls = []
305
+ app.wattpro.runtime.updateServicesResources = async (resourceUpdates) => {
306
+ updateCalls.push(resourceUpdates)
307
+ }
308
+
309
+ const configs = [
310
+ {
311
+ resources: {
312
+ services: [
313
+ { name: 'small-service', threads: 1, heap: 128 },
314
+ { name: 'large-service', threads: 4, heap: 2048 },
315
+ ],
316
+ },
317
+ },
318
+ ]
319
+
320
+ for (const config of configs) {
321
+ await app.wattpro.applyIccConfigUpdates(config)
322
+ }
323
+
324
+ assert.strictEqual(updateCalls.length, 1)
325
+
326
+ const resourceUpdates = updateCalls[0]
327
+ assert.strictEqual(resourceUpdates[0].health.maxHeapTotal, '128MB')
328
+ assert.strictEqual(resourceUpdates[1].health.maxHeapTotal, '2048MB')
329
+ })
330
+
331
+ test('should handle updateServicesResources error gracefully', async (t) => {
332
+ const appName = 'test-update-error'
333
+ const applicationId = randomUUID()
334
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
335
+
336
+ await installDeps(t, applicationPath)
337
+
338
+ const icc = await startICC(t, {
339
+ applicationId,
340
+ })
341
+
342
+ setUpEnvironment({
343
+ PLT_APP_NAME: appName,
344
+ PLT_APP_DIR: applicationPath,
345
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
346
+ })
347
+
348
+ const app = await start()
349
+
350
+ t.after(async () => {
351
+ await app.close()
352
+ await icc.close()
353
+ })
354
+
355
+ let errorThrown = false
356
+ app.wattpro.runtime.updateServicesResources = async (resourceUpdates) => {
357
+ assert.strictEqual(resourceUpdates[0].health.maxHeapTotal, '256MB')
358
+ errorThrown = true
359
+ throw new Error('Mock update error')
360
+ }
361
+
362
+ const config = {
363
+ resources: {
364
+ services: [{ name: 'test-service', threads: 1, heap: 256 }],
365
+ },
366
+ }
367
+
368
+ await app.wattpro.applyIccConfigUpdates(config)
369
+
370
+ assert.strictEqual(
371
+ errorThrown,
372
+ true,
373
+ 'updateServicesResources should have been called and thrown an error'
374
+ )
375
+ })
376
+
377
+ test('should not set opentelemetry if it is disabled', async (t) => {
378
+ const applicationName = 'test-application'
379
+ const applicationId = randomUUID()
380
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
381
+
382
+ const icc = await startICC(t, {
383
+ applicationId,
384
+ applicationName,
385
+ enableOpenTelemetry: false,
386
+ })
387
+
388
+ setUpEnvironment({
389
+ PLT_APP_NAME: applicationName,
390
+ PLT_APP_DIR: applicationPath,
391
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
392
+ })
393
+
394
+ const app = await start()
395
+
396
+ t.after(async () => {
397
+ await app.close()
398
+ await icc.close()
399
+ })
400
+
401
+ // main config
402
+ const { statusCode, body } = await request('http://127.0.0.1:3042/config')
403
+ assert.strictEqual(statusCode, 200)
404
+
405
+ const expectedTelemetry = {
406
+ enabled: false,
407
+ applicationName: 'test-application-main',
408
+ skip: [
409
+ {
410
+ method: 'GET',
411
+ path: '/documentation',
412
+ },
413
+ {
414
+ method: 'GET',
415
+ path: '/documentation/json',
416
+ },
417
+ ],
418
+ exporter: {
419
+ type: 'otlp',
420
+ options: {
421
+ url: 'http://127.0.0.1:3000/risk-service/v1/traces',
422
+ headers: {
423
+ 'x-platformatic-application-id': applicationId,
424
+ },
425
+ keepAlive: true,
426
+ httpAgentOptions: {
427
+ rejectUnauthorized: false,
428
+ },
429
+ },
430
+ },
431
+ }
432
+ const mainConfig = await body.json()
433
+ assert.deepStrictEqual(mainConfig.telemetry, expectedTelemetry)
434
+ })
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { randomUUID } from 'node:crypto'
7
+ import { start } from '../index.js'
8
+ import {
9
+ setUpEnvironment,
10
+ startICC,
11
+ installDeps
12
+ } from './helper.js'
13
+
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = dirname(__filename)
16
+
17
+ test('should spawn a runtime disabling all the scheduler jobs', async (t) => {
18
+ const applicationName = 'test-app'
19
+ const applicationId = randomUUID()
20
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-scheduler')
21
+
22
+ await installDeps(t, applicationPath)
23
+
24
+ let savedWattJob = null
25
+ const icc = await startICC(t, {
26
+ applicationId,
27
+ applicationName,
28
+ saveWattJob: (job) => {
29
+ savedWattJob = job
30
+ }
31
+ })
32
+
33
+ setUpEnvironment({
34
+ PLT_APP_NAME: applicationName,
35
+ PLT_APP_DIR: applicationPath,
36
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
37
+ })
38
+ const app = await start()
39
+
40
+ t.after(async () => {
41
+ await app.close()
42
+ await icc.close()
43
+ })
44
+
45
+ const config = await app.wattpro.runtime.getRuntimeConfig()
46
+
47
+ const { scheduler } = config
48
+
49
+ // The scheduler should be disabled
50
+ const expectedSchedulerConfig = [{
51
+ enabled: false,
52
+ name: 'test',
53
+ callbackUrl: 'http://localhost:3000',
54
+ cron: '*/5 * * * *',
55
+ method: 'GET',
56
+ maxRetries: 3
57
+ }]
58
+
59
+ assert.deepStrictEqual(scheduler, expectedSchedulerConfig)
60
+
61
+ // ICC is called to save the job
62
+ const expectedWattJob = {
63
+ name: 'test',
64
+ callbackUrl: 'http://localhost:3000',
65
+ schedule: '*/5 * * * *',
66
+ method: 'GET',
67
+ maxRetries: 3,
68
+ applicationId
69
+ }
70
+ assert.deepEqual(savedWattJob, expectedWattJob)
71
+ })
@@ -0,0 +1,138 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import buildApp from '../app.js'
7
+ import pino from 'pino'
8
+ import {
9
+ setUpEnvironment
10
+ } from './helper.js'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+
15
+ // Create a null logger for tests
16
+ const logger = pino({
17
+ level: 'silent'
18
+ })
19
+
20
+ // Helper to close resources after each test
21
+ const closeResources = async (app) => {
22
+ if (app && typeof app.close === 'function') {
23
+ try {
24
+ await app.close()
25
+ } catch (err) {}
26
+ }
27
+ }
28
+
29
+ test('should retry sending info to ICC with exponential backoff', async (t) => {
30
+ const applicationName = 'test-app'
31
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
32
+ const retryInterval = 50 // Small interval for test
33
+
34
+ setUpEnvironment({
35
+ PLT_APP_NAME: applicationName,
36
+ PLT_APP_DIR: applicationPath,
37
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
38
+ PLT_ICC_RETRY_TIME: retryInterval
39
+ })
40
+
41
+ const app = await buildApp(logger)
42
+ t.after(async () => closeResources(app))
43
+
44
+ // Mock failure for the first 2 calls, then succeed
45
+ let callCount = 0
46
+ const retryTimes = []
47
+ let lastCallTime = Date.now()
48
+
49
+ app.sendToICC = () => {
50
+ const now = Date.now()
51
+ if (callCount > 0) {
52
+ // Calculate actual delay between calls
53
+ retryTimes.push(now - lastCallTime)
54
+ }
55
+ lastCallTime = now
56
+
57
+ callCount++
58
+ if (callCount <= 2) {
59
+ throw new Error('Mock ICC connection failure')
60
+ }
61
+ return true
62
+ }
63
+
64
+ await app.sendToICCWithRetry()
65
+ assert.strictEqual(callCount, 3, 'ICC should be called 3 times (1 initial + 2 retries)')
66
+ assert.ok(retryTimes[0] >= retryInterval, 'First retry should wait at least the base retry interval')
67
+ assert.ok(retryTimes[1] > retryTimes[0], 'Second retry should have longer delay due to exponential backoff')
68
+ })
69
+
70
+ test('should continue retrying until success within max attempts', async (t) => {
71
+ const applicationName = 'test-app'
72
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
73
+ const retryInterval = 10 // Very small interval for fast test
74
+
75
+ setUpEnvironment({
76
+ PLT_APP_NAME: applicationName,
77
+ PLT_APP_DIR: applicationPath,
78
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
79
+ PLT_ICC_RETRY_TIME: retryInterval
80
+ })
81
+
82
+ const app = await buildApp(logger)
83
+ t.after(async () => closeResources(app))
84
+ let callCount = 0
85
+ const succeedAfter = 5 // Succeed after 5 attempts
86
+
87
+ const retryTimes = []
88
+ let lastCallTime = Date.now()
89
+
90
+ app.sendToICC = () => {
91
+ const now = Date.now()
92
+ if (callCount > 0) {
93
+ retryTimes.push(now - lastCallTime)
94
+ }
95
+ lastCallTime = now
96
+
97
+ callCount++
98
+ if (callCount <= succeedAfter) {
99
+ throw new Error(`Mock ICC connection failure (attempt ${callCount})`)
100
+ }
101
+ return true
102
+ }
103
+
104
+ await app.sendToICCWithRetry()
105
+ assert.strictEqual(callCount, succeedAfter + 1, `ICC should be called ${succeedAfter + 1} times (1 initial + ${succeedAfter} retries)`)
106
+
107
+ for (let i = 1; i < retryTimes.length; i++) {
108
+ assert.ok(
109
+ retryTimes[i] > retryTimes[i - 1],
110
+ `Retry interval should increase: ${retryTimes[i]} > ${retryTimes[i - 1]}`
111
+ )
112
+ }
113
+ })
114
+
115
+ test('should not retry if first attempt succeeds', async (t) => {
116
+ const applicationName = 'test-app'
117
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
118
+ const retryInterval = 100 // Small interval for test
119
+
120
+ setUpEnvironment({
121
+ PLT_APP_NAME: applicationName,
122
+ PLT_APP_DIR: applicationPath,
123
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
124
+ PLT_ICC_RETRY_TIME: retryInterval
125
+ })
126
+
127
+ const app = await buildApp(logger)
128
+ t.after(async () => closeResources(app))
129
+
130
+ let callCount = 0
131
+ app.sendToICC = () => {
132
+ callCount++
133
+ return true
134
+ }
135
+
136
+ await app.sendToICCWithRetry()
137
+ assert.strictEqual(callCount, 1, 'ICC should be called only once')
138
+ })