start-command 0.20.3 → 0.21.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.
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Sequence Parser for Isolation Stacking
3
+ *
4
+ * Parses space-separated sequences with underscore placeholders for
5
+ * distributing options across isolation levels.
6
+ *
7
+ * Based on Links Notation conventions (https://github.com/link-foundation/links-notation)
8
+ */
9
+
10
+ /**
11
+ * Parse a space-separated sequence with underscore placeholders
12
+ * @param {string} value - Space-separated values (e.g., "screen ssh docker")
13
+ * @returns {(string|null)[]} Array of values, with null for underscore placeholders
14
+ */
15
+ function parseSequence(value) {
16
+ if (!value || typeof value !== 'string') {
17
+ return [];
18
+ }
19
+
20
+ const trimmed = value.trim();
21
+ if (!trimmed) {
22
+ return [];
23
+ }
24
+
25
+ // Split by whitespace
26
+ const parts = trimmed.split(/\s+/);
27
+
28
+ // Convert underscores to null (placeholder)
29
+ return parts.map((v) => (v === '_' ? null : v));
30
+ }
31
+
32
+ /**
33
+ * Format a sequence array back to a string
34
+ * @param {(string|null)[]} sequence - Array of values with nulls for placeholders
35
+ * @returns {string} Space-separated string with underscores for nulls
36
+ */
37
+ function formatSequence(sequence) {
38
+ if (!Array.isArray(sequence) || sequence.length === 0) {
39
+ return '';
40
+ }
41
+
42
+ return sequence.map((v) => (v === null ? '_' : v)).join(' ');
43
+ }
44
+
45
+ /**
46
+ * Shift sequence by removing first element
47
+ * @param {(string|null)[]} sequence - Parsed sequence
48
+ * @returns {(string|null)[]} New sequence without first element
49
+ */
50
+ function shiftSequence(sequence) {
51
+ if (!Array.isArray(sequence) || sequence.length === 0) {
52
+ return [];
53
+ }
54
+ return sequence.slice(1);
55
+ }
56
+
57
+ /**
58
+ * Check if a string represents a multi-value sequence (contains spaces)
59
+ * @param {string} value - Value to check
60
+ * @returns {boolean} True if contains spaces (multi-value)
61
+ */
62
+ function isSequence(value) {
63
+ return typeof value === 'string' && value.includes(' ');
64
+ }
65
+
66
+ /**
67
+ * Distribute a single option value across all isolation levels
68
+ * If the value is a sequence, validate length matches stack depth
69
+ * If the value is a single value, replicate it for all levels
70
+ *
71
+ * @param {string} optionValue - Space-separated or single value
72
+ * @param {number} stackDepth - Number of isolation levels
73
+ * @param {string} optionName - Name of option for error messages
74
+ * @returns {(string|null)[]} Array of values for each level
75
+ * @throws {Error} If sequence length doesn't match stack depth
76
+ */
77
+ function distributeOption(optionValue, stackDepth, optionName) {
78
+ if (!optionValue) {
79
+ return Array(stackDepth).fill(null);
80
+ }
81
+
82
+ const parsed = parseSequence(optionValue);
83
+
84
+ // Single value: replicate for all levels
85
+ if (parsed.length === 1 && stackDepth > 1) {
86
+ return Array(stackDepth).fill(parsed[0]);
87
+ }
88
+
89
+ // Sequence: validate length matches
90
+ if (parsed.length !== stackDepth) {
91
+ throw new Error(
92
+ `${optionName} has ${parsed.length} value(s) but isolation stack has ${stackDepth} level(s). ` +
93
+ `Use underscores (_) as placeholders for levels that don't need this option.`
94
+ );
95
+ }
96
+
97
+ return parsed;
98
+ }
99
+
100
+ /**
101
+ * Get the value at a specific level from a distributed option
102
+ * @param {(string|null)[]} distributedOption - Distributed option array
103
+ * @param {number} level - Zero-based level index
104
+ * @returns {string|null} Value at that level or null
105
+ */
106
+ function getValueAtLevel(distributedOption, level) {
107
+ if (
108
+ !Array.isArray(distributedOption) ||
109
+ level < 0 ||
110
+ level >= distributedOption.length
111
+ ) {
112
+ return null;
113
+ }
114
+ return distributedOption[level];
115
+ }
116
+
117
+ /**
118
+ * Validate that required options are provided for specific isolation types
119
+ * @param {(string|null)[]} isolationStack - Stack of isolation backends
120
+ * @param {object} options - Object containing distributed options
121
+ * @param {(string|null)[]} options.endpoints - Distributed endpoints for SSH
122
+ * @param {(string|null)[]} options.images - Distributed images for Docker
123
+ * @throws {Error} If required options are missing
124
+ */
125
+ function validateStackOptions(isolationStack, options) {
126
+ const errors = [];
127
+
128
+ isolationStack.forEach((backend, i) => {
129
+ if (backend === 'ssh') {
130
+ if (!options.endpoints || !options.endpoints[i]) {
131
+ errors.push(
132
+ `Level ${i + 1} is SSH but no endpoint specified. ` +
133
+ `Use --endpoint with a value at position ${i + 1}.`
134
+ );
135
+ }
136
+ }
137
+ // Docker doesn't require image - has default
138
+ // Screen and tmux don't require special options
139
+ });
140
+
141
+ if (errors.length > 0) {
142
+ throw new Error(errors.join('\n'));
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Build remaining options for next isolation level
148
+ * @param {object} options - Current level options
149
+ * @returns {object} Options for next level
150
+ */
151
+ function buildNextLevelOptions(options) {
152
+ const next = { ...options };
153
+
154
+ // Shift isolation stack
155
+ if (next.isolatedStack && next.isolatedStack.length > 1) {
156
+ next.isolatedStack = shiftSequence(next.isolatedStack);
157
+ next.isolated = next.isolatedStack[0];
158
+ } else {
159
+ next.isolatedStack = [];
160
+ next.isolated = null;
161
+ }
162
+
163
+ // Shift distributed options
164
+ if (next.imageStack && next.imageStack.length > 1) {
165
+ next.imageStack = shiftSequence(next.imageStack);
166
+ next.image = next.imageStack[0];
167
+ } else if (next.imageStack) {
168
+ next.imageStack = [];
169
+ }
170
+
171
+ if (next.endpointStack && next.endpointStack.length > 1) {
172
+ next.endpointStack = shiftSequence(next.endpointStack);
173
+ next.endpoint = next.endpointStack[0];
174
+ } else if (next.endpointStack) {
175
+ next.endpointStack = [];
176
+ }
177
+
178
+ if (next.sessionStack && next.sessionStack.length > 1) {
179
+ next.sessionStack = shiftSequence(next.sessionStack);
180
+ next.session = next.sessionStack[0];
181
+ } else if (next.sessionStack) {
182
+ next.sessionStack = [];
183
+ }
184
+
185
+ return next;
186
+ }
187
+
188
+ /**
189
+ * Format isolation chain for display
190
+ * @param {(string|null)[]} stack - Isolation stack
191
+ * @param {object} options - Options with distributed values
192
+ * @returns {string} Formatted chain (e.g., "screen → ssh@host → docker:ubuntu")
193
+ */
194
+ function formatIsolationChain(stack, options = {}) {
195
+ if (!Array.isArray(stack) || stack.length === 0) {
196
+ return '';
197
+ }
198
+
199
+ return stack
200
+ .map((backend, i) => {
201
+ if (!backend) {
202
+ return '_';
203
+ }
204
+
205
+ if (backend === 'ssh' && options.endpointStack?.[i]) {
206
+ return `ssh@${options.endpointStack[i]}`;
207
+ }
208
+
209
+ if (backend === 'docker' && options.imageStack?.[i]) {
210
+ // Extract short image name
211
+ const image = options.imageStack[i];
212
+ const shortName = image.split(':')[0].split('/').pop();
213
+ return `docker:${shortName}`;
214
+ }
215
+
216
+ return backend;
217
+ })
218
+ .join(' → ');
219
+ }
220
+
221
+ module.exports = {
222
+ parseSequence,
223
+ formatSequence,
224
+ shiftSequence,
225
+ isSequence,
226
+ distributeOption,
227
+ getValueAtLevel,
228
+ validateStackOptions,
229
+ buildNextLevelOptions,
230
+ formatIsolationChain,
231
+ };
@@ -232,7 +232,7 @@ describe('parseArgs', () => {
232
232
  'npm',
233
233
  'test',
234
234
  ]);
235
- }, /--image option is only valid with --isolated docker/);
235
+ }, /--image option is only valid when isolation stack includes docker/);
236
236
  });
