kickload-watcher-mcp 0.1.2 → 0.1.4
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/kickload-client.js +42 -25
- package/package.json +1 -1
- package/pipeline.js +82 -5
package/kickload-client.js
CHANGED
|
@@ -82,34 +82,19 @@ export class KickloadClient {
|
|
|
82
82
|
// Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
|
|
83
83
|
// ══════════════════════════════════════════════════════════════
|
|
84
84
|
async generateTestPlan({ prompt, jmxFilePath } = {}) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const form = new FormData();
|
|
90
|
-
|
|
91
|
-
if (prompt) {
|
|
92
|
-
form.append("prompt", prompt);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (jmxFilePath) {
|
|
96
|
-
const resolved = path.resolve(jmxFilePath);
|
|
97
|
-
if (!fs.existsSync(resolved)) {
|
|
98
|
-
throw new Error(`JMX file not found: ${resolved}`);
|
|
99
|
-
}
|
|
100
|
-
form.append("file", fs.createReadStream(resolved), {
|
|
101
|
-
filename: path.basename(resolved),
|
|
102
|
-
contentType: "application/xml",
|
|
103
|
-
});
|
|
104
|
-
}
|
|
85
|
+
if (!prompt && !jmxFilePath) {
|
|
86
|
+
throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
|
|
87
|
+
}
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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}`);
|
|
109
92
|
|
|
93
|
+
// ✅ FIX: send JSON when only prompt is provided
|
|
94
|
+
if (prompt && !jmxFilePath) {
|
|
110
95
|
const result = await this._fetch("POST", "/generate-test-plan", {
|
|
111
|
-
body:
|
|
112
|
-
|
|
96
|
+
body: JSON.stringify({ prompt }),
|
|
97
|
+
isJson: true,
|
|
113
98
|
});
|
|
114
99
|
|
|
115
100
|
if (result.status !== "success") {
|
|
@@ -122,6 +107,38 @@ export class KickloadClient {
|
|
|
122
107
|
return result;
|
|
123
108
|
}
|
|
124
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
|
+
}
|
|
125
142
|
// ══════════════════════════════════════════════════════════════
|
|
126
143
|
// STEP 2 — Run Test
|
|
127
144
|
// POST /run-test/{jmx_filename}
|
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 }),
|
|
@@ -414,7 +488,10 @@ async function withRetry(fn, label) {
|
|
|
414
488
|
return await fn();
|
|
415
489
|
} catch (err) {
|
|
416
490
|
lastErr = err;
|
|
417
|
-
const status =
|
|
491
|
+
const status =
|
|
492
|
+
err.status ||
|
|
493
|
+
err.response?.status ||
|
|
494
|
+
(err.message?.includes("500") ? 500 : 0);
|
|
418
495
|
if (status >= 400 && status < 500 && status !== 429) throw err;
|
|
419
496
|
if (attempt <= MAX_RETRIES) {
|
|
420
497
|
logger.warn(`[${label}] attempt ${attempt} failed — retrying in ${RETRY_DELAY_MS / 1000}s (${err.message})`);
|