start-command 0.35.4 → 0.37.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -0
  3. package/package.json +1 -1
  4. package/src/bin/cli.js +48 -7
  5. package/src/lib/args-parser-queries.js +14 -0
  6. package/src/lib/args-parser.js +32 -1
  7. package/src/lib/atomic-write.js +34 -0
  8. package/src/lib/attached-diagnostics.js +90 -0
  9. package/src/lib/cgroup-memory.js +43 -11
  10. package/src/lib/command-builder.js +43 -0
  11. package/src/lib/cpu-penalty-monitor.js +226 -0
  12. package/src/lib/cpu-penalty.js +175 -0
  13. package/src/lib/detached-finalize.js +16 -0
  14. package/src/lib/docker-cleanup.js +35 -7
  15. package/src/lib/docker-command-handoff.js +75 -0
  16. package/src/lib/docker-labels.js +33 -0
  17. package/src/lib/docker-resource-limits.js +28 -0
  18. package/src/lib/docker-resource-options.js +180 -0
  19. package/src/lib/docker-runtime-args.js +23 -0
  20. package/src/lib/docker-snapshot-safety.js +164 -0
  21. package/src/lib/execution-recovery.js +153 -30
  22. package/src/lib/execution-resume-all.js +11 -0
  23. package/src/lib/execution-resume.js +314 -54
  24. package/src/lib/execution-store.js +25 -121
  25. package/src/lib/exit-evidence.js +123 -0
  26. package/src/lib/exit-reason.js +24 -6
  27. package/src/lib/failure-handler.js +15 -5
  28. package/src/lib/isolation.js +48 -7
  29. package/src/lib/launch-owner.js +25 -0
  30. package/src/lib/launch-persistence.js +54 -0
  31. package/src/lib/log-sanitizer.js +267 -0
  32. package/src/lib/log-uploader.js +38 -19
  33. package/src/lib/query-commands.js +18 -6
  34. package/src/lib/resume-resources.js +88 -0
  35. package/src/lib/shell-utils.js +67 -17
  36. package/src/lib/status-formatter.js +28 -0
  37. package/src/lib/store-lock.js +133 -0
  38. package/src/lib/usage.js +16 -1
  39. package/test/ci-workflow-invariants.js +15 -14
  40. package/test/docker-command-handoff.js +107 -0
  41. package/test/docker-runtime-options.js +10 -0
  42. package/test/docker-snapshot-safety.js +106 -0
  43. package/test/execution-resume.js +19 -11
  44. package/test/exit-reason.js +6 -1
  45. package/test/failure-handler.js +11 -0
  46. package/test/isolation.js +5 -4
  47. package/test/javascript-first-ci.js +216 -0
  48. package/test/log-sanitizer.js +306 -0
  49. package/test/regression-174.js +3 -1
  50. package/test/regression-176.js +7 -0
  51. package/test/regression-180.js +17 -10
  52. package/test/regression-182.js +7 -3
  53. package/test/regression-185.js +4 -9
  54. package/test/regression-187.js +11 -2
  55. package/test/regression-189.js +224 -0
  56. package/test/regression-190.js +111 -0
  57. package/test/regression-191.js +160 -0
  58. package/test/regression-192.js +66 -0
  59. package/test/regression-193.js +251 -0
  60. package/test/regression-194.js +182 -0
  61. package/test/regression-198.js +281 -0
  62. package/test/regression-199.js +79 -0
  63. package/test/regression-201.js +25 -0
  64. package/test/regression-202.js +92 -0
  65. package/test/regression-91.js +23 -20
  66. package/test/shared-golden.js +54 -0
  67. package/test/status-query.js +97 -15
