plum-e2e 2.5.8 → 2.5.10

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.
@@ -0,0 +1,67 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ /**
19
+ * `.plum-server.json`/`.plum-node.json` live in whatever directory the user
20
+ * happened to run `plum server start`/`plum node start` from. `plum update`
21
+ * needs to find and restart those installs later regardless of the cwd it's
22
+ * invoked from — this is the one place, independent of any project
23
+ * directory, that remembers where they are.
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const os = require('os');
28
+ const path = require('path');
29
+
30
+ const REGISTRY_DIR = path.join(os.homedir(), '.plum');
31
+ const REGISTRY_PATH = path.join(REGISTRY_DIR, 'installs.json');
32
+
33
+ function load() {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
36
+ } catch {
37
+ return { server: [], node: [] };
38
+ }
39
+ }
40
+
41
+ function save(data) {
42
+ fs.mkdirSync(REGISTRY_DIR, { recursive: true });
43
+ fs.writeFileSync(REGISTRY_PATH, JSON.stringify(data, null, 2) + '\n', 'utf8');
44
+ }
45
+
46
+ /** Records `dir` as a known `type` ('server' | 'node') install location. */
47
+ function registerInstall(type, dir) {
48
+ const data = load();
49
+ if (!data[type]) data[type] = [];
50
+ if (!data[type].includes(dir)) {
51
+ data[type].push(dir);
52
+ save(data);
53
+ }
54
+ }
55
+
56
+ /** Known install dirs for `type`, pruned of any that no longer exist on disk. */
57
+ function getInstalls(type) {
58
+ const data = load();
59
+ const dirs = (data[type] || []).filter((d) => fs.existsSync(d));
60
+ if (dirs.length !== (data[type] || []).length) {
61
+ data[type] = dirs;
62
+ save(data);
63
+ }
64
+ return dirs;
65
+ }
66
+
67
+ module.exports = { REGISTRY_PATH, registerInstall, getInstalls };
@@ -67,6 +67,7 @@ async function api(method, path, body) {
67
67
  const get = (path) => api('GET', path);
68
68
  const post = (path, body) => api('POST', path, body);
69
69
  const put = (path, body) => api('PUT', path, body);
70
+ const del = (path) => api('DELETE', path);
70
71
 
71
72
  // ---------------------------------------------------------------------------
72
73
  // Polling helper for test runs
@@ -176,6 +177,33 @@ server.tool(
176
177
  }
177
178
  );
178
179
 
180
+ server.tool(
181
+ 'update_test_suite',
182
+ "Update a test suite's name, description, or priority. Only the fields provided are changed.",
183
+ {
184
+ suiteId: z.string().describe('UUID of the suite to update'),
185
+ name: z.string().min(1).optional(),
186
+ description: z.string().optional(),
187
+ priority: z.enum(['Critical', 'High', 'Medium', 'Low']).optional()
188
+ },
189
+ async ({ suiteId, name, description, priority }) => {
190
+ const data = await put(`/test-suites/${suiteId}`, { name, description, priority });
191
+ return { content: [{ type: 'text', text: JSON.stringify(data.suite, null, 2) }] };
192
+ }
193
+ );
194
+
195
+ server.tool(
196
+ 'delete_test_suite',
197
+ 'Permanently delete a test suite and all of its test cases. This cannot be undone.',
198
+ {
199
+ suiteId: z.string().describe('UUID of the suite to delete')
200
+ },
201
+ async ({ suiteId }) => {
202
+ await del(`/test-suites/${suiteId}`);
203
+ return { content: [{ type: 'text', text: `Suite ${suiteId} deleted.` }] };
204
+ }
205
+ );
206
+
179
207
  // -- Test Repository: Cases -------------------------------------------------
180
208
 
181
209
  server.tool(
@@ -193,6 +221,45 @@ server.tool(
193
221
  }
194
222
  );
195
223
 
