@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
package/test/helper.js ADDED
@@ -0,0 +1,274 @@
1
+ import { Agent, setGlobalDispatcher } from 'undici'
2
+ import { mkdir, symlink, writeFile, rm } from 'node:fs/promises'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import fastify from 'fastify'
6
+ import fp from 'fastify-plugin'
7
+ import why from 'why-is-node-running'
8
+ import fastifyWebsocket from '@fastify/websocket'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+
13
+ setInterval(why, 120000).unref()
14
+
15
+ setGlobalDispatcher(new Agent({
16
+ keepAliveTimeout: 10,
17
+ keepAliveMaxTimeout: 10
18
+ }))
19
+
20
+ function setUpEnvironment (env = {}) {
21
+ const defaultEnv = {
22
+ PLT_ZIO_HOSTNAME: '127.0.0.1',
23
+ PLT_ZIO_PORT: 4042,
24
+ PLT_ZIO_LOG_LEVEL: 'debug',
25
+ PLT_APP_HOSTNAME: '127.0.0.1',
26
+ PLT_APP_PORT: 3042,
27
+ PLT_METRICS_PORT: 9090,
28
+ PLT_DISABLE_COMPLIANCE_CHECK: 'true',
29
+ PLT_DISABLE_FLAMEGRAPHS: 'true',
30
+ PLT_THROW_ON_COMPLIANCE_FAILURE: 'false',
31
+ PLT_TEST_TOKEN: createJwtToken(3600)
32
+ }
33
+ Object.assign(process.env, defaultEnv, env)
34
+ }
35
+
36
+ async function startICC (t, opts = {}) {
37
+ let {
38
+ applicationId,
39
+ applicationName,
40
+ iccServices,
41
+ iccConfig = {},
42
+ enableOpenTelemetry = false,
43
+ enableSlicerInterceptor = false,
44
+ enableTrafficInterceptor = false,
45
+ controlPlaneResponse
46
+ } = opts
47
+
48
+ iccServices = iccServices || {
49
+ riskEngine: {
50
+ url: 'http://127.0.0.1:3000/risk-service'
51
+ },
52
+ trafficante: {
53
+ url: 'http://127.0.0.1:3000/trafficante'
54
+ },
55
+ compliance: {
56
+ url: 'http://127.0.0.1:3000/compliance'
57
+ },
58
+ cron: {
59
+ url: 'http://127.0.0.1:3000/cron'
60
+ },
61
+ scaler: {
62
+ url: 'http://127.0.0.1:3000/scaler'
63
+ }
64
+ }
65
+
66
+ const icc = fastify({
67
+ keepAliveTimeout: 1,
68
+ forceCloseConnections: true
69
+ })
70
+
71
+ await icc.register(fastifyWebsocket)
72
+
73
+ // Main
74
+ await icc.register(fp(async (icc) => {
75
+ const connections = new Map()
76
+
77
+ icc.decorate('emitApplicationUpdate', (applicationId, config) => {
78
+ const appConnections = connections.get(applicationId)
79
+ if (appConnections) {
80
+ for (const connection of appConnections) {
81
+ connection.send(JSON.stringify(config))
82
+ }
83
+ }
84
+ })
85
+
86
+ icc.get('/api/updates/applications/:id', { websocket: true }, async (connection, req) => {
87
+ connection.on('message', (message) => {
88
+ const applicationId = req.params.id
89
+ const { command, topic } = JSON.parse(message.toString())
90
+
91
+ if (command === 'subscribe' && topic === '/config') {
92
+ connection.send(JSON.stringify({ command: 'ack' }))
93
+ }
94
+
95
+ let appConnections = connections.get(applicationId)
96
+ if (!appConnections) {
97
+ appConnections = []
98
+ connections.set(applicationId, appConnections)
99
+ }
100
+ appConnections.push(connection)
101
+ })
102
+ })
103
+ }))
104
+
105
+ // Control Plane
106
+ await icc.register(async (icc) => {
107
+ icc.post('/pods/:podId/instance', async (req) => {
108
+ if (typeof controlPlaneResponse === 'function') {
109
+ return controlPlaneResponse(req)
110
+ }
111
+ return controlPlaneResponse || {
112
+ applicationId,
113
+ applicationName,
114
+ iccServices,
115
+ config: iccConfig,
116
+ enableOpenTelemetry,
117
+ enableSlicerInterceptor,
118
+ enableTrafficInterceptor
119
+ }
120
+ })
121
+
122
+ icc.post('/applications/:id/metadata', async (req) => {
123
+ const { applicationId, data } = req.body
124
+ return opts.saveComplianceMetadata?.(applicationId, data)
125
+ })
126
+
127
+ icc.post('/pods/:id/instance/state', async (req) => {
128
+ const instanceId = req.params.id
129
+ const state = req.body
130
+
131
+ await opts.saveApplicationInstanceState?.({ instanceId, state })
132
+ return {}
133
+ })
134
+ }, { prefix: '/control-plane' })
135
+
136
+ // Compliance
137
+ await icc.register(async (icc) => {
138
+ icc.post('/metadata', async (req) => {
139
+ const { applicationId, data } = req.body
140
+ return opts.saveComplianceMetadata?.(applicationId, data)
141
+ })
142
+
143
+ icc.post('/compliance', async (req) => {
144
+ const { applicationId } = req.body
145
+ return opts.getComplianceReport?.(applicationId)
146
+ })
147
+ }, { prefix: '/compliance' })
148
+
149
+ // Trafficante
150
+ await icc.register(async (icc) => {
151
+ icc.post('/requests/hash', async (req) => {
152
+ const { taxonomyId, applicationId } = JSON.parse(req.headers['x-labels'])
153
+ const { timestamp, request, response } = req.body
154
+ opts.saveRequestHash?.({ taxonomyId, applicationId, timestamp, request, response })
155
+ })
156
+
157
+ icc.post('/requests', async (req) => {
158
+ const { taxonomyId, applicationId } = JSON.parse(req.headers['x-labels'])
159
+ const request = JSON.parse(req.headers['x-request-data'])
160
+ const response = JSON.parse(req.headers['x-response-data'])
161
+ response.body = req.body
162
+
163
+ opts.saveRequest?.({ taxonomyId, applicationId, request, response })
164
+ })
165
+ }, { prefix: '/trafficante' })
166
+
167
+ // Risk Service
168
+ await icc.register(async (icc) => {
169
+ icc.addContentTypeParser(
170
+ 'application/x-protobuf',
171
+ (req, payload, done) => {
172
+ payload.resume()
173
+ payload.on('end', done)
174
+ }
175
+ )
176
+ icc.post('/v1/traces', async () => {})
177
+ }, { prefix: '/risk-service' })
178
+
179
+ // Scaler
180
+ await icc.register(async (icc) => {
181
+ icc.addContentTypeParser(
182
+ 'application/octet-stream',
183
+ function (request, payload, done) {
184
+ const chunks = []
185
+ payload.on('data', chunk => chunks.push(chunk))
186
+ payload.on('end', () => {
187
+ done(null, Buffer.concat(chunks))
188
+ })
189
+ payload.on('error', done)
190
+ }
191
+ )
192
+ icc.post('/alerts', async (req) => {
193
+ return opts.processAlerts?.(req)
194
+ })
195
+ icc.post('/pods/:podId/services/:serviceId/flamegraph', async (req) => {
196
+ return opts.processFlamegraphs?.(req)
197
+ })
198
+ }, { prefix: '/scaler' })
199
+
200
+ // Cron
201
+ await icc.register(async (icc) => {
202
+ icc.put('/watt-jobs', async (req) => {
203
+ const body = req.body
204
+ opts.saveWattJob?.(body)
205
+ })
206
+ }, { prefix: '/cron' })
207
+
208
+ await icc.listen({ port: 3000 })
209
+ return icc
210
+ }
211
+
212
+ async function installDeps (t, appDir, packageNames, testDependencies = []) {
213
+ const appDepsDir = join(appDir, 'node_modules')
214
+ await rm(appDepsDir, { force: true, recursive: true }).catch(() => {})
215
+ await mkdir(appDepsDir, { recursive: true })
216
+ t.after(() => rm(appDepsDir, { force: true, recursive: true }))
217
+
218
+ const toInstall = Array.from(new Set([
219
+ ...packageNames ?? [],
220
+ '@platformatic/runtime',
221
+ '@platformatic/config',
222
+ '@platformatic/service',
223
+ '@platformatic/node',
224
+ 'undici'
225
+ ]))
226
+
227
+ for (const packageName of toInstall) {
228
+ let targetDir = null
229
+ let packageDir = null
230
+
231
+ if (packageName.includes('/')) {
232
+ const [scope, name] = packageName.split('/')
233
+ await mkdir(join(appDepsDir, scope), { recursive: true })
234
+
235
+ targetDir = join(__dirname, '..', 'node_modules', scope, name)
236
+ packageDir = join(appDepsDir, scope, name)
237
+ } else {
238
+ packageDir = join(appDepsDir, packageName)
239
+ targetDir = join(__dirname, '..', 'node_modules', packageName)
240
+ }
241
+ await symlink(targetDir, packageDir)
242
+ }
243
+
244
+ for (const testDependency of testDependencies) {
245
+ const { name, version } = testDependency
246
+ const packageDir = join(appDepsDir, name)
247
+ await mkdir(packageDir, { recursive: true })
248
+
249
+ const packageJsonPath = join(packageDir, 'package.json')
250
+ const packageJsonFile = JSON.stringify({ name, version })
251
+ await writeFile(packageJsonPath, packageJsonFile)
252
+ }
253
+ }
254
+
255
+ function createJwtToken (expiresInSeconds) {
256
+ const header = { alg: 'HS256', typ: 'JWT' }
257
+ const currentTime = Math.floor(Date.now() / 1000)
258
+ const payload = {
259
+ sub: '1234567890',
260
+ iat: currentTime,
261
+ exp: currentTime + (expiresInSeconds || 3600) // Default 1 hour expiration
262
+ }
263
+ const base64Header = Buffer.from(JSON.stringify(header)).toString('base64').replace(/=/g, '')
264
+ const base64Payload = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=/g, '')
265
+ const signature = 'test_signature'
266
+ return `${base64Header}.${base64Payload}.${signature}`
267
+ }
268
+
269
+ export {
270
+ setUpEnvironment,
271
+ startICC,
272
+ installDeps,
273
+ createJwtToken
274
+ }
@@ -0,0 +1,243 @@
1
+ import { test } from 'node:test'
2
+ import { equal } from 'node:assert'
3
+ import { hostname } from 'node:os'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { startICC } from './helper.js'
6
+ import initPlugin from '../plugins/init.js'
7
+
8
+ const createMockApp = (env = {}) => {
9
+ const logMessages = []
10
+ return {
11
+ env: {
12
+ PLT_CONTROL_PLANE_URL: 'http://127.0.0.1:3000/control-plane',
13
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
14
+ ...env
15
+ },
16
+ log: {
17
+ info: (msg) => {
18
+ logMessages.push(msg)
19
+ },
20
+ warn: () => {},
21
+ error: () => {}
22
+ },
23
+ getAuthorizationHeader: async () => 'Bearer test-token',
24
+ logMessages
25
+ }
26
+ }
27
+
28
+ test('init plugin with explicit PLT_APP_NAME', async (t) => {
29
+ const applicationName = 'test-app-explicit'
30
+ const applicationId = randomUUID()
31
+ const instanceId = hostname()
32
+
33
+ const icc = await startICC(t, {
34
+ applicationId,
35
+ applicationName
36
+ })
37
+
38
+ t.after(async () => {
39
+ await icc.close()
40
+ })
41
+
42
+ const app = createMockApp({
43
+ PLT_APP_NAME: applicationName,
44
+ PLT_APP_DIR: '/test/dir'
45
+ })
46
+
47
+ await initPlugin(app)
48
+
49
+ equal(app.applicationName, applicationName)
50
+ equal(app.instanceId, instanceId)
51
+ equal(app.instanceConfig.applicationId, applicationId)
52
+ })
53
+
54
+ test('init plugin with optional PLT_APP_NAME - uses ICC response', async (t) => {
55
+ const applicationName = 'test-app-from-icc'
56
+ const applicationId = randomUUID()
57
+ const instanceId = hostname()
58
+
59
+ const icc = await startICC(t, {
60
+ applicationId,
61
+ controlPlaneResponse: {
62
+ applicationId,
63
+ applicationName, // ICC returns the application name
64
+ iccServices: {
65
+ riskEngine: { url: 'http://127.0.0.1:3000/risk-service' },
66
+ trafficante: { url: 'http://127.0.0.1:3000/trafficante' },
67
+ compliance: { url: 'http://127.0.0.1:3000/compliance' },
68
+ cron: { url: 'http://127.0.0.1:3000/cron' },
69
+ scaler: { url: 'http://127.0.0.1:3000/scaler' }
70
+ },
71
+ config: {},
72
+ enableOpenTelemetry: false,
73
+ enableSlicerInterceptor: false,
74
+ enableTrafficInterceptor: false
75
+ }
76
+ })
77
+
78
+ t.after(async () => {
79
+ await icc.close()
80
+ })
81
+
82
+ const app = createMockApp({
83
+ // PLT_APP_NAME is not set
84
+ PLT_APP_DIR: '/test/dir'
85
+ })
86
+
87
+ await initPlugin(app)
88
+
89
+ equal(app.applicationName, applicationName)
90
+ equal(app.instanceId, instanceId)
91
+ equal(app.instanceConfig.applicationId, applicationId)
92
+
93
+ // Check that logging shows the resolved application name
94
+ const resolvedNameLog = app.logMessages.find(msg =>
95
+ typeof msg === 'object' && msg.applicationName === applicationName
96
+ )
97
+ equal(!!resolvedNameLog, true)
98
+ })
99
+
100
+ test('init plugin in standalone mode - no ICC URL', async (t) => {
101
+ const applicationName = 'test-app-standalone'
102
+ const instanceId = hostname()
103
+
104
+ const app = createMockApp({
105
+ PLT_APP_NAME: applicationName,
106
+ PLT_APP_DIR: '/test/dir',
107
+ PLT_ICC_URL: undefined // No ICC URL set
108
+ })
109
+
110
+ await initPlugin(app)
111
+
112
+ equal(app.applicationName, applicationName)
113
+ equal(app.instanceId, instanceId)
114
+ equal(app.instanceConfig, null)
115
+
116
+ // Check that logging shows ICC initialization was skipped
117
+ const skipLog = app.logMessages.find(msg =>
118
+ typeof msg === 'string' && msg.includes('skipping ICC initialization')
119
+ )
120
+ equal(!!skipLog, true)
121
+ })
122
+
123
+ test('init plugin handles ICC connection failure gracefully', async (t) => {
124
+ const applicationName = 'test-app-failure'
125
+ const instanceId = hostname()
126
+
127
+ const app = createMockApp({
128
+ PLT_APP_NAME: applicationName,
129
+ PLT_APP_DIR: '/test/dir',
130
+ PLT_ICC_URL: 'http://127.0.0.1:9999' // Non-existent ICC
131
+ })
132
+
133
+ // Override error logging to capture errors
134
+ const errorMessages = []
135
+ app.log.error = (err, msg) => {
136
+ errorMessages.push({ err, msg })
137
+ }
138
+
139
+ await initPlugin(app)
140
+
141
+ // Should still set applicationName and instanceId
142
+ equal(app.applicationName, applicationName)
143
+ equal(app.instanceId, instanceId)
144
+
145
+ // Should have logged an error
146
+ equal(errorMessages.length, 1)
147
+ equal(errorMessages[0].msg, 'Failed to get application information')
148
+ })
149
+
150
+ test('init plugin sends correct request structure when PLT_APP_NAME provided', async (t) => {
151
+ const applicationName = 'test-app-request'
152
+ const applicationId = randomUUID()
153
+ const instanceId = hostname()
154
+
155
+ let capturedRequest = null
156
+
157
+ const icc = await startICC(t, {
158
+ applicationId,
159
+ controlPlaneResponse: (req) => {
160
+ capturedRequest = {
161
+ params: req.params,
162
+ body: req.body
163
+ }
164
+ return {
165
+ applicationId,
166
+ applicationName,
167
+ iccServices: {
168
+ riskEngine: { url: 'http://127.0.0.1:3000/risk-service' },
169
+ trafficante: { url: 'http://127.0.0.1:3000/trafficante' },
170
+ compliance: { url: 'http://127.0.0.1:3000/compliance' },
171
+ cron: { url: 'http://127.0.0.1:3000/cron' },
172
+ scaler: { url: 'http://127.0.0.1:3000/scaler' }
173
+ },
174
+ config: {}
175
+ }
176
+ }
177
+ })
178
+
179
+ t.after(async () => {
180
+ await icc.close()
181
+ })
182
+
183
+ const app = createMockApp({
184
+ PLT_APP_NAME: applicationName,
185
+ PLT_APP_DIR: '/test/dir'
186
+ })
187
+
188
+ await initPlugin(app)
189
+
190
+ // Verify request structure
191
+ equal(capturedRequest.params.podId, instanceId)
192
+ equal(capturedRequest.body.applicationName, applicationName)
193
+ equal(capturedRequest.body.podId, instanceId)
194
+ })
195
+
196
+ test('init plugin sends request without applicationName when not provided', async (t) => {
197
+ const applicationName = 'test-app-no-name'
198
+ const applicationId = randomUUID()
199
+ const instanceId = hostname()
200
+
201
+ let capturedRequest = null
202
+
203
+ const icc = await startICC(t, {
204
+ applicationId,
205
+ controlPlaneResponse: (req) => {
206
+ capturedRequest = {
207
+ params: req.params,
208
+ body: req.body
209
+ }
210
+ return {
211
+ applicationId,
212
+ applicationName, // ICC provides the name
213
+ iccServices: {
214
+ riskEngine: { url: 'http://127.0.0.1:3000/risk-service' },
215
+ trafficante: { url: 'http://127.0.0.1:3000/trafficante' },
216
+ compliance: { url: 'http://127.0.0.1:3000/compliance' },
217
+ cron: { url: 'http://127.0.0.1:3000/cron' },
218
+ scaler: { url: 'http://127.0.0.1:3000/scaler' }
219
+ },
220
+ config: {}
221
+ }
222
+ }
223
+ })
224
+
225
+ t.after(async () => {
226
+ await icc.close()
227
+ })
228
+
229
+ const app = createMockApp({
230
+ // PLT_APP_NAME is not provided
231
+ PLT_APP_DIR: '/test/dir'
232
+ })
233
+
234
+ await initPlugin(app)
235
+
236
+ // Verify request structure - should not contain applicationName
237
+ equal(capturedRequest.params.podId, instanceId)
238
+ equal(capturedRequest.body.podId, instanceId)
239
+ equal(capturedRequest.body.applicationName, undefined)
240
+
241
+ // But app should have the name from ICC response
242
+ equal(app.applicationName, applicationName)
243
+ })