start-command 0.7.6 → 0.10.0
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/ARCHITECTURE.md +297 -0
- package/CHANGELOG.md +46 -0
- package/README.md +68 -7
- package/REQUIREMENTS.md +72 -1
- package/experiments/user-isolation-research.md +83 -0
- package/package.json +1 -1
- package/src/bin/cli.js +131 -36
- package/src/lib/args-parser.js +95 -5
- package/src/lib/isolation.js +184 -43
- package/src/lib/user-manager.js +429 -0
- package/test/args-parser.test.js +309 -0
- package/test/docker-autoremove.test.js +169 -0
- package/test/isolation-cleanup.test.js +377 -0
- package/test/isolation.test.js +233 -0
- package/test/user-manager.test.js +286 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Resource cleanup tests for isolation module
|
|
4
|
+
* Tests that verify isolation environments release resources after command execution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it } = require('node:test');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
10
|
+
|
|
11
|
+
describe('Isolation Resource Cleanup Verification', () => {
|
|
12
|
+
// These tests verify that isolation environments release resources after command execution
|
|
13
|
+
// This ensures uniform behavior across all backends where resources are freed by default
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
runInScreen,
|
|
17
|
+
runInTmux,
|
|
18
|
+
runInDocker,
|
|
19
|
+
} = require('../src/lib/isolation');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// Helper to wait for a condition with timeout
|
|
23
|
+
async function waitFor(conditionFn, timeout = 5000, interval = 100) {
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
while (Date.now() - startTime < timeout) {
|
|
26
|
+
if (conditionFn()) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('screen resource cleanup', () => {
|
|
35
|
+
it('should not list screen session after command completes (auto-exit by default)', async () => {
|
|
36
|
+
if (!isCommandAvailable('screen')) {
|
|
37
|
+
console.log(' Skipping: screen not installed');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sessionName = `test-cleanup-screen-${Date.now()}`;
|
|
42
|
+
|
|
43
|
+
// Run a quick command in detached mode
|
|
44
|
+
const result = await runInScreen('echo "test" && sleep 0.1', {
|
|
45
|
+
session: sessionName,
|
|
46
|
+
detached: true,
|
|
47
|
+
keepAlive: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(result.success, true);
|
|
51
|
+
|
|
52
|
+
// Wait for the session to exit naturally (should happen quickly)
|
|
53
|
+
const sessionGone = await waitFor(() => {
|
|
54
|
+
try {
|
|
55
|
+
const sessions = execSync('screen -ls', {
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
return !sessions.includes(sessionName);
|
|
60
|
+
} catch {
|
|
61
|
+
// screen -ls returns non-zero when no sessions exist
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}, 10000);
|
|
65
|
+
|
|
66
|
+
assert.ok(
|
|
67
|
+
sessionGone,
|
|
68
|
+
'Screen session should not be in the list after command completes (auto-exit by default)'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Double-check with screen -ls to verify no active session
|
|
72
|
+
try {
|
|
73
|
+
const sessions = execSync('screen -ls', {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
});
|
|
77
|
+
assert.ok(
|
|
78
|
+
!sessions.includes(sessionName),
|
|
79
|
+
'Session should not appear in screen -ls output'
|
|
80
|
+
);
|
|
81
|
+
console.log(' ✓ Screen session auto-exited and resources released');
|
|
82
|
+
} catch {
|
|
83
|
+
// screen -ls returns non-zero when no sessions - this is expected
|
|
84
|
+
console.log(
|
|
85
|
+
' ✓ Screen session auto-exited (no sessions found in screen -ls)'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should keep screen session alive when keepAlive is true', async () => {
|
|
91
|
+
if (!isCommandAvailable('screen')) {
|
|
92
|
+
console.log(' Skipping: screen not installed');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sessionName = `test-keepalive-screen-${Date.now()}`;
|
|
97
|
+
|
|
98
|
+
// Run command with keepAlive enabled
|
|
99
|
+
const result = await runInScreen('echo "test"', {
|
|
100
|
+
session: sessionName,
|
|
101
|
+
detached: true,
|
|
102
|
+
keepAlive: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(result.success, true);
|
|
106
|
+
|
|
107
|
+
// Wait a bit for the command to complete
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
109
|
+
|
|
110
|
+
// Session should still exist
|
|
111
|
+
try {
|
|
112
|
+
const sessions = execSync('screen -ls', {
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
115
|
+
});
|
|
116
|
+
assert.ok(
|
|
117
|
+
sessions.includes(sessionName),
|
|
118
|
+
'Session should still be alive with keepAlive=true'
|
|
119
|
+
);
|
|
120
|
+
console.log(
|
|
121
|
+
' ✓ Screen session kept alive as expected with --keep-alive'
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
assert.fail(
|
|
125
|
+
'screen -ls should show the session when keepAlive is true'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clean up
|
|
130
|
+
try {
|
|
131
|
+
execSync(`screen -S ${sessionName} -X quit`, { stdio: 'ignore' });
|
|
132
|
+
} catch {
|
|
133
|
+
// Ignore cleanup errors
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('tmux resource cleanup', () => {
|
|
139
|
+
it('should not list tmux session after command completes (auto-exit by default)', async () => {
|
|
140
|
+
if (!isCommandAvailable('tmux')) {
|
|
141
|
+
console.log(' Skipping: tmux not installed');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sessionName = `test-cleanup-tmux-${Date.now()}`;
|
|
146
|
+
|
|
147
|
+
// Run a quick command in detached mode
|
|
148
|
+
const result = await runInTmux('echo "test" && sleep 0.1', {
|
|
149
|
+
session: sessionName,
|
|
150
|
+
detached: true,
|
|
151
|
+
keepAlive: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(result.success, true);
|
|
155
|
+
|
|
156
|
+
// Wait for the session to exit naturally
|
|
157
|
+
const sessionGone = await waitFor(() => {
|
|
158
|
+
try {
|
|
159
|
+
const sessions = execSync('tmux ls', {
|
|
160
|
+
encoding: 'utf8',
|
|
161
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
162
|
+
});
|
|
163
|
+
return !sessions.includes(sessionName);
|
|
164
|
+
} catch {
|
|
165
|
+
// tmux ls returns non-zero when no sessions exist
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}, 10000);
|
|
169
|
+
|
|
170
|
+
assert.ok(
|
|
171
|
+
sessionGone,
|
|
172
|
+
'Tmux session should not be in the list after command completes (auto-exit by default)'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Double-check with tmux ls
|
|
176
|
+
try {
|
|
177
|
+
const sessions = execSync('tmux ls', {
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
|
+
});
|
|
181
|
+
assert.ok(
|
|
182
|
+
!sessions.includes(sessionName),
|
|
183
|
+
'Session should not appear in tmux ls output'
|
|
184
|
+
);
|
|
185
|
+
console.log(' ✓ Tmux session auto-exited and resources released');
|
|
186
|
+
} catch {
|
|
187
|
+
// tmux ls returns non-zero when no sessions - this is expected
|
|
188
|
+
console.log(
|
|
189
|
+
' ✓ Tmux session auto-exited (no sessions found in tmux ls)'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should keep tmux session alive when keepAlive is true', async () => {
|
|
195
|
+
if (!isCommandAvailable('tmux')) {
|
|
196
|
+
console.log(' Skipping: tmux not installed');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sessionName = `test-keepalive-tmux-${Date.now()}`;
|
|
201
|
+
|
|
202
|
+
// Run command with keepAlive enabled
|
|
203
|
+
const result = await runInTmux('echo "test"', {
|
|
204
|
+
session: sessionName,
|
|
205
|
+
detached: true,
|
|
206
|
+
keepAlive: true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
assert.strictEqual(result.success, true);
|
|
210
|
+
|
|
211
|
+
// Wait a bit for the command to complete
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
213
|
+
|
|
214
|
+
// Session should still exist
|
|
215
|
+
try {
|
|
216
|
+
const sessions = execSync('tmux ls', {
|
|
217
|
+
encoding: 'utf8',
|
|
218
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
219
|
+
});
|
|
220
|
+
assert.ok(
|
|
221
|
+
sessions.includes(sessionName),
|
|
222
|
+
'Session should still be alive with keepAlive=true'
|
|
223
|
+
);
|
|
224
|
+
console.log(
|
|
225
|
+
' ✓ Tmux session kept alive as expected with --keep-alive'
|
|
226
|
+
);
|
|
227
|
+
} catch {
|
|
228
|
+
assert.fail('tmux ls should show the session when keepAlive is true');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Clean up
|
|
232
|
+
try {
|
|
233
|
+
execSync(`tmux kill-session -t ${sessionName}`, { stdio: 'ignore' });
|
|
234
|
+
} catch {
|
|
235
|
+
// Ignore cleanup errors
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('docker resource cleanup', () => {
|
|
241
|
+
// Helper function to check if docker daemon is running
|
|
242
|
+
function isDockerRunning() {
|
|
243
|
+
if (!isCommandAvailable('docker')) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
execSync('docker info', { stdio: 'ignore', timeout: 5000 });
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
it('should show docker container as exited after command completes (auto-exit by default)', async () => {
|
|
255
|
+
if (!isDockerRunning()) {
|
|
256
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const containerName = `test-cleanup-docker-${Date.now()}`;
|
|
261
|
+
|
|
262
|
+
// Run a quick command in detached mode
|
|
263
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
264
|
+
image: 'alpine:latest',
|
|
265
|
+
session: containerName,
|
|
266
|
+
detached: true,
|
|
267
|
+
keepAlive: false,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
assert.strictEqual(result.success, true);
|
|
271
|
+
|
|
272
|
+
// Wait for the container to exit
|
|
273
|
+
const containerExited = await waitFor(() => {
|
|
274
|
+
try {
|
|
275
|
+
const status = execSync(
|
|
276
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
277
|
+
{
|
|
278
|
+
encoding: 'utf8',
|
|
279
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
280
|
+
}
|
|
281
|
+
).trim();
|
|
282
|
+
return status === 'exited';
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}, 10000);
|
|
287
|
+
|
|
288
|
+
assert.ok(
|
|
289
|
+
containerExited,
|
|
290
|
+
'Docker container should be in exited state after command completes (auto-exit by default)'
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Verify with docker ps -a that container is exited (not running)
|
|
294
|
+
try {
|
|
295
|
+
const allContainers = execSync('docker ps -a', {
|
|
296
|
+
encoding: 'utf8',
|
|
297
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
298
|
+
});
|
|
299
|
+
assert.ok(
|
|
300
|
+
allContainers.includes(containerName),
|
|
301
|
+
'Container should appear in docker ps -a'
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const runningContainers = execSync('docker ps', {
|
|
305
|
+
encoding: 'utf8',
|
|
306
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
307
|
+
});
|
|
308
|
+
assert.ok(
|
|
309
|
+
!runningContainers.includes(containerName),
|
|
310
|
+
'Container should NOT appear in docker ps (not running)'
|
|
311
|
+
);
|
|
312
|
+
console.log(
|
|
313
|
+
' ✓ Docker container auto-exited and stopped (resources released, filesystem preserved)'
|
|
314
|
+
);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
assert.fail(`Failed to verify container status: ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Clean up
|
|
320
|
+
try {
|
|
321
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
322
|
+
} catch {
|
|
323
|
+
// Ignore cleanup errors
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should keep docker container running when keepAlive is true', async () => {
|
|
328
|
+
if (!isDockerRunning()) {
|
|
329
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const containerName = `test-keepalive-docker-${Date.now()}`;
|
|
334
|
+
|
|
335
|
+
// Run command with keepAlive enabled
|
|
336
|
+
const result = await runInDocker('echo "test"', {
|
|
337
|
+
image: 'alpine:latest',
|
|
338
|
+
session: containerName,
|
|
339
|
+
detached: true,
|
|
340
|
+
keepAlive: true,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
assert.strictEqual(result.success, true);
|
|
344
|
+
|
|
345
|
+
// Wait a bit for the command to complete
|
|
346
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
347
|
+
|
|
348
|
+
// Container should still be running
|
|
349
|
+
try {
|
|
350
|
+
const status = execSync(
|
|
351
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
352
|
+
{
|
|
353
|
+
encoding: 'utf8',
|
|
354
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
355
|
+
}
|
|
356
|
+
).trim();
|
|
357
|
+
assert.strictEqual(
|
|
358
|
+
status,
|
|
359
|
+
'running',
|
|
360
|
+
'Container should still be running with keepAlive=true'
|
|
361
|
+
);
|
|
362
|
+
console.log(
|
|
363
|
+
' ✓ Docker container kept running as expected with --keep-alive'
|
|
364
|
+
);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
assert.fail(`Failed to verify container is running: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Clean up
|
|
370
|
+
try {
|
|
371
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
372
|
+
} catch {
|
|
373
|
+
// Ignore cleanup errors
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
package/test/isolation.test.js
CHANGED
|
@@ -16,6 +16,39 @@ const {
|
|
|
16
16
|
} = require('../src/lib/isolation');
|
|
17
17
|
|
|
18
18
|
describe('Isolation Module', () => {
|
|
19
|
+
describe('wrapCommandWithUser', () => {
|
|
20
|
+
const { wrapCommandWithUser } = require('../src/lib/isolation');
|
|
21
|
+
|
|
22
|
+
it('should return command unchanged when user is null', () => {
|
|
23
|
+
const command = 'echo hello';
|
|
24
|
+
const result = wrapCommandWithUser(command, null);
|
|
25
|
+
assert.strictEqual(result, command);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should wrap command with sudo when user is specified', () => {
|
|
29
|
+
const command = 'echo hello';
|
|
30
|
+
const result = wrapCommandWithUser(command, 'john');
|
|
31
|
+
assert.ok(result.includes('sudo'));
|
|
32
|
+
assert.ok(result.includes('-u john'));
|
|
33
|
+
assert.ok(result.includes('echo hello'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should escape single quotes in command', () => {
|
|
37
|
+
const command = "echo 'hello'";
|
|
38
|
+
const result = wrapCommandWithUser(command, 'www-data');
|
|
39
|
+
// Should escape quotes properly for shell
|
|
40
|
+
assert.ok(result.includes('sudo'));
|
|
41
|
+
assert.ok(result.includes('-u www-data'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should use non-interactive sudo', () => {
|
|
45
|
+
const command = 'npm start';
|
|
46
|
+
const result = wrapCommandWithUser(command, 'john');
|
|
47
|
+
// Should include -n flag for non-interactive
|
|
48
|
+
assert.ok(result.includes('sudo -n'));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
19
52
|
describe('isCommandAvailable', () => {
|
|
20
53
|
it('should return true for common commands (echo)', () => {
|
|
21
54
|
// echo is available on all platforms
|
|
@@ -243,6 +276,206 @@ describe('Isolation Runner Error Handling', () => {
|
|
|
243
276
|
});
|
|
244
277
|
});
|
|
245
278
|
|
|
279
|
+
describe('Isolation Keep-Alive Behavior', () => {
|
|
280
|
+
// Tests for the --keep-alive option behavior
|
|
281
|
+
// These test the message output and options handling
|
|
282
|
+
|
|
283
|
+
const {
|
|
284
|
+
runInScreen,
|
|
285
|
+
runInTmux,
|
|
286
|
+
runInDocker,
|
|
287
|
+
} = require('../src/lib/isolation');
|
|
288
|
+
const { execSync } = require('child_process');
|
|
289
|
+
|
|
290
|
+
describe('runInScreen keep-alive messages', () => {
|
|
291
|
+
it('should include auto-exit message by default in detached mode', async () => {
|
|
292
|
+
if (!isCommandAvailable('screen')) {
|
|
293
|
+
console.log(' Skipping: screen not installed');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const result = await runInScreen('echo test', {
|
|
298
|
+
session: `test-autoexit-${Date.now()}`,
|
|
299
|
+
detached: true,
|
|
300
|
+
keepAlive: false,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
assert.strictEqual(result.success, true);
|
|
304
|
+
assert.ok(
|
|
305
|
+
result.message.includes('exit automatically'),
|
|
306
|
+
'Message should indicate auto-exit behavior'
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Clean up
|
|
310
|
+
try {
|
|
311
|
+
execSync(`screen -S ${result.sessionName} -X quit`, {
|
|
312
|
+
stdio: 'ignore',
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
// Session may have already exited
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should include keep-alive message when keepAlive is true', async () => {
|
|
320
|
+
if (!isCommandAvailable('screen')) {
|
|
321
|
+
console.log(' Skipping: screen not installed');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = await runInScreen('echo test', {
|
|
326
|
+
session: `test-keepalive-${Date.now()}`,
|
|
327
|
+
detached: true,
|
|
328
|
+
keepAlive: true,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
assert.strictEqual(result.success, true);
|
|
332
|
+
assert.ok(
|
|
333
|
+
result.message.includes('stay alive'),
|
|
334
|
+
'Message should indicate keep-alive behavior'
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Clean up
|
|
338
|
+
try {
|
|
339
|
+
execSync(`screen -S ${result.sessionName} -X quit`, {
|
|
340
|
+
stdio: 'ignore',
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore cleanup errors
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('runInTmux keep-alive messages', () => {
|
|
349
|
+
it('should include auto-exit message by default in detached mode', async () => {
|
|
350
|
+
if (!isCommandAvailable('tmux')) {
|
|
351
|
+
console.log(' Skipping: tmux not installed');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const result = await runInTmux('echo test', {
|
|
356
|
+
session: `test-autoexit-${Date.now()}`,
|
|
357
|
+
detached: true,
|
|
358
|
+
keepAlive: false,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
assert.strictEqual(result.success, true);
|
|
362
|
+
assert.ok(
|
|
363
|
+
result.message.includes('exit automatically'),
|
|
364
|
+
'Message should indicate auto-exit behavior'
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Clean up
|
|
368
|
+
try {
|
|
369
|
+
execSync(`tmux kill-session -t ${result.sessionName}`, {
|
|
370
|
+
stdio: 'ignore',
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
// Session may have already exited
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should include keep-alive message when keepAlive is true', async () => {
|
|
378
|
+
if (!isCommandAvailable('tmux')) {
|
|
379
|
+
console.log(' Skipping: tmux not installed');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const result = await runInTmux('echo test', {
|
|
384
|
+
session: `test-keepalive-${Date.now()}`,
|
|
385
|
+
detached: true,
|
|
386
|
+
keepAlive: true,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
assert.strictEqual(result.success, true);
|
|
390
|
+
assert.ok(
|
|
391
|
+
result.message.includes('stay alive'),
|
|
392
|
+
'Message should indicate keep-alive behavior'
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Clean up
|
|
396
|
+
try {
|
|
397
|
+
execSync(`tmux kill-session -t ${result.sessionName}`, {
|
|
398
|
+
stdio: 'ignore',
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
// Ignore cleanup errors
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('runInDocker keep-alive messages', () => {
|
|
407
|
+
// Helper function to check if docker daemon is running
|
|
408
|
+
function isDockerRunning() {
|
|
409
|
+
if (!isCommandAvailable('docker')) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
// Try to ping the docker daemon
|
|
414
|
+
execSync('docker info', { stdio: 'ignore', timeout: 5000 });
|
|
415
|
+
return true;
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
it('should include auto-exit message by default in detached mode', async () => {
|
|
422
|
+
if (!isDockerRunning()) {
|
|
423
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const containerName = `test-autoexit-${Date.now()}`;
|
|
428
|
+
const result = await runInDocker('echo test', {
|
|
429
|
+
image: 'alpine:latest',
|
|
430
|
+
session: containerName,
|
|
431
|
+
detached: true,
|
|
432
|
+
keepAlive: false,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
assert.strictEqual(result.success, true);
|
|
436
|
+
assert.ok(
|
|
437
|
+
result.message.includes('exit automatically'),
|
|
438
|
+
'Message should indicate auto-exit behavior'
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Clean up
|
|
442
|
+
try {
|
|
443
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
444
|
+
} catch {
|
|
445
|
+
// Container may have already been removed
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should include keep-alive message when keepAlive is true', async () => {
|
|
450
|
+
if (!isDockerRunning()) {
|
|
451
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const containerName = `test-keepalive-${Date.now()}`;
|
|
456
|
+
const result = await runInDocker('echo test', {
|
|
457
|
+
image: 'alpine:latest',
|
|
458
|
+
session: containerName,
|
|
459
|
+
detached: true,
|
|
460
|
+
keepAlive: true,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
assert.strictEqual(result.success, true);
|
|
464
|
+
assert.ok(
|
|
465
|
+
result.message.includes('stay alive'),
|
|
466
|
+
'Message should indicate keep-alive behavior'
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Clean up
|
|
470
|
+
try {
|
|
471
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
472
|
+
} catch {
|
|
473
|
+
// Ignore cleanup errors
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
246
479
|
describe('Isolation Runner with Available Backends', () => {
|
|
247
480
|
// Integration-style tests that run if backends are available
|
|
248
481
|
// These test actual execution in detached mode (quick and non-blocking)
|