224
+ server.tool(
225
+ 'get_test_case',
226
+ 'Get a test case by ID, including its manual steps and recent execution history.',
227
+ {
228
+ caseId: z.string().describe('UUID of the test case')
229
+ },
230
+ async ({ caseId }) => {
231
+ const data = await get(`/test-cases/${caseId}`);
232
+ return { content: [{ type: 'text', text: JSON.stringify(data.testCase, null, 2) }] };
233
+ }
234
+ );
235
+
236
+ server.tool(
237
+ 'update_test_case',
238
+ "Update a test case's title, description, or priority. Only the fields provided are changed.",
239
+ {
240
+ caseId: z.string().describe('UUID of the test case to update'),
241
+ title: z.string().min(1).optional(),
242
+ description: z.string().optional(),
243
+ priority: z.enum(['Critical', 'High', 'Medium', 'Low']).optional()
244
+ },
245
+ async ({ caseId, title, description, priority }) => {
246
+ const data = await put(`/test-cases/${caseId}`, { title, description, priority });
247
+ return { content: [{ type: 'text', text: JSON.stringify(data.testCase, null, 2) }] };
248
+ }
249
+ );
250
+
251
+ server.tool(
252
+ 'delete_test_case',
253
+ 'Permanently delete a test case and its steps. This cannot be undone.',
254
+ {
255
+ caseId: z.string().describe('UUID of the test case to delete')
256
+ },
257
+ async ({ caseId }) => {
258
+ await del(`/test-cases/${caseId}`);
259
+ return { content: [{ type: 'text', text: `Test case ${caseId} deleted.` }] };
260
+ }
261
+ );
262
+
196
263
  server.tool(
197
264
  'set_test_steps',
198
265
  'Set (replace) the manual test steps for a test case. Each step has an action, optional test data, and optional expected output.',
package/bin/plum.js CHANGED
@@ -138,6 +138,7 @@ const backendLib = path.join(plumRoot, 'backend', 'lib');
138
138
  const serverConfigLib = () => require(path.join(backendLib, 'serverConfig.js'));
139
139
  const nodeRegisterLib = () => require(path.join(backendLib, 'nodeRegister.js'));
140
140
  const runnerProcessLib = () => require(path.join(backendLib, 'runnerProcess.js'));
141
+ const globalRegistryLib = () => require(path.join(backendLib, 'globalRegistry.js'));
141
142
 
142
143
  /* -----------------------------------------------------
143
144
  * Interactive prompts
@@ -254,6 +255,7 @@ async function configureServer({ force }) {
254
255
  }
255
256
 
256
257
  saveServerConfig(cwd, cfg);
258
+ globalRegistryLib().registerInstall('server', cwd);
257
259
  return cfg;
258
260
  }
259
261
 
@@ -484,9 +486,15 @@ function npmInstallLatestWithRetry() {
484
486
  return false;
485
487
  }
486
488
 
489
+ function readPlumVersion() {
490
+ return JSON.parse(fs.readFileSync(path.join(plumRoot, 'package.json'), 'utf8')).version;
491
+ }
492
+
487
493
  async function serverUpdate() {
488
494
  clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Update ')));
489
- clack.log.step('Fetching latest Plum version…');
495
+
496
+ const fromVersion = readPlumVersion();
497
+ clack.log.step(`Fetching latest Plum version… (currently ${fromVersion})`);
490
498
  if (!npmInstallLatestWithRetry()) {
491
499
  clack.log.error(
492
500
  `Failed to install the latest version after ${NPM_INSTALL_RETRIES} attempts. Try again shortly, or run "npm install -g plum-e2e@latest" manually to see the full error.`
@@ -495,39 +503,61 @@ async function serverUpdate() {
495
503
  process.exitCode = 1;
496
504
  return;
497
505
  }
498
- clack.log.success('Plum CLI updated.');
499
506
 
500
- const serverCfgPath = path.join(process.cwd(), '.plum-server.json');
501
- const hasServerCfg = fs.existsSync(serverCfgPath);
507
+ // Re-read from disk (not require-cached) so this reflects what npm just installed.
508
+ const toVersion = readPlumVersion();
509
+ clack.log.success(`Plum CLI updated: ${fromVersion} → ${toVersion}`);
502
510
 
511
+ // Every install registers its directory here when configured (see
512
+ // configureServer/configureNode), so this finds them regardless of the cwd
513
+ // `plum update` happens to be run from.
514
+ const { getInstalls } = globalRegistryLib();
503
515
  const { loadNodeConfig } = nodeRegisterLib();
504
516
  const { loadRegistry, isAlive } = runnerProcessLib();
505
- const nodeCfg = loadNodeConfig(process.cwd());
506
- const registry = loadRegistry();
507
- const nodeRunning = !!(
508
- nodeCfg.id &&
509
- registry[String(nodeCfg.id)]?.pid &&
510
- isAlive(registry[String(nodeCfg.id)].pid)
511
- );
512
517
 
513
- // Re-exec `plum` as a fresh process for the restart steps rather than
514
- // calling serverRestart()/nodeRestart() directly — this same process
515
- // already loaded the OLD code into memory before npm install ran above,
516
- // so calling them in-process would rebuild using stale logic no matter
517
- // how new the just-installed files on disk actually are.
518
- if (hasServerCfg) {
519
- clack.log.step('Rebuilding server with the newly installed version…');
520
- execSync('plum server restart', { stdio: 'inherit' });
518
+ let restartedAnything = false;
519
+
520
+ // Re-exec `plum` as a fresh process (cwd set to each install dir) for the
521
+ // restart steps rather than calling serverRestart()/nodeRestart() directly
522
+ // this same process already loaded the OLD code into memory before npm
523
+ // install ran above, so calling them in-process would rebuild using stale
524
+ // logic no matter how new the just-installed files on disk actually are.
525
+ for (const dir of getInstalls('server')) {
526
+ if (!fs.existsSync(path.join(dir, '.plum-server.json'))) continue;
527
+ clack.log.step(`Rebuilding server at ${dir}…`);
528
+ try {
529
+ execSync('plum server restart', { stdio: 'inherit', cwd: dir });
530
+ restartedAnything = true;
531
+ } catch (e) {
532
+ clack.log.warn(`Could not restart server at ${dir}: ${e.message}`);
533
+ }
521
534
  }
522
535
 
523
- if (nodeRunning) {
524
- clack.log.step('Restarting node runner with the newly installed version…');
525
- execSync('plum node restart', { stdio: 'inherit' });
536
+ const registry = loadRegistry();
537
+ for (const dir of getInstalls('node')) {
538
+ const nodeCfg = loadNodeConfig(dir);
539
+ const running = !!(
540
+ nodeCfg.id &&
541
+ registry[String(nodeCfg.id)]?.pid &&
542
+ isAlive(registry[String(nodeCfg.id)].pid)
543
+ );
544
+ if (!running) continue;
545
+ clack.log.step(`Restarting node runner at ${dir}…`);
546
+ try {
547
+ execSync('plum node restart', { stdio: 'inherit', cwd: dir });
548
+ restartedAnything = true;
549
+ } catch (e) {
550
+ clack.log.warn(`Could not restart node at ${dir}: ${e.message}`);
551
+ }
526
552
  }
527
553
 
528
- if (!hasServerCfg && !nodeRunning) {
529
- clack.outro(pc.green('Plum updated. Run `plum server start` or `plum node start` to launch.'));
554
+ if (!restartedAnything) {
555
+ clack.log.info(
556
+ 'No running server or node found — run `plum server start` or `plum node start` when ready.'
557
+ );
530
558
  }
559
+
560
+ clack.outro(pc.green(`Plum updated: ${fromVersion} → ${toVersion}`));
531
561
  }
532
562
 
533
563
  async function serverReconfig() {
@@ -610,6 +640,7 @@ async function configureNode({ force }) {
610
640
  port,
611
641
  pid: saved.pid ?? null
612
642
  });
643
+ globalRegistryLib().registerInstall('node', cwd);
613
644
  return { primary, port, browser, token, name, url };
614
645
  }
615
646
 
@@ -665,6 +696,23 @@ async function nodeStart({ reconfig }) {
665
696
  const backendDir = path.join(plumRoot, 'backend');
666
697
  clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Node Runner ')));
667
698
 
699
+ const { loadNodeConfig } = nodeRegisterLib();
700
+ const { statusOf } = runnerProcessLib();
701
+ const existing = loadNodeConfig(process.cwd());
702
+
703
+ // Re-running `node start` on an already-running node used to spawn a second
704
+ // process on the same port (orphaning the first) and re-register a duplicate
705
+ // runner on the primary. Route to the same menu this command ends on anyway
706
+ // instead of repeating the whole configure/register/spawn dance.
707
+ if (!reconfig && existing.id && statusOf(String(existing.id)) === 'running') {
708
+ clack.log.info(
709
+ `Node "${existing.name ?? existing.id}" is already running from this folder — opening the runner menu instead of starting a new one.`
710
+ );
711
+ await openManageRunnersMenu(existing.primary);
712
+ clack.outro(`Manage runners anytime: ${pc.cyan('plum manage-runners')}`);
713
+ return;
714
+ }
715
+
668
716
  const cfg = await configureNode({ force: reconfig });
669
717
  const registeredId = await registerNode(cfg);
670
718
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.5.8",
3
+ "version": "2.5.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"