@@ -0,0 +1,224 @@
1
+ const { it, expect } = require('bun:test');
2
+ const { parseArgs } = require('../src/lib/args-parser');
3
+ it('CPU penalty is opt-in and accepts durations and percentage settings', () => {
4
+ const options = parseArgs([
5
+ '--isolated',
6
+ 'docker',
7
+ '--cpu-penalty',
8
+ '--cpu-penalty-trigger-window',
9
+ '1s',
10
+ '--',
11
+ 'true',
12
+ ]).wrapperOptions;
13
+ expect(options.cpuPenaltyConfig.triggerWindowMs).toBe(1000);
14
+ expect(options.cpuPenaltyConfig.release).toBe(65);
15
+ });
16
+ const { initialState, evaluate, average } = require('../src/lib/cpu-penalty');
17
+ const config = {
18
+ cpus: 2,
19
+ trigger: 95,
20
+ triggerWindowMs: 100,
21
+ release: 65,
22
+ releaseWindowMs: 100,
23
+ };
24
+ it('requires complete time-weighted windows, then lifts and observes afresh', () => {
25
+ let state = initialState(null, 0);
26
+ const tick = (now, cores) => {
27
+ const result = evaluate(
28
+ state,
29
+ { now, cores, daemonCpus: 6, maxGapMs: 50 },
30
+ config
31
+ );
32
+ state = result.state;
33
+ return result.action;
34
+ };
35
+ expect(tick(0, 6)).toBeNull();
36
+ expect(tick(40, 6)).toBeNull();
37
+ expect(tick(80, 6)).toBeNull();
38
+ expect(tick(100, 0.4).kind).toBe('apply');
39
+ expect(state.penaltyCount).toBe(1);
40
+ expect(tick(120, 0.4)).toBeNull();
41
+ expect(tick(160, 0.4)).toBeNull();
42
+ expect(tick(200, 0.4)).toBeNull();
43
+ expect(tick(220, 6).kind).toBe('lift');
44
+ expect(state.penalizedMs).toBe(120);
45
+ expect(tick(240, 6)).toBeNull();
46
+ expect(tick(280, 6)).toBeNull();
47
+ expect(tick(320, 6)).toBeNull();
48
+ expect(tick(340, 6).kind).toBe('apply');
49
+ expect(state.penaltyCount).toBe(2);
50
+ });
51
+ it('resets coverage on outage, gaps, resize and restart, and skips ineffective caps', () => {
52
+ let state = initialState(4, 0);
53
+ for (const [now, cores, daemonCpus] of [
54
+ [0, 4, 6],
55
+ [40, 4, 6],
56
+ [80, NaN, 6],
57
+ [120, 4, 6],
58
+ [160, 4, 6],
59
+ [200, 4, 8],
60
+ ]) {
61
+ const result = evaluate(
62
+ state,
63
+ { now, cores, daemonCpus, maxGapMs: 50 },
64
+ config
65
+ );
66
+ state = result.state;
67
+ expect(result.action).toBeNull();
68
+ }
69
+ const restarted = initialState(4, 240, state);
70
+ expect(
71
+ evaluate(
72
+ restarted,
73
+ { now: 240, cores: 4, daemonCpus: 8, maxGapMs: 50 },
74
+ config
75
+ ).action
76
+ ).toBeNull();
77
+ expect(
78
+ evaluate(state, { now: 400, cores: 4, daemonCpus: 8, maxGapMs: 50 }, config)
79
+ .action
80
+ ).toBeNull();
81
+ state = initialState(1, 0);
82
+ for (const now of [0, 40, 80, 120]) {
83
+ const r = evaluate(
84
+ state,
85
+ { now, cores: 1, daemonCpus: 6, maxGapMs: 50 },
86
+ config
87
+ );
88
+ state = r.state;
89
+ expect(r.action).toBeNull();
90
+ }
91
+ });
92
+ it('weights timestamps rather than giving short intervals equal weight', () => {
93
+ expect(
94
+ average(
95
+ [
96
+ { at: 0, cores: 6 },
97
+ { at: 90, cores: 0 },
98
+ { at: 100, cores: 0 },
99
+ ],
100
+ 100,
101
+ 100
102
+ )
103
+ ).toBe(5.4);
104
+ });
105
+ it('resume restores the original base within resized daemon capacity', () => {
106
+ const { prepareResumeResources } = require('../src/lib/resume-resources');
107
+ const record = {
108
+ options: {
109
+ isolated: 'docker',
110
+ sessionName: 'task',
111
+ cpuPenaltyConfig: config,
112
+ baseResourceLimits: ['--cpus=3'],
113
+ resourceLimits: ['--cpus=0.5'],
114
+ resolvedLimits: { cpus: 3 },
115
+ },
116
+ };
117
+ const result = prepareResumeResources(
118
+ record,
119
+ { state: 'stopped' },
120
+ {},
121
+ (_cmd, args) => ({
122
+ success: true,
123
+ stdout: JSON.stringify(
124
+ args[0] === 'info'
125
+ ? { MemTotal: 1073741824, NCPU: 2 }
126
+ : { NanoCpus: 500000000 }
127
+ ),
128
+ })
129
+ );
130
+ expect(result.resourceLimits).toContain('--cpus=2');
131
+ expect(result.resolvedLimits.cpus).toBe(2);
132
+ expect(record.options.baseResourceLimits).toEqual(['--cpus=3']);
133
+ });
134
+
135
+ it('monitor applies, lifts and reapplies with tracking disabled', () => {
136
+ if (process.platform === 'win32') {
137
+ return;
138
+ }
139
+ const fs = require('fs'),
140
+ path = require('path'),
141
+ os = require('os');
142
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-monitor-189-'));
143
+ try {
144
+ const logPath = path.join(dir, 'task.log');
145
+ fs.writeFileSync(logPath, '');
146
+ const record = {
147
+ logPath,
148
+ options: {
149
+ sessionName: 'cpu-task',
150
+ cpuPenaltyConfig: {
151
+ ...config,
152
+ triggerWindowMs: 200,
153
+ releaseWindowMs: 200,
154
+ },
155
+ baseResourceLimits: [],
156
+ },
157
+ };
158
+ const result = require('child_process').spawnSync(
159
+ '/bin/sh',
160
+ [
161
+ path.resolve(__dirname, '../../experiments/issue-195-cpu-monitor.sh'),
162
+ process.execPath,
163
+ require.resolve('../src/lib/cpu-penalty-monitor'),
164
+ '',
165
+ 'cpu-task',
166
+ '1',
167
+ JSON.stringify(record),
168
+ ],
169
+ { encoding: 'utf8' }
170
+ );
171
+ expect(result.status).toBe(0);
172
+ const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n');
173
+ expect(lines.length).toBe(3);
174
+ expect(lines[0]).toContain('CPU penalty applied: 2');
175
+ expect(lines[1]).toContain('CPU penalty lifted: 6');
176
+ expect(lines[2]).toContain('CPU penalty applied: 2');
177
+ const state = JSON.parse(
178
+ fs.readFileSync(`${logPath}.cpu-penalty.json`, 'utf8')
179
+ );
180
+ expect(state.penaltyCount).toBe(2);
181
+ expect(state.phase).toBe('penalized');
182
+ expect(state.penalizedMs).toBeGreaterThan(200);
183
+ } finally {
184
+ fs.rmSync(dir, { recursive: true, force: true });
185
+ }
186
+ }, 15000);
187
+
188
+ it('preserves legacy CPU quota format and the lower original base', () => {
189
+ const limits = require('../src/lib/docker-resource-limits');
190
+ const base = ['--cpu-period=200000', '--cpu-quota=100000'];
191
+ expect(limits.cpuCount(base)).toBe(0.5);
192
+ expect(limits.withCpuLimit(base, 0.25)).toEqual([
193
+ '--cpu-period=100000',
194
+ '--cpu-quota=25000',
195
+ ]);
196
+ expect(limits.cpuUpdateArgs(base, 0.25)).toEqual([
197
+ '--cpu-period=100000',
198
+ '--cpu-quota=25000',
199
+ ]);
200
+ const { clampCpuToDaemon } = require('../src/lib/resume-resources');
201
+ expect(
202
+ clampCpuToDaemon(base, () => ({
203
+ success: true,
204
+ stdout: '{"NCPU":4,"MemTotal":1073741824}',
205
+ }))
206
+ ).toEqual(['--cpu-period=100000', '--cpu-quota=50000']);
207
+ const { prepareResumeResources } = require('../src/lib/resume-resources');
208
+ const overridden = prepareResumeResources(
209
+ {
210
+ options: {
211
+ isolated: 'docker',
212
+ sessionName: 'task',
213
+ resourceLimits: base,
214
+ },
215
+ },
216
+ { state: 'stopped' },
217
+ { cpus: '0.25' },
218
+ () => ({ success: true, stdout: '{"CpuPeriod":200000,"CpuQuota":100000}' })
219
+ );
220
+ expect(overridden.resourceLimits).toEqual([
221
+ '--cpu-period=100000',
222
+ '--cpu-quota=25000',
223
+ ]);
224
+ });
@@ -0,0 +1,111 @@
1
+ const { describe, it, expect } = require('bun:test');
2
+ const { parseArgs } = require('../src/lib/args-parser');
3
+
4
+ describe('issue #190: launch resource options', () => {
5
+ it('recognizes resource flags and rejects invalid input before launch', () => {
6
+ const parsed = parseArgs([
7
+ '--isolated',
8
+ 'docker',
9
+ '--image',
10
+ 'alpine',
11
+ '--memory',
12
+ '90%-100%',
13
+ '--cpus=50%',
14
+ '--',
15
+ 'true',
16
+ ]);
17
+ expect(parsed.wrapperOptions.memory).toBe('90%-100%');
18
+ expect(parsed.wrapperOptions.cpus).toBe('50%');
19
+ for (const value of ['100%-90%', '101%', '0%', '0g', '3watts']) {
20
+ expect(() =>
21
+ parseArgs([
22
+ '--isolated',
23
+ 'docker',
24
+ '--image',
25
+ 'alpine',
26
+ '--memory',
27
+ value,
28
+ '--',
29
+ 'true',
30
+ ])
31
+ ).toThrow();
32
+ }
33
+ });
34
+ });
35
+
36
+ const {
37
+ resolveResourceOptions,
38
+ } = require('../src/lib/docker-resource-options');
39
+ it('uses daemon capacity, draws each range once and disables extra swap by default', () => {
40
+ let draws = 0;
41
+ const calls = [];
42
+ const result = resolveResourceOptions(
43
+ { memory: '70%-80%', cpus: '50%' },
44
+ (_cmd, args) => {
45
+ calls.push(args);
46
+ return {
47
+ success: true,
48
+ stdout: JSON.stringify({ MemTotal: 1024 ** 3 * 10, NCPU: 8 }),
49
+ };
50
+ },
51
+ () => {
52
+ draws++;
53
+ return 0.5;
54
+ }
55
+ );
56
+ expect(result.resourceLimits).toEqual([
57
+ '--memory=8053063680',
58
+ '--cpus=4',
59
+ '--memory-swap=8053063680',
60
+ ]);
61
+ expect(result.resolvedLimits.memorySwap).toBe(result.resolvedLimits.memory);
62
+ expect(calls).toHaveLength(1);
63
+ expect(draws).toBe(2);
64
+ });
65
+ it('preserves stored resolutions without another random draw or daemon query', () => {
66
+ const limits = [
67
+ '--memory=8053063680',
68
+ '--memory-swap=8053063680',
69
+ '--cpus=4',
70
+ ];
71
+ const result = resolveResourceOptions(
72
+ { resourceLimits: limits },
73
+ () => {
74
+ throw Error('unexpected query');
75
+ },
76
+ () => {
77
+ throw Error('unexpected draw');
78
+ }
79
+ );
80
+ expect(result.resourceLimits).toEqual(limits);
81
+ });
82
+ it('rejects swap below memory and daemon capacity failures', () => {
83
+ expect(() =>
84
+ resolveResourceOptions({ memory: '64m', memorySwap: '32m' })
85
+ ).toThrow();
86
+ expect(() =>
87
+ resolveResourceOptions({ memory: '80%' }, () => ({ success: false }))
88
+ ).toThrow();
89
+ });
90
+
91
+ it('forwards raw resource and penalty options through every remaining Docker level', () => {
92
+ const { buildNextLevelCommand } = require('../src/lib/command-builder');
93
+ const options = parseArgs([
94
+ '--isolated',
95
+ 'screen docker',
96
+ '--memory',
97
+ '70%-80%',
98
+ '--cpus',
99
+ '50%',
100
+ '--cpu-penalty',
101
+ '--cpu-penalty-trigger-window',
102
+ '2s',
103
+ '--',
104
+ 'work',
105
+ ]).wrapperOptions;
106
+ const command = buildNextLevelCommand(options, 'work');
107
+ const words = require('../src/lib/shell-utils').splitShellWords(command);
108
+ expect(words[words.indexOf('--memory') + 1]).toBe('70%-80%');
109
+ expect(words[words.indexOf('--cpus') + 1]).toBe('50%');
110
+ expect(command).toContain('--cpu-penalty-trigger-window 2000ms');
111
+ });
@@ -0,0 +1,160 @@
1
+ const { it, expect } = require('bun:test');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const {
6
+ ExecutionStore,
7
+ ExecutionRecord,
8
+ } = require('../src/lib/execution-store');
9
+ const { resumeExecution } = require('../src/lib/execution-resume');
10
+ const { recoverKilledExecution } = require('../src/lib/execution-recovery');
11
+ const { SessionState } = require('../src/lib/session-probe');
12
+
13
+ function fixture() {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-191-'));
15
+ const store = new ExecutionStore({ appFolder: dir, useLinks: false });
16
+ const record = new ExecutionRecord({
17
+ command: 'work',
18
+ status: 'executed',
19
+ logPath: path.join(dir, 'run.log'),
20
+ options: {
21
+ isolated: 'docker',
22
+ isolationMode: 'detached',
23
+ sessionName: 'task',
24
+ image: 'alpine',
25
+ onKillResume: 3,
26
+ onKillResumeMemory: '70%-80%',
27
+ },
28
+ });
29
+ store.save(record);
30
+ const calls = [];
31
+ const runner = (_, args) => {
32
+ calls.push(args);
33
+ if (args[0] === 'info') {
34
+ return {
35
+ success: true,
36
+ stdout: JSON.stringify({
37
+ MemTotal: 1073741824,
38
+ NCPU: 4,
39
+ DockerRootDir: os.tmpdir(),
40
+ }),
41
+ };
42
+ }
43
+ if (args.includes('--size')) {
44
+ return { success: true, stdout: '1024' };
45
+ }
46
+ if (args[0] === 'inspect') {
47
+ return {
48
+ success: true,
49
+ stdout: JSON.stringify({
50
+ Memory: 67108864,
51
+ MemorySwap: 67108864,
52
+ NanoCpus: 3000000000,
53
+ }),
54
+ };
55
+ }
56
+ return { success: true, stdout: 'id' };
57
+ };
58
+ return { dir, store, record, runner, calls };
59
+ }
60
+ it('manual in-place memory override updates before start and records the resolution', async () => {
61
+ const f = fixture();
62
+ try {
63
+ const result = await resumeExecution(f.store, f.record.uuid, {
64
+ resourceOptions: { memory: '128m' },
65
+ runner: f.runner,
66
+ probe: () => ({ alive: false, state: SessionState.STOPPED }),
67
+ startWatcher: () => true,
68
+ });
69
+ expect(result.success).toBe(true);
70
+ const update = f.calls.find((a) => a[0] === 'update');
71
+ expect(update).toContain('--memory=134217728');
72
+ expect(update).toContain('--memory-swap=134217728');
73
+ expect(f.calls.indexOf(update)).toBeLessThan(
74
+ f.calls.findIndex((a) => a[0] === 'start')
75
+ );
76
+ expect(f.store.get(f.record.uuid).options.resolvedLimits.memory).toBe(
77
+ 134217728
78
+ );
79
+ } finally {
80
+ fs.rmSync(f.dir, { recursive: true, force: true });
81
+ }
82
+ });
83
+ it('manual snapshot override applies limits to the new container at creation', async () => {
84
+ const f = fixture();
85
+ try {
86
+ const result = await resumeExecution(f.store, f.record.uuid, {
87
+ command: 'new',
88
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
89
+ resourceOptions: { memory: '128m' },
90
+ runner: f.runner,
91
+ probe: () => ({ alive: false, state: SessionState.STOPPED }),
92
+ startWatcher: () => true,
93
+ });
94
+ expect(result.success).toBe(true);
95
+ expect(f.calls.find((a) => a[0] === 'run')).toContain('--memory=134217728');
96
+ } finally {
97
+ fs.rmSync(f.dir, { recursive: true, force: true });
98
+ }
99
+ });
100
+ for (const [exitCode, oomKilled, sample, change] of [
101
+ [137, true, '67108864 66000000 1 1 100 100', true],
102
+ [137, true, '67108864 66000000 1 1 0 100', false],
103
+ [137, false, '67108864 66000000 0 0 0 100', false],
104
+ [1, true, '67108864 66000000 1 1 100 100', false],
105
+ ]) {
106
+ it(`automatic memory change requires a qualifying OOM (exit=${exitCode}, fresh=${change})`, () => {
107
+ const f = fixture();
108
+ try {
109
+ const result = recoverKilledExecution({
110
+ store: f.store,
111
+ executionId: f.record.uuid,
112
+ exitCode,
113
+ oomKilled,
114
+ cgroupMemory: sample,
115
+ finishedAt: '1970-01-01T00:01:40Z',
116
+ runner: f.runner,
117
+ random: () => 0.5,
118
+ startWatcher: () => true,
119
+ });
120
+ const update = f.calls.find((a) => a[0] === 'update');
121
+ expect(Boolean(update)).toBe(change);
122
+ if (change) {
123
+ expect(result.recovered).toBe(true);
124
+ expect(update).toContain('--memory=805306368');
125
+ expect(f.calls.indexOf(update)).toBeLessThan(
126
+ f.calls.findIndex((a) => a[0] === 'start')
127
+ );
128
+ expect(f.store.get(f.record.uuid).options.resolvedLimits.memory).toBe(
129
+ 805306368
130
+ );
131
+ }
132
+ if (exitCode === 1) {
133
+ expect(result.recovered).toBe(false);
134
+ }
135
+ } finally {
136
+ fs.rmSync(f.dir, { recursive: true, force: true });
137
+ }
138
+ });
139
+ }
140
+
141
+ it('a failed memory update prevents starting the old-limit command', async () => {
142
+ const f = fixture();
143
+ try {
144
+ const result = await resumeExecution(f.store, f.record.uuid, {
145
+ resourceOptions: { memory: '128m' },
146
+ runner: (cmd, args) =>
147
+ args[0] === 'update'
148
+ ? (f.calls.push(args),
149
+ { success: false, stderr: 'daemon rejected update' })
150
+ : f.runner(cmd, args),
151
+ probe: () => ({ alive: false, state: SessionState.STOPPED }),
152
+ startWatcher: () => true,
153
+ });
154
+ expect(result.success).toBe(false);
155
+ expect(f.calls.some((args) => args[0] === 'start')).toBe(false);
156
+ expect(f.store.get(f.record.uuid).status).toBe('executed');
157
+ } finally {
158
+ fs.rmSync(f.dir, { recursive: true, force: true });
159
+ }
160
+ });
@@ -0,0 +1,66 @@
1
+ const { it, expect } = require('bun:test');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const {
7
+ buildCgroupSamplerStartSnippet,
8
+ buildCgroupMemoryLogSnippet,
9
+ } = require('../src/lib/cgroup-memory');
10
+ it('samples remote/private cgroups through Docker and reports unavailable counters', () => {
11
+ expect(buildCgroupSamplerStartSnippet('remote-task')).toContain(
12
+ 'docker exec'
13
+ );
14
+ expect(buildCgroupMemoryLogSnippet("'/tmp/log'")).toContain('unavailable');
15
+ expect(buildCgroupSamplerStartSnippet('remote-task')).toContain(
16
+ 'HostConfig.Memory'
17
+ );
18
+ });
19
+
20
+ for (const mode of ['remote', 'no-shell', 'outage', 'shared']) {
21
+ it(`real watcher shell: ${mode}`, () => {
22
+ if (process.platform === 'win32') {
23
+ return;
24
+ }
25
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'remote-192-'));
26
+ try {
27
+ const script = path.join(dir, 'sample.sh');
28
+ fs.writeFileSync(
29
+ script,
30
+ [
31
+ buildCgroupSamplerStartSnippet('remote-task'),
32
+ 'sleep 1.2',
33
+ require('../src/lib/cgroup-memory').buildCgroupSamplerStopSnippet(),
34
+ buildCgroupMemoryLogSnippet('"$TMPDIR/memory.log"'),
35
+ 'cat "$TMPDIR/memory.log"',
36
+ ].join('; ')
37
+ );
38
+ const result = spawnSync(
39
+ '/bin/sh',
40
+ [
41
+ path.resolve(
42
+ __dirname,
43
+ '../../experiments/issue-195-memory-sampler.sh'
44
+ ),
45
+ mode,
46
+ script,
47
+ ],
48
+ { encoding: 'utf8' }
49
+ );
50
+ expect(result.status).toBe(0);
51
+ if (mode === 'remote' || mode === 'outage') {
52
+ expect(result.stdout).toContain(
53
+ 'memory.max=67108864 memory.peak=33554432 oom=1 oom_kill=3'
54
+ );
55
+ } else {
56
+ expect(result.stdout).toContain('Memory: unavailable');
57
+ expect(result.stdout).toContain('memory.limit=67108864 (HostConfig)');
58
+ expect(result.stdout).toContain(
59
+ mode === 'no-shell' ? 'sh: not found' : 'namespace=host'
60
+ );
61
+ }
62
+ } finally {
63
+ fs.rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ }, 10000);
66
+ }