start-command 0.26.0 → 0.27.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/CHANGELOG.md +11 -1
- package/README.md +1 -1
- package/bunfig.toml +2 -2
- package/eslint.config.mjs +1 -1
- package/package.json +2 -2
- package/src/bin/cli.js +30 -0
- package/src/lib/args-parser.js +72 -6
- package/src/lib/execution-control.js +317 -0
- package/src/lib/isolation.js +22 -4
- package/src/lib/status-formatter.js +46 -2
- package/src/lib/usage.js +5 -1
- package/test/args-parser-control.js +71 -0
- package/test/{args-parser.test.js → args-parser.js} +1 -1
- package/test/cli.js +260 -0
- package/test/docker-autoremove.js +175 -0
- package/test/execution-control.js +253 -0
- package/test/{isolation.test.js → isolation.js} +4 -2
- package/test/merge-changesets.mjs +154 -0
- package/test/release-name.mjs +117 -0
- package/test/{screen-integration.test.js → screen-integration.js} +1 -1
- package/test/{ssh-integration.test.js → ssh-integration.js} +1 -1
- package/test/{status-query.test.js → status-query.js} +2 -0
- package/test/{substitution.test.js → substitution.js} +1 -2
- package/test/{user-manager.test.js → user-manager.js} +17 -0
- package/test/cli.test.js +0 -218
- package/test/docker-autoremove.test.js +0 -164
- package/test/release-name.test.mjs +0 -34
- /package/test/{args-parser-shell.test.js → args-parser-shell.js} +0 -0
- /package/test/{echo-integration.test.js → echo-integration.js} +0 -0
- /package/test/{execution-store.test.js → execution-store.js} +0 -0
- /package/test/{failure-handler.test.js → failure-handler.js} +0 -0
- /package/test/{isolation-cleanup.test.js → isolation-cleanup.js} +0 -0
- /package/test/{isolation-log-utils.test.js → isolation-log-utils.js} +0 -0
- /package/test/{isolation-stacking.test.js → isolation-stacking.js} +0 -0
- /package/test/{output-blocks.test.js → output-blocks.js} +0 -0
- /package/test/{public-exports.test.js → public-exports.js} +0 -0
- /package/test/{regression-84.test.js → regression-84.js} +0 -0
- /package/test/{regression-89.test.js → regression-89.js} +0 -0
- /package/test/{regression-91.test.js → regression-91.js} +0 -0
- /package/test/{sequence-parser.test.js → sequence-parser.js} +0 -0
- /package/test/{session-name-status.test.js → session-name-status.js} +0 -0
- /package/test/{version.test.js → version.js} +0 -0
package/src/lib/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Print usage information */
|
|
2
2
|
function printUsage() {
|
|
3
|
-
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>]
|
|
3
|
+
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>] | $ --stop <id> | $ --terminate <id>
|
|
4
4
|
|
|
5
5
|
Options:
|
|
6
6
|
--isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
|
|
@@ -19,6 +19,8 @@ Options:
|
|
|
19
19
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
20
20
|
--status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
|
|
21
21
|
--list List all tracked executions (--output-format: links-notation|json|text)
|
|
22
|
+
--stop <id> Send CTRL+C/SIGINT to a detached isolated execution
|
|
23
|
+
--terminate <id> Terminate a detached isolated execution immediately
|
|
22
24
|
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
23
25
|
--cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
|
|
24
26
|
--version, -v Show version information
|
|
@@ -36,6 +38,8 @@ Examples:
|
|
|
36
38
|
$ --isolated-user --keep-user -- npm start
|
|
37
39
|
$ --list # List stored execution records
|
|
38
40
|
$ --list --output-format json # List stored records as JSON
|
|
41
|
+
$ --stop my-screen-session # Ask detached execution to stop gracefully
|
|
42
|
+
$ --terminate my-screen-session # Terminate detached execution immediately
|
|
39
43
|
$ --use-command-stream echo "Hello" # Use command-stream library`);
|
|
40
44
|
console.log('');
|
|
41
45
|
console.log('Piping with $:');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { parseArgs } = require('../src/lib/args-parser');
|
|
6
|
+
|
|
7
|
+
describe('control options', () => {
|
|
8
|
+
it('should parse --stop with UUID or session name', () => {
|
|
9
|
+
const result = parseArgs(['--stop', 'my-session']);
|
|
10
|
+
assert.strictEqual(result.wrapperOptions.stop, 'my-session');
|
|
11
|
+
assert.strictEqual(result.command, '');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should parse --stop=value format', () => {
|
|
15
|
+
const result = parseArgs(['--stop=my-session']);
|
|
16
|
+
assert.strictEqual(result.wrapperOptions.stop, 'my-session');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should parse --terminate with UUID or session name', () => {
|
|
20
|
+
const result = parseArgs(['--terminate', 'my-session']);
|
|
21
|
+
assert.strictEqual(result.wrapperOptions.terminate, 'my-session');
|
|
22
|
+
assert.strictEqual(result.command, '');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should parse --terminate=value format', () => {
|
|
26
|
+
const result = parseArgs(['--terminate=my-session']);
|
|
27
|
+
assert.strictEqual(result.wrapperOptions.terminate, 'my-session');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw error for missing --stop argument', () => {
|
|
31
|
+
assert.throws(() => {
|
|
32
|
+
parseArgs(['--stop']);
|
|
33
|
+
}, /requires a UUID or session name argument/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should throw error for empty --stop=value argument', () => {
|
|
37
|
+
assert.throws(() => {
|
|
38
|
+
parseArgs(['--stop=']);
|
|
39
|
+
}, /requires a UUID or session name argument/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should throw error for missing --terminate argument', () => {
|
|
43
|
+
assert.throws(() => {
|
|
44
|
+
parseArgs(['--terminate']);
|
|
45
|
+
}, /requires a UUID or session name argument/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw error for empty --terminate=value argument', () => {
|
|
49
|
+
assert.throws(() => {
|
|
50
|
+
parseArgs(['--terminate=']);
|
|
51
|
+
}, /requires a UUID or session name argument/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should reject combining query and control modes', () => {
|
|
55
|
+
assert.throws(() => {
|
|
56
|
+
parseArgs(['--status', 'uuid-here', '--stop', 'my-session']);
|
|
57
|
+
}, /Cannot combine --status, --list, --stop, --terminate, or --cleanup/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should reject output-format with control modes', () => {
|
|
61
|
+
assert.throws(() => {
|
|
62
|
+
parseArgs(['--stop', 'my-session', '--output-format', 'json']);
|
|
63
|
+
}, /--output-format option is only valid with --status or --list/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should default control options to null', () => {
|
|
67
|
+
const result = parseArgs(['echo', 'hello']);
|
|
68
|
+
assert.strictEqual(result.wrapperOptions.stop, null);
|
|
69
|
+
assert.strictEqual(result.wrapperOptions.terminate, null);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/test/cli.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the CLI
|
|
4
|
+
* Tests version flag and basic CLI behavior
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it } = require('node:test');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const { execSync, spawnSync } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
// Path to the CLI script
|
|
14
|
+
const CLI_PATH = path.join(__dirname, '../src/bin/cli.js');
|
|
15
|
+
|
|
16
|
+
// Timeout for CLI operations - longer on Windows due to cold-start latency
|
|
17
|
+
const CLI_TIMEOUT = process.platform === 'win32' ? 30000 : 10000;
|
|
18
|
+
const CLI_TEST_TIMEOUT = CLI_TIMEOUT + 5000;
|
|
19
|
+
|
|
20
|
+
// Helper to run CLI with timeout
|
|
21
|
+
function runCLI(args = [], options = {}) {
|
|
22
|
+
return spawnSync('bun', [CLI_PATH, ...args], {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
timeout: options.timeout ?? CLI_TIMEOUT,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
START_DISABLE_AUTO_ISSUE: '1',
|
|
28
|
+
START_DISABLE_LOG_UPLOAD: '1',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('CLI version flag', () => {
|
|
34
|
+
it(
|
|
35
|
+
'should display version with --version',
|
|
36
|
+
{ timeout: CLI_TEST_TIMEOUT },
|
|
37
|
+
() => {
|
|
38
|
+
const result = runCLI(['--version']);
|
|
39
|
+
|
|
40
|
+
// Check if process was killed (e.g., due to timeout)
|
|
41
|
+
assert.notStrictEqual(
|
|
42
|
+
result.status,
|
|
43
|
+
null,
|
|
44
|
+
`Process should complete (was killed with signal: ${result.signal})`
|
|
45
|
+
);
|
|
46
|
+
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
47
|
+
|
|
48
|
+
// Check for key elements in version output
|
|
49
|
+
assert.ok(
|
|
50
|
+
result.stdout.includes('start-command version:'),
|
|
51
|
+
'Should display start-command version'
|
|
52
|
+
);
|
|
53
|
+
assert.ok(result.stdout.includes('OS:'), 'Should display OS');
|
|
54
|
+
assert.ok(
|
|
55
|
+
result.stdout.includes('OS Version:'),
|
|
56
|
+
'Should display OS Version'
|
|
57
|
+
);
|
|
58
|
+
// Check for either Bun or Node.js version depending on runtime
|
|
59
|
+
const hasBunVersion = result.stdout.includes('Bun Version:');
|
|
60
|
+
const hasNodeVersion = result.stdout.includes('Node.js Version:');
|
|
61
|
+
assert.ok(
|
|
62
|
+
hasBunVersion || hasNodeVersion,
|
|
63
|
+
'Should display Bun Version or Node.js Version'
|
|
64
|
+
);
|
|
65
|
+
assert.ok(
|
|
66
|
+
result.stdout.includes('Architecture:'),
|
|
67
|
+
'Should display Architecture'
|
|
68
|
+
);
|
|
69
|
+
assert.ok(
|
|
70
|
+
result.stdout.includes('Isolation tools:'),
|
|
71
|
+
'Should display Isolation tools section'
|
|
72
|
+
);
|
|
73
|
+
assert.ok(
|
|
74
|
+
result.stdout.includes('screen:'),
|
|
75
|
+
'Should check for screen installation'
|
|
76
|
+
);
|
|
77
|
+
assert.ok(
|
|
78
|
+
result.stdout.includes('tmux:'),
|
|
79
|
+
'Should check for tmux installation'
|
|
80
|
+
);
|
|
81
|
+
assert.ok(
|
|
82
|
+
result.stdout.includes('docker:'),
|
|
83
|
+
'Should check for docker installation'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
it('should display version with -v', { timeout: CLI_TEST_TIMEOUT }, () => {
|
|
89
|
+
const result = runCLI(['-v']);
|
|
90
|
+
|
|
91
|
+
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
92
|
+
assert.ok(
|
|
93
|
+
result.stdout.includes('start-command version:'),
|
|
94
|
+
'Should display start-command version'
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it(
|
|
99
|
+
'should show correct package version',
|
|
100
|
+
{ timeout: CLI_TEST_TIMEOUT },
|
|
101
|
+
() => {
|
|
102
|
+
const result = runCLI(['--version']);
|
|
103
|
+
const packageJson = JSON.parse(
|
|
104
|
+
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
assert.ok(
|
|
108
|
+
result.stdout.includes(`start-command version: ${packageJson.version}`),
|
|
109
|
+
`Should display version ${packageJson.version}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('CLI basic behavior', () => {
|
|
116
|
+
it('should show usage when no arguments provided', () => {
|
|
117
|
+
const result = runCLI([]);
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
120
|
+
assert.ok(result.stdout.includes('Usage:'), 'Should display usage');
|
|
121
|
+
assert.ok(
|
|
122
|
+
result.stdout.includes('--version'),
|
|
123
|
+
'Usage should mention --version flag'
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should show usage when no command provided after --', () => {
|
|
128
|
+
const result = runCLI(['--']);
|
|
129
|
+
|
|
130
|
+
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
131
|
+
assert.ok(result.stdout.includes('Usage:'), 'Should display usage');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('CLI isolation output (issue #67)', () => {
|
|
136
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
137
|
+
|
|
138
|
+
it('should display screen session name when using screen isolation', async () => {
|
|
139
|
+
if (!isCommandAvailable('screen')) {
|
|
140
|
+
console.log(' Skipping: screen not installed');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = runCLI(['-i', 'screen', '--', 'echo', 'hello']);
|
|
145
|
+
|
|
146
|
+
// The output should contain the screen session name (in format screen-timestamp-random)
|
|
147
|
+
// Check that the session UUID is displayed
|
|
148
|
+
assert.ok(
|
|
149
|
+
result.stdout.includes('│ session'),
|
|
150
|
+
'Should display session UUID'
|
|
151
|
+
);
|
|
152
|
+
// Check that screen isolation info is displayed
|
|
153
|
+
assert.ok(
|
|
154
|
+
result.stdout.includes('│ isolation screen'),
|
|
155
|
+
'Should display screen isolation'
|
|
156
|
+
);
|
|
157
|
+
// Check that the actual screen session name is displayed (issue #67 fix)
|
|
158
|
+
assert.ok(
|
|
159
|
+
result.stdout.includes('│ screen screen-'),
|
|
160
|
+
'Should display actual screen session name for reconnection (issue #67)'
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should display tmux session name when using tmux isolation', async () => {
|
|
165
|
+
if (!isCommandAvailable('tmux')) {
|
|
166
|
+
console.log(' Skipping: tmux not installed');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result = runCLI(['-i', 'tmux', '--', 'echo', 'hello']);
|
|
171
|
+
|
|
172
|
+
// The output should contain the tmux session name
|
|
173
|
+
assert.ok(
|
|
174
|
+
result.stdout.includes('│ session'),
|
|
175
|
+
'Should display session UUID'
|
|
176
|
+
);
|
|
177
|
+
assert.ok(
|
|
178
|
+
result.stdout.includes('│ isolation tmux'),
|
|
179
|
+
'Should display tmux isolation'
|
|
180
|
+
);
|
|
181
|
+
// Check that the actual tmux session name is displayed (issue #67 fix)
|
|
182
|
+
assert.ok(
|
|
183
|
+
result.stdout.includes('│ tmux tmux-'),
|
|
184
|
+
'Should display actual tmux session name for reconnection (issue #67)'
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it(
|
|
189
|
+
'should display docker container name when using docker isolation',
|
|
190
|
+
{ timeout: 70000 },
|
|
191
|
+
() => {
|
|
192
|
+
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
193
|
+
|
|
194
|
+
if (!canRunLinuxDockerImages()) {
|
|
195
|
+
console.log(
|
|
196
|
+
' Skipping: docker not available or cannot run Linux images'
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const containerName = `docker-cli-${Date.now()}`;
|
|
202
|
+
let result;
|
|
203
|
+
try {
|
|
204
|
+
result = runCLI(
|
|
205
|
+
[
|
|
206
|
+
'-i',
|
|
207
|
+
'docker',
|
|
208
|
+
'-d',
|
|
209
|
+
'--session',
|
|
210
|
+
containerName,
|
|
211
|
+
'--image',
|
|
212
|
+
'alpine:latest',
|
|
213
|
+
'--',
|
|
214
|
+
'echo',
|
|
215
|
+
'hello',
|
|
216
|
+
],
|
|
217
|
+
{ timeout: 60000 }
|
|
218
|
+
);
|
|
219
|
+
} finally {
|
|
220
|
+
try {
|
|
221
|
+
execSync(`docker rm -f ${containerName}`, {
|
|
222
|
+
stdio: 'ignore',
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
// Container may have already exited.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assert.notStrictEqual(
|
|
230
|
+
result.status,
|
|
231
|
+
null,
|
|
232
|
+
`CLI should complete before timeout. signal=${result.signal}, stderr=${result.stderr}`
|
|
233
|
+
);
|
|
234
|
+
assert.strictEqual(
|
|
235
|
+
result.status,
|
|
236
|
+
0,
|
|
237
|
+
`CLI should exit 0. stdout=${result.stdout}, stderr=${result.stderr}`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// The output should contain the docker container name
|
|
241
|
+
assert.ok(
|
|
242
|
+
result.stdout.includes('│ session'),
|
|
243
|
+
'Should display session UUID'
|
|
244
|
+
);
|
|
245
|
+
assert.ok(
|
|
246
|
+
result.stdout.includes('│ isolation docker'),
|
|
247
|
+
'Should display docker isolation'
|
|
248
|
+
);
|
|
249
|
+
assert.ok(
|
|
250
|
+
result.stdout.includes('│ image alpine:latest'),
|
|
251
|
+
'Should display docker image'
|
|
252
|
+
);
|
|
253
|
+
// Check that the actual container name is displayed (issue #67 fix)
|
|
254
|
+
assert.ok(
|
|
255
|
+
result.stdout.includes('│ container docker-'),
|
|
256
|
+
'Should display actual container name for reconnection (issue #67)'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Docker auto-remove container feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
9
|
+
const { runInDocker } = require('../src/lib/isolation');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
// Helper to wait for a condition with timeout
|
|
13
|
+
async function waitFor(conditionFn, timeout = 5000, interval = 100) {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
while (Date.now() - startTime < timeout) {
|
|
16
|
+
if (conditionFn()) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use the canRunLinuxDockerImages function from isolation module
|
|
25
|
+
// to properly detect if Linux containers can run (handles Windows containers mode)
|
|
26
|
+
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
27
|
+
const DOCKER_TEST_TIMEOUT = 20000;
|
|
28
|
+
|
|
29
|
+
describe('Docker Auto-Remove Container Feature', () => {
|
|
30
|
+
// These tests verify the --auto-remove-docker-container option
|
|
31
|
+
// which automatically removes the container after exit (disabled by default)
|
|
32
|
+
|
|
33
|
+
describe('auto-remove enabled', () => {
|
|
34
|
+
it(
|
|
35
|
+
'should automatically remove container when autoRemoveDockerContainer is true',
|
|
36
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
37
|
+
async () => {
|
|
38
|
+
if (!canRunLinuxDockerImages()) {
|
|
39
|
+
console.log(
|
|
40
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
41
|
+
);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const containerName = `test-autoremove-${Date.now()}`;
|
|
46
|
+
|
|
47
|
+
// Run command with autoRemoveDockerContainer enabled
|
|
48
|
+
const result = await runInDocker('echo "test" && sleep 0.5', {
|
|
49
|
+
image: 'alpine:latest',
|
|
50
|
+
session: containerName,
|
|
51
|
+
detached: true,
|
|
52
|
+
keepAlive: false,
|
|
53
|
+
autoRemoveDockerContainer: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.strictEqual(result.success, true);
|
|
57
|
+
assert.ok(
|
|
58
|
+
result.message.includes('automatically removed'),
|
|
59
|
+
'Message should indicate auto-removal'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Wait for container to finish and be removed
|
|
63
|
+
const containerRemoved = await waitFor(() => {
|
|
64
|
+
try {
|
|
65
|
+
execSync(`docker inspect -f '{{.State.Status}}' ${containerName}`, {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
return false; // Container still exists
|
|
70
|
+
} catch {
|
|
71
|
+
return true; // Container does not exist (removed)
|
|
72
|
+
}
|
|
73
|
+
}, 10000);
|
|
74
|
+
|
|
75
|
+
assert.ok(
|
|
76
|
+
containerRemoved,
|
|
77
|
+
'Container should be automatically removed after exit with --auto-remove-docker-container'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Double-check with docker ps -a that container is completely removed
|
|
81
|
+
try {
|
|
82
|
+
const allContainers = execSync('docker ps -a', {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
assert.ok(
|
|
87
|
+
!allContainers.includes(containerName),
|
|
88
|
+
'Container should NOT appear in docker ps -a (completely removed)'
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
' ✓ Docker container auto-removed after exit (filesystem not preserved)'
|
|
92
|
+
);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
assert.fail(`Failed to verify container removal: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// No cleanup needed - container should already be removed
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('auto-remove disabled (default)', () => {
|
|
103
|
+
it(
|
|
104
|
+
'should preserve container filesystem by default (without autoRemoveDockerContainer)',
|
|
105
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
106
|
+
async () => {
|
|
107
|
+
if (!canRunLinuxDockerImages()) {
|
|
108
|
+
console.log(
|
|
109
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const containerName = `test-preserve-${Date.now()}`;
|
|
115
|
+
|
|
116
|
+
// Run command without autoRemoveDockerContainer
|
|
117
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
118
|
+
image: 'alpine:latest',
|
|
119
|
+
session: containerName,
|
|
120
|
+
detached: true,
|
|
121
|
+
keepAlive: false,
|
|
122
|
+
autoRemoveDockerContainer: false,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result.success, true);
|
|
126
|
+
assert.ok(
|
|
127
|
+
result.message.includes('filesystem will be preserved'),
|
|
128
|
+
'Message should indicate filesystem preservation'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Wait for container to exit
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
try {
|
|
134
|
+
const status = execSync(
|
|
135
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
136
|
+
{
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
+
}
|
|
140
|
+
).trim();
|
|
141
|
+
return status === 'exited';
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}, 10000);
|
|
146
|
+
|
|
147
|
+
// Container should still exist (in exited state)
|
|
148
|
+
try {
|
|
149
|
+
const allContainers = execSync('docker ps -a', {
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
152
|
+
});
|
|
153
|
+
assert.ok(
|
|
154
|
+
allContainers.includes(containerName),
|
|
155
|
+
'Container should appear in docker ps -a (filesystem preserved)'
|
|
156
|
+
);
|
|
157
|
+
console.log(
|
|
158
|
+
' ✓ Docker container filesystem preserved by default (can be re-entered)'
|
|
159
|
+
);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
assert.fail(
|
|
162
|
+
`Failed to verify container preservation: ${err.message}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Clean up
|
|
167
|
+
try {
|
|
168
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
169
|
+
} catch {
|
|
170
|
+
// Ignore cleanup errors
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|