kickload-watcher-mcp 0.1.4 → 0.1.6
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/compliance-poller.js +215 -216
- package/email-sender.js +5 -1
- package/github-webhook.js +321 -322
- package/kickload-client.js +41 -60
- package/package.json +1 -1
- package/pipeline.js +18 -83
package/kickload-client.js
CHANGED
|
@@ -16,7 +16,7 @@ import fs from "fs";
|
|
|
16
16
|
import path from "path";
|
|
17
17
|
|
|
18
18
|
const POLL_INTERVAL_MS = 5000; // 5s between polls — matches backend
|
|
19
|
-
const POLL_TIMEOUT_MS
|
|
19
|
+
const POLL_TIMEOUT_MS = 600000; // 10 minutes max
|
|
20
20
|
|
|
21
21
|
export class KickloadClient {
|
|
22
22
|
/**
|
|
@@ -59,16 +59,20 @@ export class KickloadClient {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
if (response.status === 401 || response.status === 403) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Kickload authentication failed — check your API token or subscription"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (response.status === 504) {
|
|
69
|
+
throw new Error("Kickload server timeout (504)");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
return parsed;
|
|
74
78
|
}
|
|
@@ -82,19 +86,34 @@ export class KickloadClient {
|
|
|
82
86
|
// Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
|
|
83
87
|
// ══════════════════════════════════════════════════════════════
|
|
84
88
|
async generateTestPlan({ prompt, jmxFilePath } = {}) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
if (!prompt && !jmxFilePath) {
|
|
90
|
+
throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const form = new FormData();
|
|
94
|
+
|
|
95
|
+
if (prompt) {
|
|
96
|
+
form.append("prompt", prompt);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (jmxFilePath) {
|
|
100
|
+
const resolved = path.resolve(jmxFilePath);
|
|
101
|
+
if (!fs.existsSync(resolved)) {
|
|
102
|
+
throw new Error(`JMX file not found: ${resolved}`);
|
|
103
|
+
}
|
|
104
|
+
form.append("file", fs.createReadStream(resolved), {
|
|
105
|
+
filename: path.basename(resolved),
|
|
106
|
+
contentType: "application/xml",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
console.log("\n📋 STEP 1: Generating test plan...");
|
|
111
|
+
if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
|
|
112
|
+
if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
|
|
92
113
|
|
|
93
|
-
// ✅ FIX: send JSON when only prompt is provided
|
|
94
|
-
if (prompt && !jmxFilePath) {
|
|
95
114
|
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
96
|
-
body:
|
|
97
|
-
|
|
115
|
+
body: form,
|
|
116
|
+
headers: form.getHeaders(),
|
|
98
117
|
});
|
|
99
118
|
|
|
100
119
|
if (result.status !== "success") {
|
|
@@ -107,38 +126,6 @@ export class KickloadClient {
|
|
|
107
126
|
return result;
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
// ✅ fallback: use FormData only if file is present
|
|
111
|
-
const form = new FormData();
|
|
112
|
-
|
|
113
|
-
if (prompt) {
|
|
114
|
-
form.append("prompt", prompt);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (jmxFilePath) {
|
|
118
|
-
const resolved = path.resolve(jmxFilePath);
|
|
119
|
-
if (!fs.existsSync(resolved)) {
|
|
120
|
-
throw new Error(`JMX file not found: ${resolved}`);
|
|
121
|
-
}
|
|
122
|
-
form.append("file", fs.createReadStream(resolved), {
|
|
123
|
-
filename: path.basename(resolved),
|
|
124
|
-
contentType: "application/xml",
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
129
|
-
body: form,
|
|
130
|
-
headers: form.getHeaders(),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (result.status !== "success") {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`Test plan generation failed: ${result.message || JSON.stringify(result)}`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
console.log(`✅ STEP 1 done — JMX: ${result.jmx_filename}`);
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
129
|
// ══════════════════════════════════════════════════════════════
|
|
143
130
|
// STEP 2 — Run Test
|
|
144
131
|
// POST /run-test/{jmx_filename}
|
|
@@ -214,13 +201,7 @@ export class KickloadClient {
|
|
|
214
201
|
throw new Error(`Task ${taskId} unknown status: ${result.status}`);
|
|
215
202
|
}
|
|
216
203
|
|
|
217
|
-
throw new Error(
|
|
218
|
-
`Task ${taskId} timed out after ${POLL_TIMEOUT_MS / 60000} minutes.\n` +
|
|
219
|
-
`Possible reasons:\n` +
|
|
220
|
-
`- Large load test\n` +
|
|
221
|
-
`- Slow backend\n` +
|
|
222
|
-
`- KickLoad server delay`
|
|
223
|
-
);
|
|
204
|
+
throw new Error(`Task ${taskId} timed out after ${POLL_TIMEOUT_MS / 60000} minutes`);
|
|
224
205
|
}
|
|
225
206
|
|
|
226
207
|
// ══════════════════════════════════════════════════════════════
|
package/package.json
CHANGED
package/pipeline.js
CHANGED
|
@@ -226,85 +226,11 @@ export async function runPhase1Pipeline(event) {
|
|
|
226
226
|
const client = new KickloadClient(config.kickload.baseUrl, config.kickload.apiToken);
|
|
227
227
|
|
|
228
228
|
logger.step("Generating test plan");
|
|
229
|
+
const { jmx_filename: jmxFilename } = await withRetry(
|
|
230
|
+
() => client.generateTestPlan({ prompt: testParams.testPrompt, jmxFilePath: jmxFilePath || null }),
|
|
231
|
+
"generate-test-plan"
|
|
232
|
+
);
|
|
229
233
|
|
|
230
|
-
let jmxFilename = null;
|
|
231
|
-
let kickloadFailed = false;
|
|
232
|
-
let kickloadErrorMsg = "";
|
|
233
|
-
|
|
234
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
235
|
-
try {
|
|
236
|
-
const res = await client.generateTestPlan({
|
|
237
|
-
prompt: testParams.testPrompt,
|
|
238
|
-
jmxFilePath: jmxFilePath || null
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
jmxFilename = res.jmx_filename;
|
|
242
|
-
break;
|
|
243
|
-
|
|
244
|
-
} catch (err) {
|
|
245
|
-
kickloadErrorMsg = err.message || "unknown error";
|
|
246
|
-
|
|
247
|
-
const status =
|
|
248
|
-
err.status ||
|
|
249
|
-
err.response?.status ||
|
|
250
|
-
(err.message?.includes("500") ? 500 : 0);
|
|
251
|
-
|
|
252
|
-
// retry only for 503 / 5xx
|
|
253
|
-
if (status >= 500 && attempt < 3) {
|
|
254
|
-
logger.warn(`Kickload attempt ${attempt}/3 failed — retrying in 3s (${kickloadErrorMsg})`);
|
|
255
|
-
await sleep(3000);
|
|
256
|
-
} else {
|
|
257
|
-
kickloadFailed = true;
|
|
258
|
-
logger.error(`Kickload failed after retries: ${kickloadErrorMsg}`);
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (kickloadFailed || !jmxFilename) {
|
|
264
|
-
logger.warn("⚠️ Kickload failed — continuing with fallback result");
|
|
265
|
-
logger.info(`Fallback reason: ${kickloadErrorMsg}`);
|
|
266
|
-
|
|
267
|
-
// fake result so pipeline completes fully
|
|
268
|
-
const totalMs = Date.now() - start;
|
|
269
|
-
|
|
270
|
-
const summary = {
|
|
271
|
-
passed: false,
|
|
272
|
-
averageLatency: null,
|
|
273
|
-
errorPercentage: 100,
|
|
274
|
-
throughput: null,
|
|
275
|
-
note: "Test plan generation failed — fallback result generated"
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
logPipelineSummary({
|
|
279
|
-
endpoint: endpoints.join(", "),
|
|
280
|
-
passed: summary.passed,
|
|
281
|
-
latency: summary.averageLatency,
|
|
282
|
-
errorRate: summary.errorPercentage,
|
|
283
|
-
throughput: summary.throughput,
|
|
284
|
-
durationSec: Math.round(totalMs / 1000),
|
|
285
|
-
emailSent: !!devEmail,
|
|
286
|
-
devEmail,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// send email
|
|
290
|
-
if (devEmail) {
|
|
291
|
-
await safeResultEmail({
|
|
292
|
-
toEmail: devEmail,
|
|
293
|
-
endpoint: endpoints.join(", "),
|
|
294
|
-
passed: false,
|
|
295
|
-
summary,
|
|
296
|
-
downloadUrl: null,
|
|
297
|
-
pdfFilename: null,
|
|
298
|
-
durationMs: totalMs,
|
|
299
|
-
testPrompt: testParams.testPrompt,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
success: true, // 👈 IMPORTANT: pipeline completed
|
|
305
|
-
fallback: true
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
234
|
logger.step("Running test");
|
|
309
235
|
const { task_id: taskId } = await withRetry(
|
|
310
236
|
() => client.runTest(jmxFilename, { numThreads: testParams.numThreads, loopCount: testParams.loopCount, rampTime: testParams.rampTime }),
|
|
@@ -352,7 +278,19 @@ return {
|
|
|
352
278
|
return { success: true, backendUrl, jmxFilename, taskId, jtlFilename: taskResult.result_file, pdfFilename, downloadUrl, summary };
|
|
353
279
|
|
|
354
280
|
} catch (err) {
|
|
355
|
-
|
|
281
|
+
if (err.message.includes("504") || err.message.includes("Gateway")) {
|
|
282
|
+
logger.error("❌ Kickload server is currently unavailable (504 timeout)");
|
|
283
|
+
logger.error(" This is a server-side issue, not your API");
|
|
284
|
+
logger.error(" Try again after some time\n");
|
|
285
|
+
} else if (err.message.includes("401") || err.message.includes("403")) {
|
|
286
|
+
logger.error("❌ Kickload authentication failed");
|
|
287
|
+
logger.error(" Possible reasons:");
|
|
288
|
+
logger.error(" • Invalid API token");
|
|
289
|
+
logger.error(" • Token expired");
|
|
290
|
+
logger.error(" • Subscription required\n");
|
|
291
|
+
} else {
|
|
292
|
+
logger.error(`Pipeline failed: ${err.message}`);
|
|
293
|
+
}
|
|
356
294
|
|
|
357
295
|
if (devEmail) {
|
|
358
296
|
await safeResultEmail({
|
|
@@ -488,10 +426,7 @@ async function withRetry(fn, label) {
|
|
|
488
426
|
return await fn();
|
|
489
427
|
} catch (err) {
|
|
490
428
|
lastErr = err;
|
|
491
|
-
const status =
|
|
492
|
-
err.status ||
|
|
493
|
-
err.response?.status ||
|
|
494
|
-
(err.message?.includes("500") ? 500 : 0);
|
|
429
|
+
const status = err.status || 0;
|
|
495
430
|
if (status >= 400 && status < 500 && status !== 429) throw err;
|
|
496
431
|
if (attempt <= MAX_RETRIES) {
|
|
497
432
|
logger.warn(`[${label}] attempt ${attempt} failed — retrying in ${RETRY_DELAY_MS / 1000}s (${err.message})`);
|