plum-e2e 2.5.13 → 2.5.14
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/backend/mcp/server.js
CHANGED
|
@@ -88,7 +88,9 @@ async function pollJob(jobId, { maxMs = 600_000, intervalMs = 5_000 } = {}) {
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
89
89
|
|
|
90
90
|
function summariseReport(report) {
|
|
91
|
-
|
|
91
|
+
// GET /reports/:id hoists content.features to a top-level `features` key
|
|
92
|
+
// and strips `content` entirely — see reportService.getReportDetail.
|
|
93
|
+
const features = report.features ?? report.content?.features ?? [];
|
|
92
94
|
const allScenarios = features.flatMap((f) => f.scenarios ?? []);
|
|
93
95
|
const total = allScenarios.length;
|
|
94
96
|
const passed = allScenarios.filter((s) => s.status === 'passed').length;
|
|
@@ -117,6 +119,18 @@ function summariseReport(report) {
|
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Screenshots
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const SCREENSHOT_FILENAME_RE = /^[\w.-]+\.(png|jpg|jpeg)$/i;
|
|
127
|
+
|
|
128
|
+
function screenshotUrl(filename) {
|
|
129
|
+
return `${API_URL}/screenshots/${filename}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const SCREENSHOT_MIME_TYPES = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg' };
|
|
133
|
+
|
|
120
134
|
// ---------------------------------------------------------------------------
|
|
121
135
|
// Server setup
|
|
122
136
|
// ---------------------------------------------------------------------------
|
|
@@ -437,6 +451,103 @@ server.tool(
|
|
|
437
451
|
}
|
|
438
452
|
);
|
|
439
453
|
|
|
454
|
+
server.tool(
|
|
455
|
+
'get_report_scenario_detail',
|
|
456
|
+
[
|
|
457
|
+
'Get full per-scenario, per-step detail for a Plum test report — the data needed to diagnose',
|
|
458
|
+
"and self-heal a failing test: every step's status, duration, full error message, and",
|
|
459
|
+
'screenshot URL (if one was captured for that step).',
|
|
460
|
+
'',
|
|
461
|
+
'Use get_report_screenshot to fetch the actual screenshot image for a filename returned here,',
|
|
462
|
+
'and get_report_logs for the raw test-run stdout/stderr.'
|
|
463
|
+
].join('\n'),
|
|
464
|
+
{
|
|
465
|
+
reportId: z.number().int().describe('Numeric report ID'),
|
|
466
|
+
onlyFailed: z
|
|
467
|
+
.boolean()
|
|
468
|
+
.optional()
|
|
469
|
+
.describe('Only include scenarios with a failed step (default true)')
|
|
470
|
+
},
|
|
471
|
+
async ({ reportId, onlyFailed = true }) => {
|
|
472
|
+
const data = await get(`/reports/${reportId}`);
|
|
473
|
+
const features = data.features ?? data.content?.features ?? [];
|
|
474
|
+
|
|
475
|
+
const scenarios = features.flatMap((feature) =>
|
|
476
|
+
(feature.scenarios ?? [])
|
|
477
|
+
.filter((s) => !onlyFailed || s.status === 'failed')
|
|
478
|
+
.map((s) => ({
|
|
479
|
+
feature: feature.name,
|
|
480
|
+
scenario: s.name,
|
|
481
|
+
tags: s.tags ?? [],
|
|
482
|
+
status: s.status,
|
|
483
|
+
duration: s.duration,
|
|
484
|
+
steps: (s.steps ?? []).map((st) => ({
|
|
485
|
+
keyword: st.keyword,
|
|
486
|
+
name: st.name,
|
|
487
|
+
status: st.status,
|
|
488
|
+
duration: st.duration,
|
|
489
|
+
error: st.error ?? null,
|
|
490
|
+
screenshot: st.screenshot ?? null,
|
|
491
|
+
screenshotUrl: st.screenshot ? screenshotUrl(st.screenshot) : null
|
|
492
|
+
}))
|
|
493
|
+
}))
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
return { content: [{ type: 'text', text: JSON.stringify({ reportId, scenarios }, null, 2) }] };
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
server.tool(
|
|
501
|
+
'get_report_screenshot',
|
|
502
|
+
'Fetch a screenshot captured during a test step and return it as an image, so it can be viewed ' +
|
|
503
|
+
"directly. Get the filename from get_report_scenario_detail's step.screenshot field.",
|
|
504
|
+
{
|
|
505
|
+
filename: z.string().describe('Screenshot filename, e.g. "3f9c1e2a-....png"')
|
|
506
|
+
},
|
|
507
|
+
async ({ filename }) => {
|
|
508
|
+
if (!SCREENSHOT_FILENAME_RE.test(filename)) {
|
|
509
|
+
throw new Error(`Invalid screenshot filename: ${filename}`);
|
|
510
|
+
}
|
|
511
|
+
const res = await fetch(screenshotUrl(filename));
|
|
512
|
+
if (!res.ok) throw new Error(`Screenshot not found: ${filename}`);
|
|
513
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
514
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
content: [
|
|
518
|
+
{
|
|
519
|
+
type: 'image',
|
|
520
|
+
data: buffer.toString('base64'),
|
|
521
|
+
mimeType: SCREENSHOT_MIME_TYPES[ext]
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
server.tool(
|
|
529
|
+
'get_report_logs',
|
|
530
|
+
'Get the raw stdout/stderr log output captured during a Plum test run, tagged per runner. ' +
|
|
531
|
+
'Useful for diagnosing failures with no clear step-level error (crashes, timeouts, setup errors).',
|
|
532
|
+
{
|
|
533
|
+
reportId: z.number().int().describe('Numeric report ID'),
|
|
534
|
+
tail: z
|
|
535
|
+
.number()
|
|
536
|
+
.int()
|
|
537
|
+
.positive()
|
|
538
|
+
.optional()
|
|
539
|
+
.describe('Only return the last N lines (default: full log)')
|
|
540
|
+
},
|
|
541
|
+
async ({ reportId, tail }) => {
|
|
542
|
+
const data = await get(`/reports/${reportId}`);
|
|
543
|
+
let logs = data.logs ?? '';
|
|
544
|
+
if (tail) {
|
|
545
|
+
logs = logs.split('\n').slice(-tail).join('\n');
|
|
546
|
+
}
|
|
547
|
+
return { content: [{ type: 'text', text: logs || '(no logs captured for this report)' }] };
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
|
|
440
551
|
// ---------------------------------------------------------------------------
|
|
441
552
|
// Start
|
|
442
553
|
// ---------------------------------------------------------------------------
|
|
@@ -60,7 +60,7 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
60
60
|
for (const [rel, content] of Object.entries(tests)) {
|
|
61
61
|
const dest = path.join(tempTestsDir, rel);
|
|
62
62
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
-
fs.writeFileSync(dest, content, '
|
|
63
|
+
fs.writeFileSync(dest, Buffer.from(content, 'base64'));
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -100,7 +100,9 @@ function collectTestFiles() {
|
|
|
100
100
|
if (entry.isDirectory()) {
|
|
101
101
|
walk(fullPath, relPath);
|
|
102
102
|
} else {
|
|
103
|
-
|
|
103
|
+
// base64, not utf8 — utf8 mangles non-text fixtures (e.g. upload test images)
|
|
104
|
+
// because arbitrary binary bytes aren't valid UTF-8 and get replaced on read.
|
|
105
|
+
files[relPath] = fs.readFileSync(fullPath).toString('base64');
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
}
|