start-command 0.29.1 → 0.30.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 +12 -0
- package/package.json +1 -1
- package/src/bin/cli.js +7 -0
- package/src/lib/args-parser.js +57 -13
- package/src/lib/command-builder.js +13 -2
- package/src/lib/docker-cleanup.js +145 -0
- package/src/lib/docker-utils.js +133 -6
- package/src/lib/isolation.js +52 -29
- package/src/lib/usage.js +4 -1
- package/test/args-parser-docker-cleanup.js +143 -0
- package/test/args-parser.js +0 -69
- package/test/command-builder-docker-cleanup.js +35 -0
- package/test/docker-autoremove.js +204 -29
- package/test/regression-138.js +199 -0
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
function parseDocker(args = []) {
|
|
8
|
+
return parseArgs([
|
|
9
|
+
'--isolated',
|
|
10
|
+
'docker',
|
|
11
|
+
'--image',
|
|
12
|
+
'alpine',
|
|
13
|
+
...args,
|
|
14
|
+
'--',
|
|
15
|
+
'npm',
|
|
16
|
+
'test',
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('docker container cleanup options', () => {
|
|
21
|
+
it('should parse --auto-remove-docker-container flag', () => {
|
|
22
|
+
const result = parseDocker(['--auto-remove-docker-container']);
|
|
23
|
+
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should parse --always-cleanup-container flag', () => {
|
|
27
|
+
const result = parseDocker(['--always-cleanup-container']);
|
|
28
|
+
assert.strictEqual(result.wrapperOptions.alwaysCleanupContainer, true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should parse --keep-container flag', () => {
|
|
32
|
+
const result = parseDocker(['--keep-container']);
|
|
33
|
+
assert.strictEqual(result.wrapperOptions.keepContainer, true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should parse --keep-container-on-fail flag', () => {
|
|
37
|
+
const result = parseDocker(['--keep-container-on-fail']);
|
|
38
|
+
assert.strictEqual(result.wrapperOptions.keepContainerOnFail, true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should default cleanup flags to false', () => {
|
|
42
|
+
const result = parseDocker();
|
|
43
|
+
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, false);
|
|
44
|
+
assert.strictEqual(result.wrapperOptions.alwaysCleanupContainer, false);
|
|
45
|
+
assert.strictEqual(result.wrapperOptions.keepContainer, false);
|
|
46
|
+
assert.strictEqual(result.wrapperOptions.keepContainerOnFail, false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should throw error for docker cleanup options without docker isolation', () => {
|
|
50
|
+
assert.throws(() => {
|
|
51
|
+
parseArgs([
|
|
52
|
+
'-i',
|
|
53
|
+
'tmux',
|
|
54
|
+
'--auto-remove-docker-container',
|
|
55
|
+
'--',
|
|
56
|
+
'npm',
|
|
57
|
+
'test',
|
|
58
|
+
]);
|
|
59
|
+
}, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
|
|
60
|
+
assert.throws(() => {
|
|
61
|
+
parseArgs([
|
|
62
|
+
'-i',
|
|
63
|
+
'tmux',
|
|
64
|
+
'--always-cleanup-container',
|
|
65
|
+
'--',
|
|
66
|
+
'npm',
|
|
67
|
+
'test',
|
|
68
|
+
]);
|
|
69
|
+
}, /--always-cleanup-container option is only valid when isolation stack includes docker/);
|
|
70
|
+
assert.throws(() => {
|
|
71
|
+
parseArgs(['-i', 'tmux', '--keep-container', '--', 'npm', 'test']);
|
|
72
|
+
}, /--keep-container option is only valid when isolation stack includes docker/);
|
|
73
|
+
assert.throws(() => {
|
|
74
|
+
parseArgs([
|
|
75
|
+
'-i',
|
|
76
|
+
'tmux',
|
|
77
|
+
'--keep-container-on-fail',
|
|
78
|
+
'--',
|
|
79
|
+
'npm',
|
|
80
|
+
'test',
|
|
81
|
+
]);
|
|
82
|
+
}, /--keep-container-on-fail option is only valid when isolation stack includes docker/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw error for docker cleanup options without isolation', () => {
|
|
86
|
+
assert.throws(() => {
|
|
87
|
+
parseArgs(['--auto-remove-docker-container', '--', 'npm', 'test']);
|
|
88
|
+
}, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
|
|
89
|
+
assert.throws(() => {
|
|
90
|
+
parseArgs(['--always-cleanup-container', '--', 'npm', 'test']);
|
|
91
|
+
}, /--always-cleanup-container option is only valid when isolation stack includes docker/);
|
|
92
|
+
assert.throws(() => {
|
|
93
|
+
parseArgs(['--keep-container', '--', 'npm', 'test']);
|
|
94
|
+
}, /--keep-container option is only valid when isolation stack includes docker/);
|
|
95
|
+
assert.throws(() => {
|
|
96
|
+
parseArgs(['--keep-container-on-fail', '--', 'npm', 'test']);
|
|
97
|
+
}, /--keep-container-on-fail option is only valid when isolation stack includes docker/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should work with keep-alive and auto-remove-docker-container', () => {
|
|
101
|
+
const result = parseArgs([
|
|
102
|
+
'-i',
|
|
103
|
+
'docker',
|
|
104
|
+
'--image',
|
|
105
|
+
'node:20',
|
|
106
|
+
'-k',
|
|
107
|
+
'--auto-remove-docker-container',
|
|
108
|
+
'--',
|
|
109
|
+
'npm',
|
|
110
|
+
'test',
|
|
111
|
+
]);
|
|
112
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'docker');
|
|
113
|
+
assert.strictEqual(result.wrapperOptions.image, 'node:20');
|
|
114
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
115
|
+
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should reject conflicting docker cleanup options', () => {
|
|
119
|
+
assert.throws(() => {
|
|
120
|
+
parseArgs([
|
|
121
|
+
'-i',
|
|
122
|
+
'docker',
|
|
123
|
+
'--keep-container',
|
|
124
|
+
'--always-cleanup-container',
|
|
125
|
+
'--',
|
|
126
|
+
'npm',
|
|
127
|
+
'test',
|
|
128
|
+
]);
|
|
129
|
+
}, /Cannot combine docker container cleanup policies/);
|
|
130
|
+
|
|
131
|
+
assert.throws(() => {
|
|
132
|
+
parseArgs([
|
|
133
|
+
'-i',
|
|
134
|
+
'docker',
|
|
135
|
+
'--keep-container',
|
|
136
|
+
'--keep-container-on-fail',
|
|
137
|
+
'--',
|
|
138
|
+
'npm',
|
|
139
|
+
'test',
|
|
140
|
+
]);
|
|
141
|
+
}, /Cannot combine docker container cleanup policies/);
|
|
142
|
+
});
|
|
143
|
+
});
|
package/test/args-parser.js
CHANGED
|
@@ -343,75 +343,6 @@ describe('parseArgs', () => {
|
|
|
343
343
|
});
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
-
describe('auto-remove-docker-container option', () => {
|
|
347
|
-
it('should parse --auto-remove-docker-container flag', () => {
|
|
348
|
-
const result = parseArgs([
|
|
349
|
-
'--isolated',
|
|
350
|
-
'docker',
|
|
351
|
-
'--image',
|
|
352
|
-
'alpine',
|
|
353
|
-
'--auto-remove-docker-container',
|
|
354
|
-
'--',
|
|
355
|
-
'npm',
|
|
356
|
-
'test',
|
|
357
|
-
]);
|
|
358
|
-
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('should default autoRemoveDockerContainer to false', () => {
|
|
362
|
-
const result = parseArgs([
|
|
363
|
-
'-i',
|
|
364
|
-
'docker',
|
|
365
|
-
'--image',
|
|
366
|
-
'alpine',
|
|
367
|
-
'--',
|
|
368
|
-
'npm',
|
|
369
|
-
'test',
|
|
370
|
-
]);
|
|
371
|
-
assert.strictEqual(
|
|
372
|
-
result.wrapperOptions.autoRemoveDockerContainer,
|
|
373
|
-
false
|
|
374
|
-
);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('should throw error for auto-remove-docker-container without docker isolation', () => {
|
|
378
|
-
assert.throws(() => {
|
|
379
|
-
parseArgs([
|
|
380
|
-
'-i',
|
|
381
|
-
'tmux',
|
|
382
|
-
'--auto-remove-docker-container',
|
|
383
|
-
'--',
|
|
384
|
-
'npm',
|
|
385
|
-
'test',
|
|
386
|
-
]);
|
|
387
|
-
}, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('should throw error for auto-remove-docker-container without isolation', () => {
|
|
391
|
-
assert.throws(() => {
|
|
392
|
-
parseArgs(['--auto-remove-docker-container', '--', 'npm', 'test']);
|
|
393
|
-
}, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should work with keep-alive and auto-remove-docker-container', () => {
|
|
397
|
-
const result = parseArgs([
|
|
398
|
-
'-i',
|
|
399
|
-
'docker',
|
|
400
|
-
'--image',
|
|
401
|
-
'node:20',
|
|
402
|
-
'-k',
|
|
403
|
-
'--auto-remove-docker-container',
|
|
404
|
-
'--',
|
|
405
|
-
'npm',
|
|
406
|
-
'test',
|
|
407
|
-
]);
|
|
408
|
-
assert.strictEqual(result.wrapperOptions.isolated, 'docker');
|
|
409
|
-
assert.strictEqual(result.wrapperOptions.image, 'node:20');
|
|
410
|
-
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
411
|
-
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
346
|
describe('command without separator', () => {
|
|
416
347
|
it('should parse command after options without separator', () => {
|
|
417
348
|
const result = parseArgs(['-i', 'tmux', '-d', 'npm', 'start']);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { buildNextLevelCommand } = require('../src/lib/command-builder');
|
|
6
|
+
|
|
7
|
+
describe('command-builder docker cleanup forwarding', () => {
|
|
8
|
+
it('forwards cleanup policy flags when a remaining level uses docker', () => {
|
|
9
|
+
const command = buildNextLevelCommand(
|
|
10
|
+
{
|
|
11
|
+
isolatedStack: ['tmux', 'docker'],
|
|
12
|
+
imageStack: [null, 'alpine'],
|
|
13
|
+
keepContainerOnFail: true,
|
|
14
|
+
},
|
|
15
|
+
'npm test'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
assert.match(command, /--isolated "docker"/);
|
|
19
|
+
assert.match(command, /--keep-container-on-fail/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('does not forward docker cleanup flags after the docker level is consumed', () => {
|
|
23
|
+
const command = buildNextLevelCommand(
|
|
24
|
+
{
|
|
25
|
+
isolatedStack: ['docker', 'tmux'],
|
|
26
|
+
imageStack: ['alpine', null],
|
|
27
|
+
keepContainer: true,
|
|
28
|
+
},
|
|
29
|
+
'npm test'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
assert.match(command, /--isolated "tmux"/);
|
|
33
|
+
assert.doesNotMatch(command, /--keep-container/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Tests for Docker
|
|
3
|
+
* Tests for Docker container cleanup behavior
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { describe, it } = require('node:test');
|
|
7
7
|
const assert = require('assert');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
8
11
|
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
9
12
|
const { runInDocker } = require('../src/lib/isolation');
|
|
10
13
|
const { execSync } = require('child_process');
|
|
@@ -26,9 +29,9 @@ async function waitFor(conditionFn, timeout = 5000, interval = 100) {
|
|
|
26
29
|
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
27
30
|
const DOCKER_TEST_TIMEOUT = 20000;
|
|
28
31
|
|
|
29
|
-
describe('Docker
|
|
30
|
-
// These tests verify
|
|
31
|
-
//
|
|
32
|
+
describe('Docker Container Cleanup Policy', () => {
|
|
33
|
+
// These tests verify that docker isolation removes finished containers by
|
|
34
|
+
// default while still providing explicit flags to keep them for investigation.
|
|
32
35
|
|
|
33
36
|
describe('auto-remove enabled', () => {
|
|
34
37
|
it(
|
|
@@ -55,8 +58,8 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
55
58
|
|
|
56
59
|
assert.strictEqual(result.success, true);
|
|
57
60
|
assert.ok(
|
|
58
|
-
result.message.includes('
|
|
59
|
-
'Message should indicate
|
|
61
|
+
result.message.includes('will be removed'),
|
|
62
|
+
'Message should indicate cleanup'
|
|
60
63
|
);
|
|
61
64
|
|
|
62
65
|
// Wait for container to finish and be removed
|
|
@@ -99,9 +102,9 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
99
102
|
);
|
|
100
103
|
});
|
|
101
104
|
|
|
102
|
-
describe('
|
|
105
|
+
describe('default cleanup', () => {
|
|
103
106
|
it(
|
|
104
|
-
'should
|
|
107
|
+
'should remove container filesystem by default',
|
|
105
108
|
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
106
109
|
async () => {
|
|
107
110
|
if (!canRunLinuxDockerImages()) {
|
|
@@ -111,9 +114,10 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
111
114
|
return;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
const containerName = `test-
|
|
117
|
+
const containerName = `test-default-cleanup-${Date.now()}`;
|
|
115
118
|
|
|
116
|
-
// Run command without
|
|
119
|
+
// Run command without any cleanup flag. The default should still remove
|
|
120
|
+
// the container after the command finishes.
|
|
117
121
|
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
118
122
|
image: 'alpine:latest',
|
|
119
123
|
session: containerName,
|
|
@@ -124,12 +128,76 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
124
128
|
|
|
125
129
|
assert.strictEqual(result.success, true);
|
|
126
130
|
assert.ok(
|
|
127
|
-
result.message.includes('
|
|
128
|
-
'Message should indicate
|
|
131
|
+
result.message.includes('will be removed'),
|
|
132
|
+
'Message should indicate default cleanup'
|
|
129
133
|
);
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
const containerRemoved = await waitFor(() => {
|
|
136
|
+
try {
|
|
137
|
+
execSync(`docker inspect -f '{{.State.Status}}' ${containerName}`, {
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
140
|
+
});
|
|
141
|
+
return false;
|
|
142
|
+
} catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}, 10000);
|
|
146
|
+
|
|
147
|
+
assert.ok(
|
|
148
|
+
containerRemoved,
|
|
149
|
+
'Container should be removed after exit by default'
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const allContainers = execSync('docker ps -a', {
|
|
154
|
+
encoding: 'utf8',
|
|
155
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
|
+
});
|
|
157
|
+
assert.ok(
|
|
158
|
+
!allContainers.includes(containerName),
|
|
159
|
+
'Container should NOT appear in docker ps -a after default cleanup'
|
|
160
|
+
);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
assert.fail(`Failed to verify container cleanup: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('keep-container opt-out', () => {
|
|
169
|
+
it(
|
|
170
|
+
'should preserve container filesystem when keepContainer is true',
|
|
171
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
172
|
+
async () => {
|
|
173
|
+
if (!canRunLinuxDockerImages()) {
|
|
174
|
+
console.log(
|
|
175
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const containerName = `test-keep-container-${Date.now()}`;
|
|
181
|
+
|
|
182
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
183
|
+
image: 'alpine:latest',
|
|
184
|
+
session: containerName,
|
|
185
|
+
detached: true,
|
|
186
|
+
keepAlive: false,
|
|
187
|
+
keepContainer: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
assert.strictEqual(result.success, true);
|
|
191
|
+
assert.ok(
|
|
192
|
+
result.message.includes('Container kept for investigation'),
|
|
193
|
+
'Message should explain that the container is kept'
|
|
194
|
+
);
|
|
195
|
+
assert.ok(
|
|
196
|
+
result.message.includes(`docker rm -f ${containerName}`),
|
|
197
|
+
'Message should include cleanup command'
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const containerExited = await waitFor(() => {
|
|
133
201
|
try {
|
|
134
202
|
const status = execSync(
|
|
135
203
|
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
@@ -144,26 +212,66 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
144
212
|
}
|
|
145
213
|
}, 10000);
|
|
146
214
|
|
|
147
|
-
|
|
215
|
+
assert.ok(containerExited, 'Container should remain in exited state');
|
|
216
|
+
|
|
148
217
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
218
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
219
|
+
} catch {
|
|
220
|
+
// Ignore cleanup errors
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
it(
|
|
226
|
+
'should preserve failed containers when keepContainerOnFail is true',
|
|
227
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
228
|
+
async () => {
|
|
229
|
+
if (!canRunLinuxDockerImages()) {
|
|
157
230
|
console.log(
|
|
158
|
-
'
|
|
159
|
-
);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
assert.fail(
|
|
162
|
-
`Failed to verify container preservation: ${err.message}`
|
|
231
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
163
232
|
);
|
|
233
|
+
return;
|
|
164
234
|
}
|
|
165
235
|
|
|
166
|
-
|
|
236
|
+
const containerName = `test-keep-on-fail-${Date.now()}`;
|
|
237
|
+
|
|
238
|
+
const result = await runInDocker('echo "test" && exit 7', {
|
|
239
|
+
image: 'alpine:latest',
|
|
240
|
+
session: containerName,
|
|
241
|
+
detached: true,
|
|
242
|
+
keepAlive: false,
|
|
243
|
+
keepContainerOnFail: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
assert.strictEqual(result.success, true);
|
|
247
|
+
assert.ok(
|
|
248
|
+
result.message.includes(
|
|
249
|
+
'Container will be kept if the command fails'
|
|
250
|
+
),
|
|
251
|
+
'Message should describe failure retention'
|
|
252
|
+
);
|
|
253
|
+
assert.ok(
|
|
254
|
+
result.message.includes(`docker rm -f ${containerName}`),
|
|
255
|
+
'Message should include cleanup command'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const containerExited = await waitFor(() => {
|
|
259
|
+
try {
|
|
260
|
+
const status = execSync(
|
|
261
|
+
`docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' ${containerName}`,
|
|
262
|
+
{
|
|
263
|
+
encoding: 'utf8',
|
|
264
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
265
|
+
}
|
|
266
|
+
).trim();
|
|
267
|
+
return status === 'exited 7';
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}, 10000);
|
|
272
|
+
|
|
273
|
+
assert.ok(containerExited, 'Failed container should be preserved');
|
|
274
|
+
|
|
167
275
|
try {
|
|
168
276
|
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
169
277
|
} catch {
|
|
@@ -172,4 +280,71 @@ describe('Docker Auto-Remove Container Feature', () => {
|
|
|
172
280
|
}
|
|
173
281
|
);
|
|
174
282
|
});
|
|
283
|
+
|
|
284
|
+
describe('attached mode logging', () => {
|
|
285
|
+
it(
|
|
286
|
+
'should write attached docker output to the provided host log path',
|
|
287
|
+
{ timeout: DOCKER_TEST_TIMEOUT },
|
|
288
|
+
async () => {
|
|
289
|
+
if (!canRunLinuxDockerImages()) {
|
|
290
|
+
console.log(
|
|
291
|
+
' Skipping: docker not available, daemon not running, or Linux containers not supported'
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const containerName = `test-attached-log-${Date.now()}`;
|
|
297
|
+
const logPath = path.join(
|
|
298
|
+
os.tmpdir(),
|
|
299
|
+
`start-attached-docker-${process.pid}-${Date.now()}.log`
|
|
300
|
+
);
|
|
301
|
+
fs.writeFileSync(logPath, '=== test log header ===\n');
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await runInDocker("printf 'attached-log-line\\n'", {
|
|
305
|
+
image: 'alpine:latest',
|
|
306
|
+
session: containerName,
|
|
307
|
+
detached: false,
|
|
308
|
+
keepAlive: false,
|
|
309
|
+
logPath,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
assert.strictEqual(result.success, true);
|
|
313
|
+
assert.ok(
|
|
314
|
+
result.message.includes('Container removed after completion'),
|
|
315
|
+
'Attached container should be removed after completion by default'
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const contents = fs.readFileSync(logPath, 'utf8');
|
|
319
|
+
assert.ok(
|
|
320
|
+
contents.includes('attached-log-line'),
|
|
321
|
+
`Attached docker output should be written to host log, got:\n${contents}`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const containerRemoved = await waitFor(() => {
|
|
325
|
+
try {
|
|
326
|
+
execSync(
|
|
327
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
328
|
+
{
|
|
329
|
+
encoding: 'utf8',
|
|
330
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
return false;
|
|
334
|
+
} catch {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}, 10000);
|
|
338
|
+
assert.ok(containerRemoved, 'Attached container should be removed');
|
|
339
|
+
} finally {
|
|
340
|
+
try {
|
|
341
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore cleanup errors
|
|
344
|
+
}
|
|
345
|
+
fs.rmSync(logPath, { force: true });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
});
|
|
175
350
|
});
|