plum-e2e 2.6.6 → 2.6.8

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.
@@ -32,6 +32,14 @@ if (!fs.existsSync(jsonReportFile)) {
32
32
  // and saves the combined report after all lanes finish.
33
33
  if (process.env.PLUM_MODE === 'node') process.exit(0);
34
34
 
35
+ // `run-test` runs on the host without Docker/Postgres, so DATABASE_URL is
36
+ // never set there — skip the DB save instead of letting Prisma's connection
37
+ // attempt surface as an unhandled-looking stack trace after tests already ran.
38
+ if (!process.env.DATABASE_URL) {
39
+ console.log('No DATABASE_URL configured — skipping report save to database.');
40
+ process.exit(0);
41
+ }
42
+
35
43
  (async () => {
36
44
  try {
37
45
  const raw = JSON.parse(fs.readFileSync(jsonReportFile, 'utf8'));
@@ -77,12 +77,17 @@ function findPidOnPort(port) {
77
77
  if (process.platform === 'win32') {
78
78
  const out = execSync('netstat -ano', { encoding: 'utf8' });
79
79
  for (const line of out.split('\n')) {
80
- const upper = line.toUpperCase();
81
- if (upper.includes(`:${portStr}`) && upper.includes('LISTENING')) {
82
- const parts = line.trim().split(/\s+/);
83
- const pid = parseInt(parts[parts.length - 1], 10);
84
- if (!isNaN(pid) && pid > 0) return pid;
85
- }
80
+ if (!line.toUpperCase().includes('LISTENING')) continue;
81
+ const parts = line.trim().split(/\s+/);
82
+ // Columns: Proto, Local Address, Foreign Address, State, PID.
83
+ // Match the port exactly off the local address (e.g. "0.0.0.0:3002"
84
+ // or "[::]:3002") a substring check would false-positive port
85
+ // "300" against "3002", "13000", etc.
86
+ const localAddress = parts[1] ?? '';
87
+ const localPort = localAddress.slice(localAddress.lastIndexOf(':') + 1);
88
+ if (localPort !== portStr) continue;
89
+ const pid = parseInt(parts[parts.length - 1], 10);
90
+ if (!isNaN(pid) && pid > 0) return pid;
86
91
  }
87
92
  } else {
88
93
  const out = execSync(`lsof -i :${portStr} -t -sTCP:LISTEN`, { encoding: 'utf8' }).trim();
@@ -122,12 +127,18 @@ function parsePort(url) {
122
127
  }
123
128
  }
124
129
 
125
- /** Drops registry entries whose process has died and persists the result. */
130
+ /**
131
+ * Clears the pid of registry entries whose process has died, and persists the
132
+ * result. Keeps the entry itself (with its `port`) rather than deleting it —
133
+ * that's the only place the last-used port for a runner is remembered, so a
134
+ * later Start can reuse it instead of falling back to whatever's in the
135
+ * runner's URL (or a hardcoded default, if the URL has no explicit port).
136
+ */
126
137
  function pruneDead(registry = loadRegistry()) {
127
138
  let changed = false;
128
139
  for (const [id, entry] of Object.entries(registry)) {
129
- if (!entry?.pid || !isAlive(entry.pid)) {
130
- delete registry[id];
140
+ if (entry?.pid && !isAlive(entry.pid)) {
141
+ registry[id] = { ...entry, pid: null };
131
142
  changed = true;
132
143
  }
133
144
  }
@@ -233,10 +244,10 @@ function stopNode(id, fallbackPort = null) {
233
244
  let signalled = false;
234
245
 
235
246
  let pid = entry?.pid && isAlive(entry.pid) ? entry.pid : null;
247
+ const port = fallbackPort ?? (entry?.port ? Number(entry.port) : null);
236
248
 
237
- if (!pid) {
238
- const port = fallbackPort ?? (entry?.port ? Number(entry.port) : null);
239
- if (port) pid = findPidOnPort(port);
249
+ if (!pid && port) {
250
+ pid = findPidOnPort(port);
240
251
  }
241
252
 
242
253
  if (pid) {
@@ -245,7 +256,15 @@ function stopNode(id, fallbackPort = null) {
245
256
  signalled = true;
246
257
  } catch {}
247
258
  }
248
- delete registry[id];
259
+
260
+ // Keep the port on record (clearing only the pid) so a later Start reuses
261
+ // the same port instead of falling back to the runner's URL — which may
262
+ // not even have an explicit port — or a hardcoded default.
263
+ if (port) {
264
+ registry[id] = { ...entry, pid: null, port: String(port) };
265
+ } else {
266
+ delete registry[id];
267
+ }
249
268
  saveRegistry(registry);
250
269
  return signalled;
251
270
  }
@@ -0,0 +1,18 @@
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
+ 📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
18
+ Backend running on port 3996 (node/runner mode)
@@ -0,0 +1,18 @@
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
+ 📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
18
+ Backend running on port 3996 (node/runner mode)
@@ -204,7 +204,12 @@ async function runAction(r) {
204
204
  const action = await clack.select({ message: `${r.name} — ${r.url}`, options });
205
205
  if (cancelled(action) || action === 'back') return;
206
206
 
207
- const port = parsePort(r.url);
207
+ // Prefer the port this runner actually last ran on (remembered in the
208
+ // local registry even after being stopped) over parsing the URL — the URL
209
+ // may not carry an explicit port at all, and would otherwise silently fall
210
+ // back to the primary's own default port.
211
+ const remembered = runnerProcess.loadRegistry()[r.id]?.port;
212
+ const port = remembered || parsePort(r.url);
208
213
 
209
214
  if (action === 'start') {
210
215
  const s = clack.spinner();
package/backend/server.js CHANGED
@@ -92,16 +92,30 @@ async function start() {
92
92
  try {
93
93
  const reg = loadRegistry();
94
94
  // A self-restart already wrote the replacement's pid under this
95
- // id before this process exits — only clear the entry if it's
96
- // still ours, so we don't erase the new process's registration.
95
+ // id before this process exits — only touch the entry if it's
96
+ // still ours, so we don't clobber the new process's registration.
97
+ // Keep the entry (with its port) rather than deleting it — that's
98
+ // the only place a later manual Start can find the port this
99
+ // runner was last running on.
97
100
  if (reg[runnerId]?.pid === process.pid) {
98
- delete reg[runnerId];
101
+ reg[runnerId] = { ...reg[runnerId], pid: null };
99
102
  saveRegistry(reg);
100
103
  }
101
104
  } catch {}
102
105
  };
103
- process.once('SIGTERM', cleanup);
104
- process.once('SIGINT', cleanup);
106
+ // Adding a SIGTERM/SIGINT listener suppresses Node's default
107
+ // "terminate immediately" behavior — the handler must exit itself,
108
+ // or the process (and the port it's bound to) lives on forever
109
+ // after a plain `kill`/SIGTERM with nothing left to stop it short
110
+ // of SIGKILL.
111
+ process.once('SIGTERM', () => {
112
+ cleanup();
113
+ process.exit(0);
114
+ });
115
+ process.once('SIGINT', () => {
116
+ cleanup();
117
+ process.exit(0);
118
+ });
105
119
  process.once('exit', cleanup);
106
120
  }
107
121
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.6.6",
3
+ "version": "2.6.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"