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,281 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { resumeExecution } = require('../src/lib/execution-resume');
8
+ const { recoverKilledExecution } = require('../src/lib/execution-recovery');
9
+ const { SessionState } = require('../src/lib/session-probe');
10
+ const {
11
+ buildDetachedDockerCompletionScript,
12
+ DOCKER_CONTAINER_CLEANUP_POLICY,
13
+ } = require('../src/lib/docker-cleanup');
14
+
15
+ function fixture(options = {}) {
16
+ let current = {
17
+ uuid: 'issue-198-uuid',
18
+ command: 'original',
19
+ status: 'executed',
20
+ exitCode: 137,
21
+ options: {
22
+ isolated: 'docker',
23
+ isolationMode: 'detached',
24
+ sessionName: 'box',
25
+ containerId: 'same-id',
26
+ ...options,
27
+ },
28
+ };
29
+ const store = {
30
+ get: () => globalThis.structuredClone(current),
31
+ save: (r) => {
32
+ current = globalThis.structuredClone(r);
33
+ },
34
+ };
35
+ return { store, current: () => current };
36
+ }
37
+
38
+ function mockDocker(calls, scripts = []) {
39
+ return (_bin, args) => {
40
+ calls.push(args);
41
+ if (args[0] === 'cp') {
42
+ scripts.push(fs.readFileSync(args[1], 'utf8'));
43
+ }
44
+ return {
45
+ success: true,
46
+ stdout:
47
+ args[0] === 'info'
48
+ ? JSON.stringify({ DockerRootDir: '/docker' })
49
+ : args.includes('--size')
50
+ ? '1024'
51
+ : args.includes('{{.State.Running}}')
52
+ ? 'true'
53
+ : args.includes('{{json .HostConfig}}')
54
+ ? '{}'
55
+ : args[0] === 'run'
56
+ ? 'new-container-id'
57
+ : '',
58
+ status: 0,
59
+ };
60
+ };
61
+ }
62
+
63
+ const stopped = () => ({ state: SessionState.STOPPED, alive: false });
64
+
65
+ test('replacement command resumes the same container without a snapshot or changed ID', async () => {
66
+ const { store, current } = fixture({ commandHandoff: true });
67
+ const calls = [],
68
+ scripts = [];
69
+ const result = await resumeExecution(store, 'box', {
70
+ command: "printf '%s' 'new $command'",
71
+ probe: stopped,
72
+ runner: mockDocker(calls, scripts),
73
+ startWatcher: () => true,
74
+ });
75
+ assert.equal(result.success, true, result.error);
76
+ assert.ok(calls.some((args) => args[0] === 'cp'));
77
+ assert.ok(calls.some((args) => args[0] === 'start'));
78
+ assert.ok(
79
+ !calls.some((args) => ['commit', 'run', 'create'].includes(args[0]))
80
+ );
81
+ assert.equal(current().options.containerId, 'same-id');
82
+ assert.equal(current().options.sessionName, 'box');
83
+ assert.ok(scripts[0].includes('new $command'));
84
+ });
85
+
86
+ test('automatic recovery replaces a prior explicit handoff and exports its attempt', () => {
87
+ const { store } = fixture({
88
+ commandHandoff: true,
89
+ onKillResume: 1,
90
+ recoveryCommand: 'printf recovered',
91
+ });
92
+ const scripts = [];
93
+ const result = recoverKilledExecution({
94
+ store,
95
+ executionId: 'issue-198-uuid',
96
+ exitCode: 137,
97
+ oomKilled: false,
98
+ runner: mockDocker([], scripts),
99
+ startWatcher: () => true,
100
+ });
101
+ assert.equal(result.recovered, true, result.reason);
102
+ assert.ok(scripts[0].includes('START_COMMAND_RECOVERY_ATTEMPT=1'));
103
+ assert.ok(scripts[0].includes('printf recovered'));
104
+ });
105
+
106
+ test('legacy snapshots preflight before commit and remove originals only with non-forced rm after start', async () => {
107
+ const { store, current } = fixture();
108
+ const calls = [];
109
+ const result = await resumeExecution(store, 'box', {
110
+ command: 'new',
111
+ probe: stopped,
112
+ removeOriginal: true,
113
+ runner: mockDocker(calls),
114
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
115
+ startWatcher: () => true,
116
+ });
117
+ assert.equal(result.success, true, result.error);
118
+ assert.ok(
119
+ calls.findIndex((a) => a.includes('--size')) <
120
+ calls.findIndex((a) => a[0] === 'commit')
121
+ );
122
+ assert.ok(
123
+ calls.findIndex((a) => a[0] === 'run') <
124
+ calls.findIndex((a) => a[0] === 'rm')
125
+ );
126
+ assert.deepEqual(
127
+ calls.find((a) => a[0] === 'rm'),
128
+ ['rm', 'box']
129
+ );
130
+ assert.equal(current().options.commandHandoff, true);
131
+ assert.equal(current().options.snapshotImage, 'start-command-resume/box:1');
132
+ assert.match(result.output, /snapshotting/);
133
+ });
134
+
135
+ test('disk shortage rolls back the reservation without committing or removing the original', async () => {
136
+ const { store, current } = fixture();
137
+ const calls = [];
138
+ const result = await resumeExecution(store, 'box', {
139
+ command: 'new',
140
+ probe: stopped,
141
+ removeOriginal: true,
142
+ runner: mockDocker(calls),
143
+ snapshotOptions: { freeBytes: () => 0 },
144
+ startWatcher: () => true,
145
+ });
146
+ assert.equal(result.success, false);
147
+ assert.match(result.error, /Insufficient disk/);
148
+ assert.ok(!calls.some((a) => ['commit', 'rm', 'rmi'].includes(a[0])));
149
+ assert.equal(current().options.sessionName, 'box');
150
+ assert.equal(current().options.launchPending, undefined);
151
+ });
152
+
153
+ test('snapshot failure removes a completed image and restores the original execution', async () => {
154
+ const { store, current } = fixture();
155
+ const calls = [];
156
+ const base = mockDocker(calls);
157
+ const result = await resumeExecution(store, 'box', {
158
+ command: 'new',
159
+ probe: stopped,
160
+ runner: (bin, args) =>
161
+ args[0] === 'run'
162
+ ? { success: false, stderr: 'launch failed' }
163
+ : base(bin, args),
164
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
165
+ startWatcher: () => true,
166
+ });
167
+ assert.equal(result.success, false);
168
+ assert.ok(
169
+ calls.some((a) => a[0] === 'rmi' && a[1] === 'start-command-resume/box:1')
170
+ );
171
+ assert.equal(current().options.sessionName, 'box');
172
+ });
173
+
174
+ test('watcher removes its generated image after the cleanup policy removes its container', () => {
175
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cleanup-198-'));
176
+ const watcher = path.join(dir, 'watcher.sh');
177
+ const events = path.join(dir, 'events');
178
+ try {
179
+ fs.writeFileSync(
180
+ watcher,
181
+ `#!/bin/sh
182
+ docker() {
183
+ case "$1" in
184
+ wait) echo 0 ;;
185
+ inspect) case "$3" in
186
+ *State.Running*) echo false ;;
187
+ *State.ExitCode*) echo '0 false 2026-10-10T00:00:00Z 2026-10-10T00:00:01Z' ;;
188
+ *snapshot-image*) echo 'start-command-resume/box:1' ;;
189
+ *) echo '' ;;
190
+ esac ;;
191
+ rm|rmi) printf '%s\\n' "$*" >> events ;;
192
+ esac
193
+ }
194
+ ${buildDetachedDockerCompletionScript(
195
+ 'box-resume-1',
196
+ DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS,
197
+ null
198
+ )}
199
+ `
200
+ );
201
+ // Keep POSIX quoting inside a file, out of Windows process argv/PATH.
202
+ const result = spawnSync('sh', ['watcher.sh'], {
203
+ cwd: dir,
204
+ encoding: 'utf8',
205
+ });
206
+ assert.equal(result.status, 0, result.stderr);
207
+ assert.deepEqual(fs.readFileSync(events, 'utf8').trim().split('\n'), [
208
+ 'rm -f box-resume-1',
209
+ 'rmi start-command-resume/box:1',
210
+ ]);
211
+ } finally {
212
+ fs.rmSync(dir, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ test('changed caller labels use a guarded snapshot and reapply merged labels', async () => {
217
+ const { store, current } = fixture({
218
+ commandHandoff: true,
219
+ labels: ['role=old', 'team=runtime'],
220
+ });
221
+ const calls = [];
222
+ const result = await resumeExecution(store, 'box', {
223
+ probe: stopped,
224
+ resourceOptions: { labels: ['role=new'] },
225
+ runner: mockDocker(calls),
226
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
227
+ startWatcher: () => true,
228
+ });
229
+ assert.equal(result.success, true, result.error);
230
+ const run = calls.find((args) => args[0] === 'run');
231
+ assert.ok(run.includes('role=new'));
232
+ assert.ok(run.includes('team=runtime'));
233
+ assert.ok(!run.includes('role=old'));
234
+ assert.ok(calls.some((args) => args[0] === 'commit'));
235
+ assert.deepEqual(current().options.labels, ['role=new', 'team=runtime']);
236
+ assert.equal(current().options.commandHandoff, true);
237
+ });
238
+
239
+ test('original retained by default and when a successor has already stopped', async () => {
240
+ for (const removeOriginal of [false, true]) {
241
+ const { store } = fixture();
242
+ const calls = [];
243
+ const base = mockDocker(calls);
244
+ const result = await resumeExecution(store, 'box', {
245
+ command: 'new',
246
+ probe: stopped,
247
+ removeOriginal,
248
+ runner: (bin, args) =>
249
+ args.includes('{{.State.Running}}')
250
+ ? { success: true, stdout: 'false' }
251
+ : base(bin, args),
252
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
253
+ startWatcher: () => true,
254
+ });
255
+ assert.equal(result.success, true, result.error);
256
+ assert.ok(!calls.some((args) => args[0] === 'rm'));
257
+ }
258
+ });
259
+
260
+ test('repeated snapshot successors retain the original root-session attribution', async () => {
261
+ const { store, current } = fixture();
262
+ for (const role of ['first', 'second']) {
263
+ const calls = [];
264
+ const result = await resumeExecution(store, 'box', {
265
+ command: 'new',
266
+ probe: stopped,
267
+ resourceOptions: { labels: [`role=${role}`] },
268
+ runner: mockDocker(calls),
269
+ snapshotOptions: { freeBytes: () => 100 * 1024 ** 3 },
270
+ startWatcher: () => true,
271
+ });
272
+ assert.equal(result.success, true, result.error);
273
+ assert.ok(
274
+ calls
275
+ .find((args) => args[0] === 'run')
276
+ .includes('start-command.root-session=box')
277
+ );
278
+ assert.equal(current().options.rootSession, 'box');
279
+ }
280
+ assert.equal(current().options.resumeCount, 2);
281
+ });
@@ -0,0 +1,79 @@
1
+ const { describe, it, expect } = require('bun:test');
2
+ const { parseArgs } = require('../src/lib/args-parser');
3
+ const {
4
+ buildDockerRuntimeArgs,
5
+ buildDockerRuntimeMetadata,
6
+ } = require('../src/lib/docker-runtime-args');
7
+ const { buildNextLevelCommand } = require('../src/lib/command-builder');
8
+
9
+ describe('Docker attribution labels (#199)', () => {
10
+ it('attributes replacement containers to their stable root and execution', () => {
11
+ const { dockerLabels } = require('../src/lib/docker-labels');
12
+ expect(
13
+ dockerLabels({ session: 'demo-resume-1', sessionId: 'uuid' })
14
+ ).toContain('start-command.root-session=demo-resume-1');
15
+ expect(
16
+ dockerLabels({
17
+ session: 'demo-resume-1-resume-2',
18
+ sessionId: 'uuid',
19
+ resumeCount: 2,
20
+ })
21
+ ).toEqual([
22
+ 'start-command.session=demo-resume-1-resume-2',
23
+ 'start-command.root-session=demo',
24
+ 'start-command.resume-count=2',
25
+ 'start-command.uuid=uuid',
26
+ ]);
27
+ });
28
+ it('parses repeated labels and preserves equals signs and empty values', () => {
29
+ const { wrapperOptions } = parseArgs([
30
+ '-i',
31
+ 'docker',
32
+ '--label',
33
+ 'hive-mind.tool=codex',
34
+ '--label=task.url=https://github.com/o/r/issues/1?q=a=b',
35
+ '--label',
36
+ 'empty=',
37
+ '--',
38
+ 'echo',
39
+ ]);
40
+ expect(wrapperOptions.labels).toEqual([
41
+ 'hive-mind.tool=codex',
42
+ 'task.url=https://github.com/o/r/issues/1?q=a=b',
43
+ 'empty=',
44
+ ]);
45
+ const args = buildDockerRuntimeArgs(wrapperOptions);
46
+ for (const label of wrapperOptions.labels) {
47
+ expect(args).toContain(label);
48
+ }
49
+ expect(buildDockerRuntimeMetadata(wrapperOptions).labels).toEqual(
50
+ wrapperOptions.labels
51
+ );
52
+ });
53
+ it('rejects malformed, reserved and non-Docker labels', () => {
54
+ for (const label of [
55
+ 'missing-value',
56
+ '=empty-key',
57
+ 'start-command.uuid=spoof',
58
+ ]) {
59
+ expect(() =>
60
+ parseArgs(['-i', 'docker', '--label', label, '--', 'true'])
61
+ ).toThrow();
62
+ }
63
+ expect(() => parseArgs(['--label', 'task=one', '--', 'true'])).toThrow();
64
+ });
65
+ it('forwards labels safely through nested Docker isolation', () => {
66
+ const command = buildNextLevelCommand(
67
+ {
68
+ isolatedStack: ['screen', 'docker'],
69
+ labels: ['task=a "quote" $value'],
70
+ },
71
+ 'true'
72
+ );
73
+ expect(
74
+ parseArgs(
75
+ require('../src/lib/shell-utils').splitShellWords(command).slice(1)
76
+ ).wrapperOptions.labels
77
+ ).toEqual(['task=a "quote" $value']);
78
+ });
79
+ });
@@ -0,0 +1,25 @@
1
+ const { describe, it, expect } = require('bun:test');
2
+ const { spawnSync } = require('child_process');
3
+ const path = require('path');
4
+ const { parseArgs } = require('../src/lib/args-parser');
5
+
6
+ describe('wrapper help (#201)', () => {
7
+ for (const flag of ['--help', '-h']) {
8
+ it(`${flag} prints the no-argument usage and exits successfully`, () => {
9
+ const cli = path.join(__dirname, '../src/bin/cli.js');
10
+ const usage = spawnSync(process.execPath, [cli], { encoding: 'utf8' });
11
+ const result = spawnSync(process.execPath, [cli, flag], {
12
+ encoding: 'utf8',
13
+ });
14
+ expect(result.status).toBe(0);
15
+ expect(result.stderr).toBe('');
16
+ expect(result.stdout).toBe(usage.stdout);
17
+ expect(parseArgs([flag]).wrapperOptions.help).toBe(true);
18
+ });
19
+ it(`${flag} after the separator remains part of the command`, () => {
20
+ const result = parseArgs(['--', 'grep', flag]);
21
+ expect(result.wrapperOptions.help).toBe(false);
22
+ expect(result.rawCommand).toEqual(['grep', flag]);
23
+ });
24
+ }
25
+ });
@@ -0,0 +1,92 @@
1
+ const { describe, it, expect } = require('bun:test');
2
+ const { spawnSync } = require('child_process');
3
+ const {
4
+ isInteractiveShellCommand,
5
+ isShellInvocationWithArgs,
6
+ buildShellWithArgsCmdArgs,
7
+ buildDisplayCommand,
8
+ } = require('../src/lib/shell-utils');
9
+
10
+ describe('shell command semantics (#202)', () => {
11
+ for (const operator of [';', '&&', '||', '|', '&', '>', '<', '\n']) {
12
+ it(`keeps the whole command line when it contains ${JSON.stringify(operator)}`, () => {
13
+ const command = `sh -c 'exit 7'${operator} echo after $?`;
14
+ expect(isShellInvocationWithArgs(command)).toBe(false);
15
+ expect(buildDisplayCommand(command)).toBe(command);
16
+ expect(isInteractiveShellCommand(`sh ${operator} echo after`)).toBe(
17
+ false
18
+ );
19
+ });
20
+ }
21
+ it('preserves the script, empty positional argv, quotes, and quoted operators', () => {
22
+ const command = `sh -c 'printf "%s\\n" "$0" "$1" "$2"; echo "|"' a 'b c' ''`;
23
+ expect(isShellInvocationWithArgs(command)).toBe(true);
24
+ expect(buildShellWithArgsCmdArgs(command)).toEqual([
25
+ 'sh',
26
+ '-c',
27
+ 'printf "%s\\n" "$0" "$1" "$2"; echo "|"',
28
+ 'a',
29
+ 'b c',
30
+ '',
31
+ ]);
32
+ });
33
+ it('does not classify a missing script or a script-file argument as -c', () => {
34
+ expect(isShellInvocationWithArgs('sh -c')).toBe(false);
35
+ expect(isShellInvocationWithArgs("sh file -c 'echo hi'")).toBe(false);
36
+ });
37
+ it('retains direct bash -i -c behavior from #91', () => {
38
+ expect(buildShellWithArgsCmdArgs('bash -i -c "nvm --version"')).toEqual([
39
+ 'bash',
40
+ '-i',
41
+ '-c',
42
+ 'nvm --version',
43
+ ]);
44
+ });
45
+ it('leaves outer expansions, comments and globbing to the shell', () => {
46
+ for (const command of [
47
+ 'sh -c "echo $$"',
48
+ 'sh -c echo $HOME',
49
+ 'sh -c echo *',
50
+ 'sh -c echo # comment',
51
+ 'sh -c echo {a,b}',
52
+ "sh -c 'echo hi'\u00a0arg",
53
+ 'sh -c echo \\\nhi',
54
+ ]) {
55
+ expect(isShellInvocationWithArgs(command)).toBe(false);
56
+ }
57
+ });
58
+ it('preserves literal escapes within a directly executed script', () => {
59
+ expect(buildShellWithArgsCmdArgs('sh -c "printf \\q"')).toEqual([
60
+ 'sh',
61
+ '-c',
62
+ 'printf \\q',
63
+ ]);
64
+ expect(buildShellWithArgsCmdArgs('sh -c "echo \\$0" a')).toEqual([
65
+ 'sh',
66
+ '-c',
67
+ 'echo $0',
68
+ 'a',
69
+ ]);
70
+ expect(buildShellWithArgsCmdArgs('sh -c echo a\\;b')).toEqual([
71
+ 'sh',
72
+ '-c',
73
+ 'echo',
74
+ 'a;b',
75
+ ]);
76
+ });
77
+ const run = process.platform === 'win32' ? it.skip : it;
78
+ for (const [command, output, status] of [
79
+ ["sh -c 'exit 7'; echo after $?", 'after 7', 0],
80
+ ["sh -c 'echo $0 $1' a b", 'a b', 0],
81
+ ["sh -c 'kill -9 $$'; exit $?", '', 137],
82
+ ]) {
83
+ run(`executes ${command} with the expected output and exit`, () => {
84
+ const args = isShellInvocationWithArgs(command)
85
+ ? buildShellWithArgsCmdArgs(command)
86
+ : ['sh', '-c', command];
87
+ const result = spawnSync(args[0], args.slice(1), { encoding: 'utf8' });
88
+ expect(result.stdout.trim()).toBe(output);
89
+ expect(result.status).toBe(status);
90
+ });
91
+ }
92
+ });
@@ -69,7 +69,7 @@ function buildAttachedCmdArgs(command, shellToUse = '/bin/bash') {
69
69
  describe('isShellInvocationWithArgs (issue #91)', () => {
70
70
  it('should return true for "bash -i -c nvm --version"', () => {
71
71
  assert.strictEqual(
72
- isShellInvocationWithArgs('bash -i -c nvm --version'),
72
+ isShellInvocationWithArgs('bash -i -c "nvm --version"'),
73
73
  true
74
74
  );
75
75
  });
@@ -79,11 +79,14 @@ describe('isShellInvocationWithArgs (issue #91)', () => {
79
79
  });
80
80
 
81
81
  it('should return true for "bash -c echo hello"', () => {
82
- assert.strictEqual(isShellInvocationWithArgs('bash -c echo hello'), true);
82
+ assert.strictEqual(isShellInvocationWithArgs('bash -c "echo hello"'), true);
83
83
  });
84
84
 
85
85
  it('should return true for "zsh -c nvm --version"', () => {
86
- assert.strictEqual(isShellInvocationWithArgs('zsh -c nvm --version'), true);
86
+ assert.strictEqual(
87
+ isShellInvocationWithArgs('zsh -c "nvm --version"'),
88
+ true
89
+ );
87
90
  });
88
91
 
89
92
  it('should return true for "sh -c ls"', () => {
@@ -115,12 +118,12 @@ describe('isShellInvocationWithArgs (issue #91)', () => {
115
118
 
116
119
  describe('buildShellWithArgsCmdArgs (issue #91)', () => {
117
120
  it('should reconstruct "bash -i -c nvm --version" correctly', () => {
118
- const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
121
+ const result = buildShellWithArgsCmdArgs('bash -i -c "nvm --version"');
119
122
  assert.deepStrictEqual(result, ['bash', '-i', '-c', 'nvm --version']);
120
123
  });
121
124
 
122
125
  it('should reconstruct "bash -c echo hello" correctly', () => {
123
- const result = buildShellWithArgsCmdArgs('bash -c echo hello');
126
+ const result = buildShellWithArgsCmdArgs('bash -c "echo hello"');
124
127
  assert.deepStrictEqual(result, ['bash', '-c', 'echo hello']);
125
128
  });
126
129
 
@@ -130,13 +133,13 @@ describe('buildShellWithArgsCmdArgs (issue #91)', () => {
130
133
  });
131
134
 
132
135
  it('should handle zsh with -c', () => {
133
- const result = buildShellWithArgsCmdArgs('zsh -c nvm --version');
136
+ const result = buildShellWithArgsCmdArgs('zsh -c "nvm --version"');
134
137
  assert.deepStrictEqual(result, ['zsh', '-c', 'nvm --version']);
135
138
  });
136
139
 
137
140
  it('should handle /bin/bash -i -c with multi-word script', () => {
138
141
  const result = buildShellWithArgsCmdArgs(
139
- '/bin/bash -i -c node -e process.version'
142
+ '/bin/bash -i -c "node -e process.version"'
140
143
  );
141
144
  assert.deepStrictEqual(result, [
142
145
  '/bin/bash',
@@ -147,7 +150,7 @@ describe('buildShellWithArgsCmdArgs (issue #91)', () => {
147
150
  });
148
151
 
149
152
  it('should not include -c argument inside script argument (no double -c)', () => {
150
- const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
153
+ const result = buildShellWithArgsCmdArgs('bash -i -c "nvm --version"');
151
154
  // The script arg must be 'nvm --version', not '-c nvm --version'
152
155
  assert.strictEqual(result[result.length - 1], 'nvm --version');
153
156
  assert.strictEqual(result.indexOf('-c'), 2);
@@ -159,13 +162,13 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
159
162
  // Each test verifies that `bash -i -c "cmd"` style commands are NOT re-wrapped
160
163
  // in another outer shell -c invocation.
161
164
  //
162
- // Before fix: buildAttachedCmdArgs('bash -i -c nvm --version')
163
- // → ['/bin/bash', '-i', '-c', 'bash -i -c nvm --version'] (WRONG: double-wrap)
164
- // After fix: buildAttachedCmdArgs('bash -i -c nvm --version')
165
+ // Before fix: buildAttachedCmdArgs('bash -i -c "nvm --version"')
166
+ // → ['/bin/bash', '-i', '-c', 'bash -i -c "nvm --version"'] (WRONG: double-wrap)
167
+ // After fix: buildAttachedCmdArgs('bash -i -c "nvm --version"')
165
168
  // → ['bash', '-i', '-c', 'nvm --version'] (CORRECT: direct pass)
166
169
 
167
170
  it('should pass "bash -i -c nvm --version" directly without outer shell wrapper', () => {
168
- const args = buildAttachedCmdArgs('bash -i -c nvm --version');
171
+ const args = buildAttachedCmdArgs('bash -i -c "nvm --version"');
169
172
  // Must start with 'bash', not with '/bin/bash' (the outer shellToUse)
170
173
  assert.strictEqual(args[0], 'bash');
171
174
  // Must not wrap in outer bash -i -c
@@ -182,7 +185,7 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
182
185
  });
183
186
 
184
187
  it('should pass "zsh -c nvm --version" directly without outer shell wrapper', () => {
185
- const args = buildAttachedCmdArgs('zsh -c nvm --version', '/bin/zsh');
188
+ const args = buildAttachedCmdArgs('zsh -c "nvm --version"', '/bin/zsh');
186
189
  assert.deepStrictEqual(args, ['zsh', '-c', 'nvm --version']);
187
190
  assert.ok(
188
191
  args.filter((a) => a === '-c').length === 1,
@@ -191,12 +194,12 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
191
194
  });
192
195
 
193
196
  it('should pass "bash -c echo hello" directly', () => {
194
- const args = buildAttachedCmdArgs('bash -c echo hello');
197
+ const args = buildAttachedCmdArgs('bash -c "echo hello"');
195
198
  assert.deepStrictEqual(args, ['bash', '-c', 'echo hello']);
196
199
  });
197
200
 
198
201
  it('should not introduce a second bash layer (no shell-inside-shell)', () => {
199
- const args = buildAttachedCmdArgs('bash -i -c nvm --version');
202
+ const args = buildAttachedCmdArgs('bash -i -c "nvm --version"');
200
203
  // The first element of the docker image command args must NOT be the outer shell
201
204
  // i.e., must not be ['/bin/bash', '-i', '-c', 'bash ...']
202
205
  assert.notStrictEqual(
@@ -206,7 +209,7 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
206
209
  );
207
210
  // Must not contain the original full command string as an argument
208
211
  assert.ok(
209
- !args.includes('bash -i -c nvm --version'),
212
+ !args.includes('bash -i -c "nvm --version"'),
210
213
  'Must not contain the full command string as a single argument (double-wrap)'
211
214
  );
212
215
  });
@@ -241,7 +244,7 @@ describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShel
241
244
  'bash',
242
245
  'bash -i',
243
246
  'bash --norc',
244
- 'bash -i -c nvm --version',
247
+ 'bash -i -c "nvm --version"',
245
248
  'bash -c echo hi',
246
249
  'zsh -c ls',
247
250
  'nvm --version',
@@ -263,14 +266,14 @@ describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShel
263
266
  describe('buildDisplayCommand (issue #91 display fix)', () => {
264
267
  it('should quote the -c script argument when it contains spaces', () => {
265
268
  assert.strictEqual(
266
- buildDisplayCommand('bash -i -c nvm --version'),
269
+ buildDisplayCommand('bash -i -c "nvm --version"'),
267
270
  'bash -i -c "nvm --version"'
268
271
  );
269
272
  });
270
273
 
271
274
  it('should quote the -c script argument for plain "bash -c echo hello"', () => {
272
275
  assert.strictEqual(
273
- buildDisplayCommand('bash -c echo hello'),
276
+ buildDisplayCommand('bash -c "echo hello"'),
274
277
  'bash -c "echo hello"'
275
278
  );
276
279
  });
@@ -289,7 +292,7 @@ describe('buildDisplayCommand (issue #91 display fix)', () => {
289
292
 
290
293
  it('should handle zsh -c with spaces', () => {
291
294
  assert.strictEqual(
292
- buildDisplayCommand('zsh -c echo hello world'),
295
+ buildDisplayCommand('zsh -c "echo hello world"'),
293
296
  'zsh -c "echo hello world"'
294
297
  );
295
298
  });
@@ -0,0 +1,54 @@
1
+ const { describe, it } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { readFileSync } = require('node:fs');
4
+ const { resolve } = require('node:path');
5
+ const { parseArgs } = require('../src/lib/args-parser');
6
+ const { buildDockerRuntimeArgs } = require('../src/lib/docker-runtime-args');
7
+ const { ExecutionRecord } = require('../src/lib/execution-store');
8
+ const { formatRecord } = require('../src/lib/status-formatter');
9
+ const fixtures = JSON.parse(
10
+ readFileSync(resolve(__dirname, '../../parity/fixtures/contracts.json'))
11
+ );
12
+
13
+ describe('shared JavaScript and Rust golden contracts', () => {
14
+ for (const fixture of fixtures['cli-parsing']) {
15
+ it(`CLI parsing: ${fixture.name}`, () => {
16
+ if (fixture.error) {
17
+ assert.throws(() => parseArgs(fixture.argv), new RegExp(fixture.error));
18
+ return;
19
+ }
20
+ const parsed = parseArgs(fixture.argv);
21
+ assert.equal(parsed.command, fixture.command);
22
+ assert.deepEqual(parsed.rawCommand, fixture.rawCommand);
23
+ for (const [key, value] of Object.entries(fixture.options)) {
24
+ assert.deepEqual(parsed.wrapperOptions[key], value, key);
25
+ }
26
+ });
27
+ }
28
+ for (const fixture of fixtures['isolation-arguments']) {
29
+ it(`isolation argv: ${fixture.name}`, () => {
30
+ assert.deepEqual(buildDockerRuntimeArgs(fixture.options), fixture.argv);
31
+ });
32
+ }
33
+ it('uses the same JSON and text status output', () => {
34
+ const fixture = fixtures['status-output'];
35
+ const record = new ExecutionRecord(fixture.record);
36
+ assert.deepEqual(JSON.parse(formatRecord(record, 'json')), fixture.json);
37
+ assert.equal(formatRecord(record, 'text'), fixture.text);
38
+ });
39
+ for (const fixture of fixtures['execution-record-format']) {
40
+ it(`preserves the complete execution record format (exit ${fixture.exitCode})`, () => {
41
+ assert.deepEqual(ExecutionRecord.fromObject(fixture).toObject(), fixture);
42
+ });
43
+ }
44
+ it('executes the JavaScript source of the generated Rust parity policy', async () => {
45
+ const { minimumRustTestCount } =
46
+ await import('../../scripts/parity-threshold.mjs');
47
+ for (const fixture of fixtures['parity-threshold']) {
48
+ assert.equal(
49
+ minimumRustTestCount(fixture.javascript),
50
+ fixture.minimumRust
51
+ );
52
+ }
53
+ });
54
+ });