237
237
  });
238
238
 
@@ -264,7 +264,7 @@ describe('parseArgs', () => {
264
264
  it('should throw error for ssh without endpoint', () => {
265
265
  assert.throws(() => {
266
266
  parseArgs(['--isolated', 'ssh', '--', 'npm', 'test']);
267
- }, /SSH isolation requires --endpoint option/);
267
+ }, /SSH isolation at level 1 requires --endpoint option/);
268
268
  });
269
269
 
270
270
  it('should throw error for endpoint with non-ssh backend', () => {
@@ -278,7 +278,7 @@ describe('parseArgs', () => {
278
278
  'npm',
279
279
  'test',
280
280
  ]);
281
- }, /--endpoint option is only valid with --isolated ssh/);
281
+ }, /--endpoint option is only valid when isolation stack includes ssh/);
282
282
  });
283
283
  });
284
284
 
@@ -384,13 +384,13 @@ describe('parseArgs', () => {
384
384
  'npm',
385
385
  'test',
386
386
  ]);
387
- }, /--auto-remove-docker-container option is only valid with --isolated docker/);
387
+ }, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
388
388
  });
389
389
 
390
390
  it('should throw error for auto-remove-docker-container without isolation', () => {
391
391
  assert.throws(() => {
392
392
  parseArgs(['--auto-remove-docker-container', '--', 'npm', 'test']);
393
- }, /--auto-remove-docker-container option is only valid with --isolated docker/);
393
+ }, /--auto-remove-docker-container option is only valid when isolation stack includes docker/);
394
394
  });
