plum-e2e 1.3.5 → 1.3.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.
@@ -27,7 +27,9 @@ const runners = parallel || process.env.REPORT_RUNNERS || '1';
27
27
  const tag = process.env.TAG || process.argv.slice(2).find((a) => a.startsWith('@'));
28
28
  const browser = process.env.BROWSER || 'chromium';
29
29
 
30
- const reportFile = path.resolve(process.cwd(), 'reports', 'cucumber_report.json');
30
+ const reportFile =
31
+ process.env.CUCUMBER_REPORT_FILE ||
32
+ path.resolve(process.cwd(), 'reports', 'cucumber_report.json');
31
33
 
32
34
  // Wipe any previous report so a crashed/empty run can never return stale results.
33
35
  try {
@@ -87,7 +89,7 @@ try {
87
89
  );
88
90
  }
89
91
 
90
- baseCommand.push('--format', 'json:reports/cucumber_report.json');
92
+ baseCommand.push('--format', `json:${reportFile.replace(/\\/g, '/')}`);
91
93
 
92
94
  if (tag) {
93
95
  baseCommand.push('--tags', `"${tag}"`);
@@ -50,13 +50,18 @@ router.post('/execute', authGuard, (req, res) => {
50
50
  }
51
51
  }
52
52
 
53
+ // Each job writes to its own temp file so concurrent jobs on the same node
54
+ // cannot clobber each other's reports (shared cucumber_report.json race condition).
55
+ const reportFile = path.join(os.tmpdir(), `plum-report-${jobId}.json`);
56
+
53
57
  jobs[jobId] = {
54
58
  status: 'running',
55
59
  logs: '',
56
60
  exitCode: null,
57
61
  startedAt: Date.now(),
58
62
  meta: { tags: tags || '', browser, workers },
59
- tempTestsDir
63
+ tempTestsDir,
64
+ reportFile
60
65
  };
61
66
 
62
67
  const env = {
@@ -65,6 +70,7 @@ router.post('/execute', authGuard, (req, res) => {
65
70
  TRIGGER: TRIGGER_REMOTE,
66
71
  BROWSER: browser,
67
72
  REPORT_RUNNERS: String(workers),
73
+ CUCUMBER_REPORT_FILE: reportFile,
68
74
  ...(tempTestsDir ? { TESTS_ROOT: tempTestsDir } : {})
69
75
  };
70
76
  if (workers > 1) env.PARALLEL = String(workers);
@@ -81,9 +87,8 @@ router.post('/execute', authGuard, (req, res) => {
81
87
  jobs[jobId].exitCode = code;
82
88
 
83
89
  try {
84
- const reportPath = path.resolve(process.cwd(), 'reports', 'cucumber_report.json');
85
- if (fs.existsSync(reportPath)) {
86
- jobs[jobId].reportContent = fs.readFileSync(reportPath, 'utf8');
90
+ if (fs.existsSync(reportFile)) {
91
+ jobs[jobId].reportContent = fs.readFileSync(reportFile, 'utf8');
87
92
  }
88
93
  } catch {}
89
94
 
@@ -91,7 +96,12 @@ router.post('/execute', authGuard, (req, res) => {
91
96
  fs.rm(jobs[jobId].tempTestsDir, { recursive: true, force: true }, () => {});
92
97
  }
93
98
 
94
- setTimeout(() => delete jobs[jobId], 600_000);
99
+ setTimeout(() => {
100
+ try {
101
+ fs.unlinkSync(reportFile);
102
+ } catch {}
103
+ delete jobs[jobId];
104
+ }, 600_000);
95
105
  });
96
106
 
97
107
  res.json({ jobId, status: 'started' });
@@ -28,7 +28,22 @@ const getAll = () => prisma.runner.findMany({ orderBy: { createdAt: 'asc' } });
28
28
  const create = ({ name, url, token, browser = 'chromium' }) =>
29
29
  prisma.runner.create({ data: { name, url, token, browser } });
30
30
 
31
- const remove = (id) => prisma.runner.delete({ where: { id } });
31
+ async function remove(id) {
32
+ // Scrub the deleted runner from any cron job's runnerIds string before
33
+ // deleting, since that field has no relational constraint.
34
+ const jobs = await prisma.cronJob.findMany({ select: { id: true, runnerIds: true } });
35
+ for (const job of jobs) {
36
+ const ids = job.runnerIds
37
+ .split(',')
38
+ .map((s) => s.trim())
39
+ .filter((s) => s && s !== id);
40
+ await prisma.cronJob.update({
41
+ where: { id: job.id },
42
+ data: { runnerIds: ids.length > 0 ? ids.join(',') : 'built-in' }
43
+ });
44
+ }
45
+ return prisma.runner.delete({ where: { id } });
46
+ }
32
47
 
33
48
  const update = (id, data) => prisma.runner.update({ where: { id }, data });
34
49
 
package/bin/plum.js CHANGED
@@ -323,7 +323,7 @@ async function configureNode({ force }) {
323
323
 
324
324
  if (interactive) {
325
325
  const primaryVal = await clack.text({
326
- message: 'Primary server URL',
326
+ message: 'Your Plum server backend URL',
327
327
  placeholder: primary || 'http://localhost:3001',
328
328
  defaultValue: primary
329
329
  });
@@ -331,7 +331,7 @@ async function configureNode({ force }) {
331
331
  primary = primaryVal || primary;
332
332
 
333
333
  const portVal = await clack.text({
334
- message: 'Local port this node listens on',
334
+ message: 'Local port this Plum node listens on',
335
335
  placeholder: port,
336
336
  defaultValue: port
337
337
  });
@@ -340,7 +340,7 @@ async function configureNode({ force }) {
340
340
 
341
341
  const defaultUrl = url || `http://${detectLanIp()}:${port}`;
342
342
  const urlVal = await clack.text({
343
- message: 'URL the primary calls back (advertised)',
343
+ message: 'The URL your Plum server calls to communicate with this node',
344
344
  placeholder: defaultUrl,
345
345
  defaultValue: defaultUrl
346
346
  });
@@ -234,7 +234,7 @@
234
234
  function adjustWorkers(delta) {
235
235
  runnerConfig.update((c) => ({
236
236
  ...c,
237
- workers: Math.max(1, Math.min(16, c.workers + delta))
237
+ workers: Math.max(1, Math.min(10, c.workers + delta))
238
238
  }));
239
239
  }
240
240
 
@@ -353,7 +353,7 @@
353
353
  <button
354
354
  class="step-btn"
355
355
  on:click={() => adjustWorkers(1)}
356
- disabled={cfg.workers >= 16 || state.running}>+</button
356
+ disabled={cfg.workers >= 10 || state.running}>+</button
357
357
  >
358
358
  </div>
359
359
  </div>
@@ -27,8 +27,6 @@ export const TRIGGER_TYPES = Object.freeze({
27
27
  CLI: 'command-line-trigger'
28
28
  });
29
29
 
30
- export const WORKER_OPTIONS = [1, 2, 4, 8];
31
-
32
30
  export const REPORTS_PER_PAGE = 15;
33
31
 
34
32
  export const COPY_TIMEOUT_MS = 1400;
@@ -26,7 +26,7 @@
26
26
  } from '$lib/api/schedules';
27
27
  import { fetchRunners } from '$lib/api/runners';
28
28
  import { activeCronJobs } from '$lib/stores/runner';
29
- import { BROWSERS, WORKER_OPTIONS, TOAST_TIMEOUT_MS } from '$lib/constants';
29
+ import { BROWSERS, TOAST_TIMEOUT_MS } from '$lib/constants';
30
30
  import { stagger } from '$lib/utils/format';
31
31
  import Button from '$lib/components/ui/Button.svelte';
32
32
  import Badge from '$lib/components/ui/Badge.svelte';
@@ -124,6 +124,10 @@
124
124
  form.runnerIds = current.includes(id) ? current.filter((r) => r !== id) : [...current, id];
125
125
  }
126
126
 
127
+ function adjustFormWorkers(delta) {
128
+ form.workers = Math.max(1, Math.min(10, (form.workers || 1) + delta));
129
+ }
130
+
127
131
  function openAddModal() {
128
132
  isEditing = false;
129
133
  editTaskName = '';
@@ -144,14 +148,16 @@
144
148
  function openEditModal(job) {
145
149
  isEditing = true;
146
150
  editTaskName = job.taskName;
151
+ const validIds = new Set(['built-in', ...availableRunners.map((r) => r.id)]);
147
152
  const storedIds = job.runnerIds ? job.runnerIds.split(',').map((s) => s.trim()) : ['built-in'];
153
+ const prunedIds = storedIds.filter((id) => validIds.has(id));
148
154
  form = {
149
155
  taskName: job.taskName,
150
156
  cronExpression: job.cronExpression,
151
157
  tags: job.tags,
152
158
  workers: job.workers ?? 1,
153
159
  browser: job.browser ?? 'chromium',
154
- runnerIds: storedIds
160
+ runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in']
155
161
  };
156
162
  const isPreset = scheduleOptions.some((o) => o.value === job.cronExpression);
157
163
  useCustomCron = !isPreset;
@@ -303,18 +309,20 @@
303
309
  <span>Workers</span>
304
310
  <span class="field-hint">Parallel workers for this job</span>
305
311
  </div>
306
- <div class="seg-control">
307
- {#each WORKER_OPTIONS as n}
308
- <button
309
- type="button"
310
- class="seg-btn"
311
- class:active={form.workers === n}
312
- on:click={() => (form.workers = n)}
313
- >
314
- <span class="seg-num">{n}</span>
315
- <span class="seg-label">{n === 1 ? 'worker' : 'workers'}</span>
316
- </button>
317
- {/each}
312
+ <div class="stepper">
313
+ <button
314
+ type="button"
315
+ class="step-btn"
316
+ on:click={() => adjustFormWorkers(-1)}
317
+ disabled={form.workers <= 1}>−</button
318
+ >
319
+ <span class="step-val">{form.workers}</span>
320
+ <button
321
+ type="button"
322
+ class="step-btn"
323
+ on:click={() => adjustFormWorkers(1)}
324
+ disabled={form.workers >= 10}>+</button
325
+ >
318
326
  </div>
319
327
  </div>
320
328
 
@@ -663,6 +671,57 @@
663
671
  color: var(--fail);
664
672
  }
665
673
 
674
+ /* Workers stepper */
675
+ .stepper {
676
+ display: flex;
677
+ align-items: center;
678
+ background: var(--bg-subtle);
679
+ border: 1px solid var(--border);
680
+ border-radius: var(--radius-sm);
681
+ overflow: hidden;
682
+ width: fit-content;
683
+ }
684
+
685
+ .step-btn {
686
+ width: 28px;
687
+ height: 30px;
688
+ display: flex;
689
+ align-items: center;
690
+ justify-content: center;
691
+ border: none;
692
+ background: transparent;
693
+ color: var(--text-muted);
694
+ cursor: pointer;
695
+ font-size: 0.875rem;
696
+ font-weight: 400;
697
+ line-height: 1;
698
+ transition:
699
+ color var(--duration-fast),
700
+ background var(--duration-fast);
701
+ }
702
+
703
+ .step-btn:hover:not(:disabled) {
704
+ color: var(--text);
705
+ background: var(--bg-elevated);
706
+ }
707
+
708
+ .step-btn:disabled {
709
+ opacity: 0.3;
710
+ cursor: default;
711
+ }
712
+
713
+ .step-val {
714
+ min-width: 28px;
715
+ text-align: center;
716
+ font-size: 0.85rem;
717
+ font-weight: 500;
718
+ color: var(--text);
719
+ font-family: 'JetBrains Mono', monospace;
720
+ line-height: 30px;
721
+ border-left: 1px solid var(--border);
722
+ border-right: 1px solid var(--border);
723
+ }
724
+
666
725
  /* Segmented control */
667
726
  .seg-control {
668
727
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"