start-command 0.3.1
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.changeset/isolation-support.md +30 -0
- package/.github/workflows/release.yml +292 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +6 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +24 -0
- package/README.md +249 -0
- package/REQUIREMENTS.md +229 -0
- package/bun.lock +453 -0
- package/bunfig.toml +3 -0
- package/eslint.config.mjs +122 -0
- package/experiments/debug-regex.js +49 -0
- package/experiments/isolation-design.md +142 -0
- package/experiments/test-cli.sh +42 -0
- package/experiments/test-substitution.js +143 -0
- package/package.json +63 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/check-file-size.mjs +103 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +89 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +219 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/publish-to-npm.mjs +129 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +107 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/bin/cli.js +670 -0
- package/src/lib/args-parser.js +259 -0
- package/src/lib/isolation.js +419 -0
- package/src/lib/substitution.js +323 -0
- package/src/lib/substitutions.lino +308 -0
- package/test/args-parser.test.js +389 -0
- package/test/isolation.test.js +248 -0
- package/test/substitution.test.js +236 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the argument parser
|
|
4
|
+
* Tests wrapper options parsing, validation, and command extraction
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it } = require('node:test');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const {
|
|
10
|
+
parseArgs,
|
|
11
|
+
validateOptions,
|
|
12
|
+
generateSessionName,
|
|
13
|
+
hasIsolation,
|
|
14
|
+
getEffectiveMode,
|
|
15
|
+
VALID_BACKENDS,
|
|
16
|
+
} = require('../src/lib/args-parser');
|
|
17
|
+
|
|
18
|
+
describe('parseArgs', () => {
|
|
19
|
+
describe('basic command parsing', () => {
|
|
20
|
+
it('should parse simple command without options', () => {
|
|
21
|
+
const result = parseArgs(['echo', 'hello', 'world']);
|
|
22
|
+
assert.strictEqual(result.command, 'echo hello world');
|
|
23
|
+
assert.strictEqual(result.wrapperOptions.isolated, null);
|
|
24
|
+
assert.strictEqual(result.wrapperOptions.attached, false);
|
|
25
|
+
assert.strictEqual(result.wrapperOptions.detached, false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should parse command with -- separator', () => {
|
|
29
|
+
const result = parseArgs(['--', 'npm', 'test']);
|
|
30
|
+
assert.strictEqual(result.command, 'npm test');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should parse empty command correctly', () => {
|
|
34
|
+
const result = parseArgs([]);
|
|
35
|
+
assert.strictEqual(result.command, '');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('isolation options', () => {
|
|
40
|
+
it('should parse --isolated with value', () => {
|
|
41
|
+
const result = parseArgs(['--isolated', 'tmux', '--', 'npm', 'test']);
|
|
42
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'tmux');
|
|
43
|
+
assert.strictEqual(result.command, 'npm test');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should parse -i shorthand', () => {
|
|
47
|
+
const result = parseArgs(['-i', 'screen', '--', 'npm', 'start']);
|
|
48
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'screen');
|
|
49
|
+
assert.strictEqual(result.command, 'npm start');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should parse --isolated=value format', () => {
|
|
53
|
+
const result = parseArgs(['--isolated=tmux', '--', 'ls', '-la']);
|
|
54
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'tmux');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should normalize backend to lowercase', () => {
|
|
58
|
+
const result = parseArgs(['--isolated', 'TMUX', '--', 'echo', 'test']);
|
|
59
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'tmux');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should throw error for missing backend argument', () => {
|
|
63
|
+
assert.throws(() => {
|
|
64
|
+
parseArgs(['--isolated']);
|
|
65
|
+
}, /requires a backend argument/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('attached and detached modes', () => {
|
|
70
|
+
it('should parse --attached flag', () => {
|
|
71
|
+
const result = parseArgs([
|
|
72
|
+
'--isolated',
|
|
73
|
+
'tmux',
|
|
74
|
+
'--attached',
|
|
75
|
+
'--',
|
|
76
|
+
'npm',
|
|
77
|
+
'test',
|
|
78
|
+
]);
|
|
79
|
+
assert.strictEqual(result.wrapperOptions.attached, true);
|
|
80
|
+
assert.strictEqual(result.wrapperOptions.detached, false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should parse -a shorthand', () => {
|
|
84
|
+
const result = parseArgs(['-i', 'screen', '-a', '--', 'npm', 'start']);
|
|
85
|
+
assert.strictEqual(result.wrapperOptions.attached, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should parse --detached flag', () => {
|
|
89
|
+
const result = parseArgs([
|
|
90
|
+
'--isolated',
|
|
91
|
+
'tmux',
|
|
92
|
+
'--detached',
|
|
93
|
+
'--',
|
|
94
|
+
'npm',
|
|
95
|
+
'start',
|
|
96
|
+
]);
|
|
97
|
+
assert.strictEqual(result.wrapperOptions.detached, true);
|
|
98
|
+
assert.strictEqual(result.wrapperOptions.attached, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should parse -d shorthand', () => {
|
|
102
|
+
const result = parseArgs(['-i', 'screen', '-d', '--', 'npm', 'start']);
|
|
103
|
+
assert.strictEqual(result.wrapperOptions.detached, true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should throw error when both --attached and --detached are set', () => {
|
|
107
|
+
assert.throws(() => {
|
|
108
|
+
parseArgs([
|
|
109
|
+
'--isolated',
|
|
110
|
+
'tmux',
|
|
111
|
+
'--attached',
|
|
112
|
+
'--detached',
|
|
113
|
+
'--',
|
|
114
|
+
'npm',
|
|
115
|
+
'test',
|
|
116
|
+
]);
|
|
117
|
+
}, /Cannot use both --attached and --detached/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should provide helpful error message for mode conflict', () => {
|
|
121
|
+
try {
|
|
122
|
+
parseArgs(['-i', 'screen', '-a', '-d', '--', 'npm', 'test']);
|
|
123
|
+
assert.fail('Should have thrown an error');
|
|
124
|
+
} catch (err) {
|
|
125
|
+
assert.ok(err.message.includes('Please choose only one mode'));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('session option', () => {
|
|
131
|
+
it('should parse --session with value', () => {
|
|
132
|
+
const result = parseArgs([
|
|
133
|
+
'--isolated',
|
|
134
|
+
'tmux',
|
|
135
|
+
'--session',
|
|
136
|
+
'my-session',
|
|
137
|
+
'--',
|
|
138
|
+
'npm',
|
|
139
|
+
'test',
|
|
140
|
+
]);
|
|
141
|
+
assert.strictEqual(result.wrapperOptions.session, 'my-session');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should parse -s shorthand', () => {
|
|
145
|
+
const result = parseArgs([
|
|
146
|
+
'-i',
|
|
147
|
+
'screen',
|
|
148
|
+
'-s',
|
|
149
|
+
'test-session',
|
|
150
|
+
'--',
|
|
151
|
+
'npm',
|
|
152
|
+
'start',
|
|
153
|
+
]);
|
|
154
|
+
assert.strictEqual(result.wrapperOptions.session, 'test-session');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should parse --session=value format', () => {
|
|
158
|
+
const result = parseArgs([
|
|
159
|
+
'--isolated',
|
|
160
|
+
'tmux',
|
|
161
|
+
'--session=my-session',
|
|
162
|
+
'--',
|
|
163
|
+
'npm',
|
|
164
|
+
'test',
|
|
165
|
+
]);
|
|
166
|
+
assert.strictEqual(result.wrapperOptions.session, 'my-session');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should throw error for session without isolation', () => {
|
|
170
|
+
assert.throws(() => {
|
|
171
|
+
parseArgs(['--session', 'my-session', '--', 'npm', 'test']);
|
|
172
|
+
}, /--session option is only valid with --isolated/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('docker image option', () => {
|
|
177
|
+
it('should parse --image with value', () => {
|
|
178
|
+
const result = parseArgs([
|
|
179
|
+
'--isolated',
|
|
180
|
+
'docker',
|
|
181
|
+
'--image',
|
|
182
|
+
'node:20',
|
|
183
|
+
'--',
|
|
184
|
+
'npm',
|
|
185
|
+
'test',
|
|
186
|
+
]);
|
|
187
|
+
assert.strictEqual(result.wrapperOptions.image, 'node:20');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should parse --image=value format', () => {
|
|
191
|
+
const result = parseArgs([
|
|
192
|
+
'--isolated',
|
|
193
|
+
'docker',
|
|
194
|
+
'--image=alpine:latest',
|
|
195
|
+
'--',
|
|
196
|
+
'ls',
|
|
197
|
+
]);
|
|
198
|
+
assert.strictEqual(result.wrapperOptions.image, 'alpine:latest');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should throw error for docker without image', () => {
|
|
202
|
+
assert.throws(() => {
|
|
203
|
+
parseArgs(['--isolated', 'docker', '--', 'npm', 'test']);
|
|
204
|
+
}, /Docker isolation requires --image option/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should throw error for image with non-docker backend', () => {
|
|
208
|
+
assert.throws(() => {
|
|
209
|
+
parseArgs([
|
|
210
|
+
'--isolated',
|
|
211
|
+
'tmux',
|
|
212
|
+
'--image',
|
|
213
|
+
'node:20',
|
|
214
|
+
'--',
|
|
215
|
+
'npm',
|
|
216
|
+
'test',
|
|
217
|
+
]);
|
|
218
|
+
}, /--image option is only valid with --isolated docker/);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('command without separator', () => {
|
|
223
|
+
it('should parse command after options without separator', () => {
|
|
224
|
+
const result = parseArgs(['-i', 'tmux', '-d', 'npm', 'start']);
|
|
225
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'tmux');
|
|
226
|
+
assert.strictEqual(result.wrapperOptions.detached, true);
|
|
227
|
+
assert.strictEqual(result.command, 'npm start');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle mixed options and command', () => {
|
|
231
|
+
const result = parseArgs(['-i', 'screen', 'echo', 'hello']);
|
|
232
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'screen');
|
|
233
|
+
assert.strictEqual(result.command, 'echo hello');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('backend validation', () => {
|
|
238
|
+
it('should accept valid backends', () => {
|
|
239
|
+
for (const backend of VALID_BACKENDS) {
|
|
240
|
+
// Docker requires image, so handle it separately
|
|
241
|
+
if (backend === 'docker') {
|
|
242
|
+
const result = parseArgs([
|
|
243
|
+
'-i',
|
|
244
|
+
backend,
|
|
245
|
+
'--image',
|
|
246
|
+
'alpine',
|
|
247
|
+
'--',
|
|
248
|
+
'echo',
|
|
249
|
+
'test',
|
|
250
|
+
]);
|
|
251
|
+
assert.strictEqual(result.wrapperOptions.isolated, backend);
|
|
252
|
+
} else {
|
|
253
|
+
const result = parseArgs(['-i', backend, '--', 'echo', 'test']);
|
|
254
|
+
assert.strictEqual(result.wrapperOptions.isolated, backend);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should throw error for invalid backend', () => {
|
|
260
|
+
assert.throws(() => {
|
|
261
|
+
parseArgs(['--isolated', 'invalid-backend', '--', 'npm', 'test']);
|
|
262
|
+
}, /Invalid isolation backend/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should list valid backends in error message', () => {
|
|
266
|
+
try {
|
|
267
|
+
parseArgs(['--isolated', 'invalid', '--', 'npm', 'test']);
|
|
268
|
+
assert.fail('Should have thrown an error');
|
|
269
|
+
} catch (err) {
|
|
270
|
+
for (const backend of VALID_BACKENDS) {
|
|
271
|
+
assert.ok(
|
|
272
|
+
err.message.includes(backend),
|
|
273
|
+
`Error should mention ${backend}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('validateOptions', () => {
|
|
282
|
+
it('should pass for valid options', () => {
|
|
283
|
+
assert.doesNotThrow(() => {
|
|
284
|
+
validateOptions({
|
|
285
|
+
isolated: 'tmux',
|
|
286
|
+
attached: false,
|
|
287
|
+
detached: true,
|
|
288
|
+
session: 'test',
|
|
289
|
+
image: null,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should throw for attached and detached together', () => {
|
|
295
|
+
assert.throws(() => {
|
|
296
|
+
validateOptions({
|
|
297
|
+
isolated: 'screen',
|
|
298
|
+
attached: true,
|
|
299
|
+
detached: true,
|
|
300
|
+
session: null,
|
|
301
|
+
image: null,
|
|
302
|
+
});
|
|
303
|
+
}, /Cannot use both --attached and --detached/);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should pass for docker with image', () => {
|
|
307
|
+
assert.doesNotThrow(() => {
|
|
308
|
+
validateOptions({
|
|
309
|
+
isolated: 'docker',
|
|
310
|
+
attached: false,
|
|
311
|
+
detached: false,
|
|
312
|
+
session: null,
|
|
313
|
+
image: 'node:20',
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('generateSessionName', () => {
|
|
320
|
+
it('should generate unique session names', () => {
|
|
321
|
+
const name1 = generateSessionName();
|
|
322
|
+
const name2 = generateSessionName();
|
|
323
|
+
assert.notStrictEqual(name1, name2);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should use default prefix', () => {
|
|
327
|
+
const name = generateSessionName();
|
|
328
|
+
assert.ok(name.startsWith('start-'));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should use custom prefix', () => {
|
|
332
|
+
const name = generateSessionName('custom');
|
|
333
|
+
assert.ok(name.startsWith('custom-'));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should contain timestamp-like portion', () => {
|
|
337
|
+
const name = generateSessionName();
|
|
338
|
+
// Should have format: prefix-timestamp-random
|
|
339
|
+
const parts = name.split('-');
|
|
340
|
+
assert.ok(parts.length >= 3);
|
|
341
|
+
// Second part should be numeric (timestamp)
|
|
342
|
+
assert.ok(/^\d+$/.test(parts[1]));
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('hasIsolation', () => {
|
|
347
|
+
it('should return true when isolated is set', () => {
|
|
348
|
+
assert.strictEqual(hasIsolation({ isolated: 'tmux' }), true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should return false when isolated is null', () => {
|
|
352
|
+
assert.strictEqual(hasIsolation({ isolated: null }), false);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('getEffectiveMode', () => {
|
|
357
|
+
it('should return attached by default', () => {
|
|
358
|
+
const mode = getEffectiveMode({ attached: false, detached: false });
|
|
359
|
+
assert.strictEqual(mode, 'attached');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should return attached when explicitly set', () => {
|
|
363
|
+
const mode = getEffectiveMode({ attached: true, detached: false });
|
|
364
|
+
assert.strictEqual(mode, 'attached');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should return detached when set', () => {
|
|
368
|
+
const mode = getEffectiveMode({ attached: false, detached: true });
|
|
369
|
+
assert.strictEqual(mode, 'detached');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('VALID_BACKENDS', () => {
|
|
374
|
+
it('should include screen', () => {
|
|
375
|
+
assert.ok(VALID_BACKENDS.includes('screen'));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should include tmux', () => {
|
|
379
|
+
assert.ok(VALID_BACKENDS.includes('tmux'));
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should include docker', () => {
|
|
383
|
+
assert.ok(VALID_BACKENDS.includes('docker'));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should include zellij', () => {
|
|
387
|
+
assert.ok(VALID_BACKENDS.includes('zellij'));
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the isolation module
|
|
4
|
+
* Tests command availability checking and session name generation
|
|
5
|
+
* Note: Actual isolation execution tests are integration tests that require the tools to be installed
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { describe, it } = require('node:test');
|
|
9
|
+
const assert = require('assert');
|
|
10
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
11
|
+
|
|
12
|
+
describe('Isolation Module', () => {
|
|
13
|
+
describe('isCommandAvailable', () => {
|
|
14
|
+
it('should return true for common commands (echo)', () => {
|
|
15
|
+
// echo is available on all platforms
|
|
16
|
+
const result = isCommandAvailable('echo');
|
|
17
|
+
assert.strictEqual(result, true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return true for node', () => {
|
|
21
|
+
// node should be available since we are running tests with it
|
|
22
|
+
const result = isCommandAvailable('node');
|
|
23
|
+
assert.strictEqual(result, true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return false for non-existent command', () => {
|
|
27
|
+
const result = isCommandAvailable('nonexistent-command-12345');
|
|
28
|
+
assert.strictEqual(result, false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return false for empty command', () => {
|
|
32
|
+
const result = isCommandAvailable('');
|
|
33
|
+
assert.strictEqual(result, false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('isolation backend checks', () => {
|
|
38
|
+
// These tests check if specific backends are available
|
|
39
|
+
// They don't fail if not installed, just report status
|
|
40
|
+
|
|
41
|
+
it('should check if screen is available', () => {
|
|
42
|
+
const result = isCommandAvailable('screen');
|
|
43
|
+
console.log(` screen available: ${result}`);
|
|
44
|
+
assert.ok(typeof result === 'boolean');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should check if tmux is available', () => {
|
|
48
|
+
const result = isCommandAvailable('tmux');
|
|
49
|
+
console.log(` tmux available: ${result}`);
|
|
50
|
+
assert.ok(typeof result === 'boolean');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should check if docker is available', () => {
|
|
54
|
+
const result = isCommandAvailable('docker');
|
|
55
|
+
console.log(` docker available: ${result}`);
|
|
56
|
+
assert.ok(typeof result === 'boolean');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should check if zellij is available', () => {
|
|
60
|
+
const result = isCommandAvailable('zellij');
|
|
61
|
+
console.log(` zellij available: ${result}`);
|
|
62
|
+
assert.ok(typeof result === 'boolean');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Isolation Runner Error Handling', () => {
|
|
68
|
+
// These tests verify error messages when backends are not available
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
runInScreen,
|
|
72
|
+
runInTmux,
|
|
73
|
+
runInDocker,
|
|
74
|
+
runInZellij,
|
|
75
|
+
} = require('../src/lib/isolation');
|
|
76
|
+
|
|
77
|
+
describe('runInScreen', () => {
|
|
78
|
+
it('should return informative error if screen is not installed', async () => {
|
|
79
|
+
// Skip if screen is installed
|
|
80
|
+
if (isCommandAvailable('screen')) {
|
|
81
|
+
console.log(' Skipping: screen is installed');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await runInScreen('echo test', { detached: true });
|
|
86
|
+
assert.strictEqual(result.success, false);
|
|
87
|
+
assert.ok(result.message.includes('screen is not installed'));
|
|
88
|
+
assert.ok(
|
|
89
|
+
result.message.includes('apt-get') || result.message.includes('brew')
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('runInTmux', () => {
|
|
95
|
+
it('should return informative error if tmux is not installed', async () => {
|
|
96
|
+
// Skip if tmux is installed
|
|
97
|
+
if (isCommandAvailable('tmux')) {
|
|
98
|
+
console.log(' Skipping: tmux is installed');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await runInTmux('echo test', { detached: true });
|
|
103
|
+
assert.strictEqual(result.success, false);
|
|
104
|
+
assert.ok(result.message.includes('tmux is not installed'));
|
|
105
|
+
assert.ok(
|
|
106
|
+
result.message.includes('apt-get') || result.message.includes('brew')
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('runInDocker', () => {
|
|
112
|
+
it('should return informative error if docker is not installed', async () => {
|
|
113
|
+
// Skip if docker is installed
|
|
114
|
+
if (isCommandAvailable('docker')) {
|
|
115
|
+
console.log(' Skipping: docker is installed');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await runInDocker('echo test', {
|
|
120
|
+
image: 'alpine',
|
|
121
|
+
detached: true,
|
|
122
|
+
});
|
|
123
|
+
assert.strictEqual(result.success, false);
|
|
124
|
+
assert.ok(result.message.includes('docker is not installed'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should require image option', async () => {
|
|
128
|
+
// Skip if docker is not installed - the error will be about docker not being installed
|
|
129
|
+
if (!isCommandAvailable('docker')) {
|
|
130
|
+
console.log(' Skipping: docker not installed');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await runInDocker('echo test', { detached: true });
|
|
135
|
+
assert.strictEqual(result.success, false);
|
|
136
|
+
// Message should mention image requirement
|
|
137
|
+
assert.ok(
|
|
138
|
+
result.message.includes('image') ||
|
|
139
|
+
result.message.includes('--image') ||
|
|
140
|
+
result.message.includes('Docker isolation requires')
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('runInZellij', () => {
|
|
146
|
+
it('should return informative error if zellij is not installed', async () => {
|
|
147
|
+
// Skip if zellij is installed
|
|
148
|
+
if (isCommandAvailable('zellij')) {
|
|
149
|
+
console.log(' Skipping: zellij is installed');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await runInZellij('echo test', { detached: true });
|
|
154
|
+
assert.strictEqual(result.success, false);
|
|
155
|
+
assert.ok(result.message.includes('zellij is not installed'));
|
|
156
|
+
assert.ok(
|
|
157
|
+
result.message.includes('cargo') || result.message.includes('brew')
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Isolation Runner with Available Backends', () => {
|
|
164
|
+
// Integration-style tests that run if backends are available
|
|
165
|
+
// These test actual execution in detached mode (quick and non-blocking)
|
|
166
|
+
|
|
167
|
+
const {
|
|
168
|
+
runInScreen,
|
|
169
|
+
runInTmux,
|
|
170
|
+
runIsolated,
|
|
171
|
+
} = require('../src/lib/isolation');
|
|
172
|
+
const { execSync } = require('child_process');
|
|
173
|
+
|
|
174
|
+
describe('runInScreen (if available)', () => {
|
|
175
|
+
it('should run command in detached screen session', async () => {
|
|
176
|
+
if (!isCommandAvailable('screen')) {
|
|
177
|
+
console.log(' Skipping: screen not installed');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = await runInScreen('echo "test from screen"', {
|
|
182
|
+
session: `test-session-${Date.now()}`,
|
|
183
|
+
detached: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
assert.strictEqual(result.success, true);
|
|
187
|
+
assert.ok(result.sessionName);
|
|
188
|
+
assert.ok(result.message.includes('screen'));
|
|
189
|
+
assert.ok(result.message.includes('Reattach with'));
|
|
190
|
+
|
|
191
|
+
// Clean up the session
|
|
192
|
+
try {
|
|
193
|
+
execSync(`screen -S ${result.sessionName} -X quit`, {
|
|
194
|
+
stdio: 'ignore',
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
// Session may have already exited
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('runInTmux (if available)', () => {
|
|
203
|
+
it('should run command in detached tmux session', async () => {
|
|
204
|
+
if (!isCommandAvailable('tmux')) {
|
|
205
|
+
console.log(' Skipping: tmux not installed');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await runInTmux('echo "test from tmux"', {
|
|
210
|
+
session: `test-session-${Date.now()}`,
|
|
211
|
+
detached: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
assert.strictEqual(result.success, true);
|
|
215
|
+
assert.ok(result.sessionName);
|
|
216
|
+
assert.ok(result.message.includes('tmux'));
|
|
217
|
+
assert.ok(result.message.includes('Reattach with'));
|
|
218
|
+
|
|
219
|
+
// Clean up the session
|
|
220
|
+
try {
|
|
221
|
+
execSync(`tmux kill-session -t ${result.sessionName}`, {
|
|
222
|
+
stdio: 'ignore',
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
// Session may have already exited
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('runIsolated dispatcher', () => {
|
|
231
|
+
it('should dispatch to correct backend', async () => {
|
|
232
|
+
// Test with a backend that returns predictable error for missing tools
|
|
233
|
+
const result = await runIsolated('nonexistent-backend', 'echo test', {});
|
|
234
|
+
assert.strictEqual(result.success, false);
|
|
235
|
+
assert.ok(result.message.includes('Unknown isolation backend'));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should pass options to backend', async () => {
|
|
239
|
+
// Test docker without image - should fail with specific error
|
|
240
|
+
const result = await runIsolated('docker', 'echo test', {});
|
|
241
|
+
assert.strictEqual(result.success, false);
|
|
242
|
+
// Either docker not installed or image required
|
|
243
|
+
assert.ok(
|
|
244
|
+
result.message.includes('docker') || result.message.includes('image')
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|