395
395
 
396
396
  it('should work with keep-alive and auto-remove-docker-container', () => {
@@ -706,7 +706,7 @@ describe('user isolation option', () => {
706
706
  'npm',
707
707
  'install',
708
708
  ]);
709
- }, /--isolated-user is not supported with Docker isolation/);
709
+ }, /--isolated-user is not supported with Docker as the first isolation level/);
710
710
  });
711
711
 
712
712
  it('should work with tmux isolation', () => {
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Tests for isolation stacking feature (issue #77)
3
+ */
4
+
5
+ const { describe, it, expect } = require('bun:test');
6
+ const {
7
+ parseArgs,
8
+ validateOptions,
9
+ hasStackedIsolation,
10
+ MAX_ISOLATION_DEPTH,
11
+ } = require('../src/lib/args-parser');
12
+
13
+ describe('Isolation Stacking - Args Parser', () => {
14
+ describe('parseArgs with stacked --isolated', () => {
15
+ it('should parse single isolation (backward compatible)', () => {
16
+ const result = parseArgs(['--isolated', 'docker', '--', 'npm', 'test']);
17
+ expect(result.wrapperOptions.isolated).toBe('docker');
18
+ expect(result.wrapperOptions.isolatedStack).toEqual(['docker']);
19
+ expect(result.command).toBe('npm test');
20
+ });
21
+
22
+ it('should parse multi-level isolation', () => {
23
+ const result = parseArgs([
24
+ '--isolated',
25
+ 'screen ssh docker',
26
+ '--endpoint',
27
+ '_ user@host _',
28
+ '--',
29
+ 'npm',
30
+ 'test',
31
+ ]);
32
+ expect(result.wrapperOptions.isolated).toBe('screen');
33
+ expect(result.wrapperOptions.isolatedStack).toEqual([
34
+ 'screen',
35
+ 'ssh',
36
+ 'docker',
37
+ ]);
38
+ });
39
+
40
+ it('should parse 5-level isolation', () => {
41
+ const result = parseArgs([
42
+ '--isolated',
43
+ 'screen ssh tmux ssh docker',
44
+ '--endpoint',
45
+ '_ user@server1 _ user@server2 _',
46
+ '--',
47
+ 'npm',
48
+ 'test',
49
+ ]);
50
+ expect(result.wrapperOptions.isolatedStack).toEqual([
51
+ 'screen',
52
+ 'ssh',
53
+ 'tmux',
54
+ 'ssh',
55
+ 'docker',
56
+ ]);
57
+ expect(result.wrapperOptions.endpointStack).toEqual([
58
+ null,
59
+ 'user@server1',
60
+ null,
61
+ 'user@server2',
62
+ null,
63
+ ]);
64
+ });
65
+
66
+ it('should parse --isolated=value syntax', () => {
67
+ const result = parseArgs(['--isolated=screen tmux', '--', 'ls']);
68
+ expect(result.wrapperOptions.isolatedStack).toEqual(['screen', 'tmux']);
69
+ });
70
+ });
71
+
72
+ describe('parseArgs with stacked --image', () => {
73
+ it('should parse single image (backward compatible)', () => {
74
+ const result = parseArgs([
75
+ '--isolated',
76
+ 'docker',
77
+ '--image',
78
+ 'ubuntu:22.04',
79
+ '--',
80
+ 'bash',
81
+ ]);
82
+ expect(result.wrapperOptions.image).toBe('ubuntu:22.04');
83
+ });
84
+
85
+ it('should parse image sequence with placeholders', () => {
86
+ const result = parseArgs([
87
+ '--isolated',
88
+ 'screen docker',
89
+ '--image',
90
+ '_ ubuntu:22.04',
91
+ '--',
92
+ 'bash',
93
+ ]);
94
+ expect(result.wrapperOptions.imageStack).toEqual([null, 'ubuntu:22.04']);
95
+ });
96
+
97
+ it('should parse --image=value syntax', () => {
98
+ const result = parseArgs([
99
+ '--isolated',
100
+ 'docker',
101
+ '--image=alpine:latest',
102
+ '--',
103
+ 'sh',
104
+ ]);
105
+ expect(result.wrapperOptions.image).toBe('alpine:latest');
106
+ });
107
+ });
108
+
109
+ describe('parseArgs with stacked --endpoint', () => {
110
+ it('should parse single endpoint (backward compatible)', () => {
111
+ const result = parseArgs([
112
+ '--isolated',
113
+ 'ssh',
114
+ '--endpoint',
115
+ 'user@host',
116
+ '--',
117
+ 'ls',
118
+ ]);
119
+ expect(result.wrapperOptions.endpoint).toBe('user@host');
120
+ });
121
+
122
+ it('should parse endpoint sequence with placeholders', () => {
123
+ const result = parseArgs([
124
+ '--isolated',
125
+ 'screen ssh ssh docker',
126
+ '--endpoint',
127
+ '_ user@host1 user@host2 _',
128
+ '--',
129
+ 'bash',
130
+ ]);
131
+ expect(result.wrapperOptions.endpointStack).toEqual([
132
+ null,
133
+ 'user@host1',
134
+ 'user@host2',
135
+ null,
136
+ ]);
137
+ });
138
+ });
139
+
140
+ describe('hasStackedIsolation', () => {
141
+ it('should return true for multi-level', () => {
142
+ const { wrapperOptions } = parseArgs([
143
+ '--isolated',
144
+ 'screen docker',
145
+ '--',
146
+ 'test',
147
+ ]);
148
+ expect(hasStackedIsolation(wrapperOptions)).toBe(true);
149
+ });
150
+
151
+ it('should return false for single level', () => {
152
+ const { wrapperOptions } = parseArgs([
153
+ '--isolated',
154
+ 'docker',
155
+ '--',
156
+ 'test',
157
+ ]);
158
+ expect(hasStackedIsolation(wrapperOptions)).toBe(false);
159
+ });
160
+
161
+ it('should return false for no isolation', () => {
162
+ const { wrapperOptions } = parseArgs(['echo', 'hello']);
163
+ // When no isolation, isolatedStack is null, so hasStackedIsolation returns falsy
164
+ expect(hasStackedIsolation(wrapperOptions)).toBeFalsy();
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('Isolation Stacking - Validation', () => {
170
+ describe('validateOptions', () => {
171
+ it('should validate single backend (backward compatible)', () => {
172
+ const options = {
173
+ isolated: 'docker',
174
+ isolatedStack: ['docker'],
175
+ };
176
+ expect(() => validateOptions(options)).not.toThrow();
177
+ // Should apply default image
178
+ expect(options.imageStack[0]).toBeDefined();
179
+ });
180
+
181
+ it('should validate multi-level stack', () => {
182
+ const options = {
183
+ isolated: 'screen',
184
+ isolatedStack: ['screen', 'ssh', 'docker'],
185
+ endpointStack: [null, 'user@host', null],
186
+ };
187
+ expect(() => validateOptions(options)).not.toThrow();
188
+ });
189
+
190
+ it('should throw on invalid backend in stack', () => {
191
+ const options = {
192
+ isolated: 'screen',
193
+ isolatedStack: ['screen', 'invalid', 'docker'],
194
+ };
195
+ expect(() => validateOptions(options)).toThrow(/Invalid isolation/);
196
+ });
197
+
198
+ it('should throw on missing SSH endpoint', () => {
199
+ const options = {
200
+ isolated: 'ssh',
201
+ isolatedStack: ['ssh'],
202
+ endpointStack: [null],
203
+ };
204
+ expect(() => validateOptions(options)).toThrow(/requires --endpoint/);
205
+ });
206
+
207
+ it('should throw on image/stack length mismatch', () => {
208
+ const options = {
209
+ isolated: 'screen',
210
+ isolatedStack: ['screen', 'ssh', 'docker'],
211
+ imageStack: [null, 'ubuntu:22.04'], // Only 2, should be 3
212
+ endpointStack: [null, 'user@host', null],
213
+ };
214
+ expect(() => validateOptions(options)).toThrow(/value\(s\)/);
215
+ });
216
+
217
+ it('should throw on depth exceeding limit', () => {
218
+ const tooDeep = Array(MAX_ISOLATION_DEPTH + 1).fill('screen');
219
+ const options = {
220
+ isolated: 'screen',
221
+ isolatedStack: tooDeep,
222
+ };
223
+ expect(() => validateOptions(options)).toThrow(/too deep/);
224
+ });
225
+
226
+ it('should distribute single image to all levels', () => {
227
+ const options = {
228
+ isolated: 'screen',
229
+ isolatedStack: ['screen', 'docker', 'docker'],
230
+ image: 'ubuntu:22.04',
231
+ };
232
+ validateOptions(options);
233
+ expect(options.imageStack).toEqual([
234
+ 'ubuntu:22.04',
235
+ 'ubuntu:22.04',
236
+ 'ubuntu:22.04',
237
+ ]);
238
+ });
239
+
240
+ it('should apply default docker image for each docker level', () => {
241
+ const options = {
242
+ isolated: 'docker',
243
+ isolatedStack: ['docker'],
244
+ };
245
+ validateOptions(options);
246
+ expect(options.imageStack[0]).toBeDefined();
247
+ expect(options.imageStack[0]).toContain(':'); // Should have image:tag format
248
+ });
249
+
250
+ it('should throw if image provided but no docker in stack', () => {
251
+ const options = {
252
+ isolated: 'screen',
253
+ isolatedStack: ['screen', 'tmux'],
254
+ image: 'ubuntu:22.04',
255
+ };
256
+ expect(() => validateOptions(options)).toThrow(/docker/);
257
+ });
258
+
259
+ it('should throw if endpoint provided but no ssh in stack', () => {
260
+ const options = {
261
+ isolated: 'screen',
262
+ isolatedStack: ['screen', 'docker'],
263
+ endpoint: 'user@host',
264
+ imageStack: [null, 'ubuntu:22.04'],
265
+ };
266
+ expect(() => validateOptions(options)).toThrow(/ssh/);
267
+ });
268
+ });
269
+ });
270
+
271
+ describe('Isolation Stacking - Backward Compatibility', () => {
272
+ it('should work with existing docker command', () => {
273
+ const result = parseArgs([
274
+ '--isolated',
275
+ 'docker',
276
+ '--image',
277
+ 'node:18',
278
+ '--',
279
+ 'npm',
280
+ 'test',
281
+ ]);
282
+ expect(result.wrapperOptions.isolated).toBe('docker');
283
+ expect(result.wrapperOptions.image).toBe('node:18');
284
+ expect(result.wrapperOptions.isolatedStack).toEqual(['docker']);
285
+ expect(result.command).toBe('npm test');
286
+ });
287
+
288
+ it('should work with existing ssh command', () => {
289
+ const result = parseArgs([
290
+ '--isolated',
291
+ 'ssh',
292
+ '--endpoint',
293
+ 'user@server',
294
+ '--',
295
+ 'ls',
296
+ '-la',
297
+ ]);
298
+ expect(result.wrapperOptions.isolated).toBe('ssh');
299
+ expect(result.wrapperOptions.endpoint).toBe('user@server');
300
+ expect(result.wrapperOptions.isolatedStack).toEqual(['ssh']);
301
+ });
302
+
303
+ it('should work with existing screen command', () => {
304
+ const result = parseArgs([
305
+ '--isolated',
306
+ 'screen',
307
+ '--detached',
308
+ '--keep-alive',
309
+ '--',
310
+ 'long-running-task',
311
+ ]);
312
+ expect(result.wrapperOptions.isolated).toBe('screen');
313
+ expect(result.wrapperOptions.detached).toBe(true);
314
+ expect(result.wrapperOptions.keepAlive).toBe(true);
315
+ expect(result.wrapperOptions.isolatedStack).toEqual(['screen']);
316
+ });
317
+
318
+ it('should work with -i shorthand', () => {
319
+ const result = parseArgs(['-i', 'tmux', '--', 'vim']);
320
+ expect(result.wrapperOptions.isolated).toBe('tmux');
321
+ expect(result.wrapperOptions.isolatedStack).toEqual(['tmux']);
322
+ });
323
+
324
+ it('should work with attached mode', () => {
325
+ const result = parseArgs([
326
+ '--isolated',
327
+ 'docker',
328
+ '--attached',
329
+ '--image',
330
+ 'alpine',
331
+ '--',
332
+ 'sh',
333
+ ]);
334
+ expect(result.wrapperOptions.attached).toBe(true);
335
+ expect(result.wrapperOptions.isolated).toBe('docker');
336
+ });
337
+
338
+ it('should work with session name', () => {
339
+ const result = parseArgs([
340
+ '--isolated',
341
+ 'screen',
342
+ '--session',
343
+ 'my-session',
344
+ '--',
345
+ 'bash',
346
+ ]);
347
+ expect(result.wrapperOptions.session).toBe('my-session');
348
+ });
349
+
350
+ it('should work with session-id', () => {
351
+ const result = parseArgs([
352
+ '--isolated',
353
+ 'docker',
354
+ '--image',
355
+ 'alpine',
356
+ '--session-id',
357
+ '12345678-1234-4123-8123-123456789012',
358
+ '--',
359
+ 'echo',
360
+ 'hi',
361
+ ]);
362
+ expect(result.wrapperOptions.sessionId).toBe(
363
+ '12345678-1234-4123-8123-123456789012'
364
+ );
365
+ });
366
+ });
@@ -533,13 +533,41 @@ describe('output-blocks module', () => {
533
533
  // $ docker pull alpine:latest
534
534
  // <empty line>
535
535
  // <docker output>
536
+ // <empty line>
536
537
  // ✓
537
538
  // │
538
539
  // $ echo hi
539
540
  // <empty line>
540
541
  // hi
542
+ // <empty line>
541
543
  // ✓
542
544
  });
545
+
546
+ it('output formatting follows visual continuity pattern', () => {
547
+ // Issue #73: All commands should have consistent formatting:
548
+ // 1. Command line ($ ...)
549
+ // 2. Empty line (visual separation)
550
+ // 3. Command output
551
+ // 4. Empty line (visual separation)
552
+ // 5. Result marker (✓ or ✗)
553
+ //
554
+ // This test documents the expected output structure that
555
+ // dockerPullImage and runInDocker should produce.
556
+
557
+ // Verify createCommandLine produces the correct format
558
+ const commandLine = createCommandLine('docker pull alpine:latest');
559
+ expect(commandLine).toBe('$ docker pull alpine:latest');
560
+
561
+ // Verify createVirtualCommandBlock matches
562
+ const virtualCommandLine = createVirtualCommandBlock(
563
+ 'docker pull alpine:latest'
564
+ );
565
+ expect(virtualCommandLine).toBe(commandLine);
566
+
567
+ // Verify result markers
568
+ expect(createVirtualCommandResult(true)).toBe('✓');
569
+ expect(createVirtualCommandResult(false)).toBe('✗');
570
+ });
543
571
  });
544
572
  });
545
573