kickload-watcher-mcp 0.1.6 → 0.1.7
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 +216 -215
- package/email-sender.js +1 -5
- package/github-webhook.js +322 -321
- package/kickload-client.js +60 -41
- package/package.json +1 -1
- package/pipeline.js +83 -18
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 = 1800000; // 30 min
|
|
20
20
|
|
|
21
21
|
export class KickloadClient {
|
|
22
22
|
/**
|
|
@@ -59,20 +59,16 @@ export class KickloadClient {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error(
|
|
73
|
-
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}`
|
|
74
|
-
);
|
|
75
|
-
}
|
|
62
|
+
const err = new Error(
|
|
63
|
+
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}\n` +
|
|
64
|
+
`URL: ${url}\n` +
|
|
65
|
+
`Response: ${text.substring(0, 500)}`
|
|
66
|
+
);
|
|
67
|
+
err.status = response.status;
|
|
68
|
+
err.body = text;
|
|
69
|
+
err.parsed = parsed;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
76
72
|
|
|
77
73
|
return parsed;
|
|
78
74
|
}
|
|
@@ -86,34 +82,19 @@ export class KickloadClient {
|
|
|
86
82
|
// Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
|
|
87
83
|
// ══════════════════════════════════════════════════════════════
|
|
88
84
|
async generateTestPlan({ prompt, jmxFilePath } = {}) {
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
}
|
|
85
|
+
if (!prompt && !jmxFilePath) {
|
|
86
|
+
throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
|
|
87
|
+
}
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
89
|
+
console.log("\n📋 STEP 1: Generating test plan...");
|
|
90
|
+
if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
|
|
91
|
+
if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
|
|
113
92
|
|
|
93
|
+
// ✅ FIX: send JSON when only prompt is provided
|
|
94
|
+
if (prompt && !jmxFilePath) {
|
|
114
95
|
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
115
|
-
body:
|
|
116
|
-
|
|
96
|
+
body: JSON.stringify({ prompt }),
|
|
97
|
+
isJson: true,
|
|
117
98
|
});
|
|
118
99
|
|
|
119
100
|
if (result.status !== "success") {
|
|
@@ -126,6 +107,38 @@ export class KickloadClient {
|
|
|
126
107
|
return result;
|
|
127
108
|
}
|
|
128
109
|
|
|
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
|
+
}
|
|
129
142
|
// ══════════════════════════════════════════════════════════════
|
|
130
143
|
// STEP 2 — Run Test
|
|
131
144
|
// POST /run-test/{jmx_filename}
|
|
@@ -201,7 +214,13 @@ export class KickloadClient {
|
|
|
201
214
|
throw new Error(`Task ${taskId} unknown status: ${result.status}`);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
throw new Error(
|
|
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
|
+
);
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
// ══════════════════════════════════════════════════════════════
|
package/package.json
CHANGED
package/pipeline.js
CHANGED
|
@@ -226,11 +226,85 @@ 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
|
-
);
|
|
233
229
|
|
|
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
|
+
}
|
|
234
308
|
logger.step("Running test");
|
|
235
309
|
const { task_id: taskId } = await withRetry(
|
|
236
310
|
() => client.runTest(jmxFilename, { numThreads: testParams.numThreads, loopCount: testParams.loopCount, rampTime: testParams.rampTime }),
|
|
@@ -278,19 +352,7 @@ export async function runPhase1Pipeline(event) {
|
|
|
278
352
|
return { success: true, backendUrl, jmxFilename, taskId, jtlFilename: taskResult.result_file, pdfFilename, downloadUrl, summary };
|
|
279
353
|
|
|
280
354
|
} catch (err) {
|
|
281
|
-
|
|
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
|
-
}
|
|
355
|
+
logger.error(`Pipeline failed: ${err.message}`);
|
|
294
356
|
|
|
295
357
|
if (devEmail) {
|
|
296
358
|
await safeResultEmail({
|
|
@@ -426,7 +488,10 @@ async function withRetry(fn, label) {
|
|
|
426
488
|
return await fn();
|
|
427
489
|
} catch (err) {
|
|
428
490
|
lastErr = err;
|
|
429
|
-
const status =
|
|
491
|
+
const status =
|
|
492
|
+
err.status ||
|
|
493
|
+
err.response?.status ||
|
|
494
|
+
(err.message?.includes("500") ? 500 : 0);
|
|
430
495
|
if (status >= 400 && status < 500 && status !== 429) throw err;
|
|
431
496
|
if (attempt <= MAX_RETRIES) {
|
|
432
497
|
logger.warn(`[${label}] attempt ${attempt} failed — retrying in ${RETRY_DELAY_MS / 1000}s (${err.message})`);
|