start-command 0.25.5 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/README.md +2 -1
- package/bunfig.toml +2 -2
- package/eslint.config.mjs +1 -1
- package/package.json +2 -2
- package/src/bin/cli.js +48 -67
- package/src/lib/args-parser.js +88 -7
- package/src/lib/execution-control.js +317 -0
- package/src/lib/isolation.js +22 -4
- package/src/lib/status-formatter.js +153 -1
- package/src/lib/usage.js +74 -0
- package/test/args-parser-control.js +71 -0
- package/test/{args-parser.test.js → args-parser.js} +28 -4
- package/test/cli.js +260 -0
- package/test/docker-autoremove.js +175 -0
- package/test/execution-control.js +253 -0
- package/test/{isolation.test.js → isolation.js} +4 -2
- package/test/merge-changesets.mjs +154 -0
- package/test/release-name.mjs +117 -0
- package/test/{screen-integration.test.js → screen-integration.js} +1 -1
- package/test/{ssh-integration.test.js → ssh-integration.js} +1 -1
- package/test/{status-query.test.js → status-query.js} +73 -0
- package/test/{substitution.test.js → substitution.js} +1 -2
- package/test/{user-manager.test.js → user-manager.js} +17 -0
- package/test/cli.test.js +0 -218
- package/test/docker-autoremove.test.js +0 -164
- package/test/release-name.test.mjs +0 -34
- /package/test/{args-parser-shell.test.js → args-parser-shell.js} +0 -0
- /package/test/{echo-integration.test.js → echo-integration.js} +0 -0
- /package/test/{execution-store.test.js → execution-store.js} +0 -0
- /package/test/{failure-handler.test.js → failure-handler.js} +0 -0
- /package/test/{isolation-cleanup.test.js → isolation-cleanup.js} +0 -0
- /package/test/{isolation-log-utils.test.js → isolation-log-utils.js} +0 -0
- /package/test/{isolation-stacking.test.js → isolation-stacking.js} +0 -0
- /package/test/{output-blocks.test.js → output-blocks.js} +0 -0
- /package/test/{public-exports.test.js → public-exports.js} +0 -0
- /package/test/{regression-84.test.js → regression-84.js} +0 -0
- /package/test/{regression-89.test.js → regression-89.js} +0 -0
- /package/test/{regression-91.test.js → regression-91.js} +0 -0
- /package/test/{sequence-parser.test.js → sequence-parser.js} +0 -0
- /package/test/{session-name-status.test.js → session-name-status.js} +0 -0
- /package/test/{version.test.js → version.js} +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { mergeChangesetsIn } from '../../scripts/merge-changesets.mjs';
|
|
15
|
+
|
|
16
|
+
function makeTempPackage(packageName) {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), 'merge-changesets-'));
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(dir, 'package.json'),
|
|
20
|
+
JSON.stringify({ name: packageName, version: '0.0.0' })
|
|
21
|
+
);
|
|
22
|
+
mkdirSync(join(dir, '.changeset'));
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeChangeset(dir, fileName, body) {
|
|
27
|
+
writeFileSync(join(dir, '.changeset', fileName), body);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('mergeChangesetsIn', () => {
|
|
31
|
+
let tmpDir;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpDir = makeTempPackage('start-command');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
39
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does nothing with zero changesets', () => {
|
|
44
|
+
const result = mergeChangesetsIn(tmpDir);
|
|
45
|
+
expect(result.merged).toBe(false);
|
|
46
|
+
expect(readdirSync(join(tmpDir, '.changeset'))).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does nothing with a single changeset', () => {
|
|
50
|
+
writeChangeset(
|
|
51
|
+
tmpDir,
|
|
52
|
+
'lone.md',
|
|
53
|
+
`---\n'start-command': patch\n---\n\nOnly one\n`
|
|
54
|
+
);
|
|
55
|
+
const result = mergeChangesetsIn(tmpDir);
|
|
56
|
+
expect(result.merged).toBe(false);
|
|
57
|
+
expect(readdirSync(join(tmpDir, '.changeset'))).toEqual(['lone.md']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('merges multiple changesets into a single file with the highest bump', () => {
|
|
61
|
+
writeChangeset(
|
|
62
|
+
tmpDir,
|
|
63
|
+
'a.md',
|
|
64
|
+
`---\n'start-command': patch\n---\n\nFix A\n`
|
|
65
|
+
);
|
|
66
|
+
writeChangeset(
|
|
67
|
+
tmpDir,
|
|
68
|
+
'b.md',
|
|
69
|
+
`---\n'start-command': minor\n---\n\nFeature B\n`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const result = mergeChangesetsIn(tmpDir);
|
|
73
|
+
expect(result.merged).toBe(true);
|
|
74
|
+
expect(result.bumpType).toBe('minor');
|
|
75
|
+
|
|
76
|
+
const files = readdirSync(join(tmpDir, '.changeset'));
|
|
77
|
+
expect(files.length).toBe(1);
|
|
78
|
+
const mergedFile = files[0];
|
|
79
|
+
expect(mergedFile.startsWith('merged-')).toBe(true);
|
|
80
|
+
|
|
81
|
+
const content = readFileSync(
|
|
82
|
+
join(tmpDir, '.changeset', mergedFile),
|
|
83
|
+
'utf8'
|
|
84
|
+
);
|
|
85
|
+
expect(content).toContain("'start-command': minor");
|
|
86
|
+
expect(content).toContain('Fix A');
|
|
87
|
+
expect(content).toContain('Feature B');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('chooses major over minor and patch', () => {
|
|
91
|
+
writeChangeset(tmpDir, 'a.md', `---\n'start-command': patch\n---\n\nFix\n`);
|
|
92
|
+
writeChangeset(
|
|
93
|
+
tmpDir,
|
|
94
|
+
'b.md',
|
|
95
|
+
`---\n'start-command': major\n---\n\nBreaking\n`
|
|
96
|
+
);
|
|
97
|
+
writeChangeset(
|
|
98
|
+
tmpDir,
|
|
99
|
+
'c.md',
|
|
100
|
+
`---\n'start-command': minor\n---\n\nFeat\n`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const result = mergeChangesetsIn(tmpDir);
|
|
104
|
+
expect(result.bumpType).toBe('major');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('reads package name from package.json (no hardcoded placeholder)', () => {
|
|
108
|
+
const otherDir = mkdtempSync(join(tmpdir(), 'merge-changesets-other-'));
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(
|
|
111
|
+
join(otherDir, 'package.json'),
|
|
112
|
+
JSON.stringify({ name: 'some-other-pkg', version: '1.0.0' })
|
|
113
|
+
);
|
|
114
|
+
mkdirSync(join(otherDir, '.changeset'));
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(otherDir, '.changeset', 'a.md'),
|
|
117
|
+
`---\n'some-other-pkg': minor\n---\n\nA\n`
|
|
118
|
+
);
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(otherDir, '.changeset', 'b.md'),
|
|
121
|
+
`---\n'some-other-pkg': patch\n---\n\nB\n`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const result = mergeChangesetsIn(otherDir);
|
|
125
|
+
expect(result.merged).toBe(true);
|
|
126
|
+
|
|
127
|
+
const files = readdirSync(join(otherDir, '.changeset'));
|
|
128
|
+
const merged = readFileSync(
|
|
129
|
+
join(otherDir, '.changeset', files[0]),
|
|
130
|
+
'utf8'
|
|
131
|
+
);
|
|
132
|
+
expect(merged).toContain("'some-other-pkg': minor");
|
|
133
|
+
} finally {
|
|
134
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('throws when .changeset directory is missing', () => {
|
|
139
|
+
const noChangesetDir = mkdtempSync(
|
|
140
|
+
join(tmpdir(), 'merge-changesets-empty-')
|
|
141
|
+
);
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(
|
|
144
|
+
join(noChangesetDir, 'package.json'),
|
|
145
|
+
JSON.stringify({ name: 'x', version: '0.0.0' })
|
|
146
|
+
);
|
|
147
|
+
expect(() => mergeChangesetsIn(noChangesetDir)).toThrow(
|
|
148
|
+
/Changeset directory not found/
|
|
149
|
+
);
|
|
150
|
+
} finally {
|
|
151
|
+
rmSync(noChangesetDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
encodeShieldsStaticBadgeSegment,
|
|
4
|
+
extractChangelogEntry,
|
|
5
|
+
normalizeReleaseVersionForBadge,
|
|
6
|
+
packageVersionBadge,
|
|
7
|
+
releaseName,
|
|
8
|
+
releaseTag,
|
|
9
|
+
} from '../../scripts/release-name.mjs';
|
|
10
|
+
|
|
11
|
+
describe('releaseTag', () => {
|
|
12
|
+
it('uses plain "v${version}" when no prefix is given', () => {
|
|
13
|
+
expect(releaseTag('0.25.4')).toBe('v0.25.4');
|
|
14
|
+
expect(releaseTag('0.25.4', '')).toBe('v0.25.4');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('prepends known language prefixes', () => {
|
|
18
|
+
expect(releaseTag('0.25.4', 'js-')).toBe('js-v0.25.4');
|
|
19
|
+
expect(releaseTag('0.14.0', 'rust-')).toBe('rust-v0.14.0');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('passes arbitrary prefixes through', () => {
|
|
23
|
+
expect(releaseTag('1.0.0', 'api-')).toBe('api-v1.0.0');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('releaseName', () => {
|
|
28
|
+
it('returns bare version when prefix is empty (preserves pre-issue-108 behaviour)', () => {
|
|
29
|
+
expect(releaseName('0.25.4')).toBe('0.25.4');
|
|
30
|
+
expect(releaseName('0.25.4', '')).toBe('0.25.4');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('decorates known language prefixes with human titles', () => {
|
|
34
|
+
expect(releaseName('0.25.4', 'js-')).toBe('[JavaScript] 0.25.4');
|
|
35
|
+
expect(releaseName('0.14.0', 'rust-')).toBe('[Rust] 0.14.0');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('falls back to "${prefix}${version}" for unknown prefixes', () => {
|
|
39
|
+
expect(releaseName('1.0.0', 'api-')).toBe('api-1.0.0');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('release badge helpers', () => {
|
|
44
|
+
it('strips plain and language-prefixed v tags before building badge versions', () => {
|
|
45
|
+
expect(normalizeReleaseVersionForBadge('v1.2.3')).toBe('1.2.3');
|
|
46
|
+
expect(normalizeReleaseVersionForBadge('js-v1.2.3')).toBe('1.2.3');
|
|
47
|
+
expect(normalizeReleaseVersionForBadge('rust-v0.14.1')).toBe('0.14.1');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('escapes shields.io static badge path delimiters in prerelease versions', () => {
|
|
51
|
+
expect(encodeShieldsStaticBadgeSegment('1.0.0-alpha_1')).toBe(
|
|
52
|
+
'1.0.0--alpha__1'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('builds an exact npm version badge without leaking the js tag prefix', () => {
|
|
57
|
+
const badge = packageVersionBadge({
|
|
58
|
+
packageType: 'npm',
|
|
59
|
+
packageName: 'start-command',
|
|
60
|
+
releaseVersion: 'js-v1.2.3',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(badge).toContain('/badge/npm-1.2.3-blue.svg');
|
|
64
|
+
expect(badge).not.toContain('/badge/npm-js-v1.2.3-blue.svg');
|
|
65
|
+
expect(badge).toContain('/start-command/v/1.2.3');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('builds an exact crates.io version badge without leaking the rust tag prefix', () => {
|
|
69
|
+
const badge = packageVersionBadge({
|
|
70
|
+
packageType: 'crates',
|
|
71
|
+
packageName: 'start-command',
|
|
72
|
+
releaseVersion: 'rust-v0.14.1',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(badge).toContain('/badge/crates.io-0.14.1-orange.svg');
|
|
76
|
+
expect(badge).not.toContain('/badge/crates.io-rust-v0.14.1-orange.svg');
|
|
77
|
+
expect(badge).toContain('/crates/start-command/0.14.1');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('extractChangelogEntry', () => {
|
|
82
|
+
it('extracts a Changesets-style JavaScript entry', () => {
|
|
83
|
+
const changelog = `# start-command
|
|
84
|
+
|
|
85
|
+
## 1.2.3
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- Fix release badges.
|
|
90
|
+
|
|
91
|
+
## 1.2.2
|
|
92
|
+
|
|
93
|
+
- Previous change.
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
expect(extractChangelogEntry(changelog, 'js-v1.2.3')).toBe(
|
|
97
|
+
'### Patch Changes\n\n- Fix release badges.'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('extracts a Keep-a-Changelog-style Rust entry', () => {
|
|
102
|
+
const changelog = `# Changelog
|
|
103
|
+
|
|
104
|
+
## [0.14.1] - 2026-05-02
|
|
105
|
+
|
|
106
|
+
- Fix Rust release automation.
|
|
107
|
+
|
|
108
|
+
## [0.14.0] - 2026-04-24
|
|
109
|
+
|
|
110
|
+
- Previous change.
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
expect(extractChangelogEntry(changelog, 'rust-v0.14.1')).toBe(
|
|
114
|
+
'- Fix Rust release automation.'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Integration tests for screen isolation
|
|
4
4
|
* Tests actual screen session behavior including output capture, exit codes, and edge cases.
|
|
5
|
-
* Extracted from isolation.
|
|
5
|
+
* Extracted from isolation.js to keep file sizes under the 1000-line limit.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { describe, it } = require('node:test');
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* To run locally:
|
|
10
10
|
* 1. Ensure SSH server is running
|
|
11
11
|
* 2. Set up passwordless SSH to localhost (ssh-keygen, ssh-copy-id localhost)
|
|
12
|
-
* 3. Run: bun test test/ssh-integration.
|
|
12
|
+
* 3. Run: bun run test test/ssh-integration.js
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const { describe, it, before } = require('node:test');
|
|
@@ -123,6 +123,7 @@ describe('--status query functionality', () => {
|
|
|
123
123
|
expect(parsed.status).toBe('executed');
|
|
124
124
|
expect(parsed.exitCode).toBe(0);
|
|
125
125
|
expect(parsed.pid).toBe(12345);
|
|
126
|
+
expect(parsed.processIds).toEqual({ wrapperPid: 12345 });
|
|
126
127
|
});
|
|
127
128
|
});
|
|
128
129
|
|
|
@@ -168,6 +169,78 @@ describe('--status query functionality', () => {
|
|
|
168
169
|
});
|
|
169
170
|
});
|
|
170
171
|
|
|
172
|
+
describe('--list query functionality', () => {
|
|
173
|
+
it('should output all tracked executions in links-notation format by default', () => {
|
|
174
|
+
const executingRecord = new ExecutionRecord({
|
|
175
|
+
command: 'sleep 100',
|
|
176
|
+
pid: 99999,
|
|
177
|
+
logPath: '/tmp/executing.log',
|
|
178
|
+
startTime: '2026-04-24T10:00:00.000Z',
|
|
179
|
+
});
|
|
180
|
+
store.save(executingRecord);
|
|
181
|
+
|
|
182
|
+
const result = runCli(['--list']);
|
|
183
|
+
|
|
184
|
+
expect(result.exitCode).toBe(0);
|
|
185
|
+
expect(result.stdout).toMatch(/^executions\n/);
|
|
186
|
+
expect(result.stdout).toContain(' count 2');
|
|
187
|
+
expect(result.stdout).toContain(` ${testRecord.uuid}`);
|
|
188
|
+
expect(result.stdout).toContain(` ${executingRecord.uuid}`);
|
|
189
|
+
expect(result.stdout).toContain(' command "echo hello world"');
|
|
190
|
+
expect(result.stdout).toContain(' command "sleep 100"');
|
|
191
|
+
expect(result.stdout).toContain(' status executed');
|
|
192
|
+
expect(result.stdout).toContain(' status executing');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should output all tracked executions in JSON format', () => {
|
|
196
|
+
const executingRecord = new ExecutionRecord({
|
|
197
|
+
command: 'sleep 100',
|
|
198
|
+
pid: 99999,
|
|
199
|
+
logPath: '/tmp/executing.log',
|
|
200
|
+
});
|
|
201
|
+
store.save(executingRecord);
|
|
202
|
+
|
|
203
|
+
const result = runCli(['--list', '--output-format', 'json']);
|
|
204
|
+
|
|
205
|
+
expect(result.exitCode).toBe(0);
|
|
206
|
+
const parsed = JSON.parse(result.stdout);
|
|
207
|
+
expect(parsed.count).toBe(2);
|
|
208
|
+
expect(parsed.executions.length).toBe(2);
|
|
209
|
+
expect(parsed.executions.map((record) => record.uuid)).toContain(
|
|
210
|
+
testRecord.uuid
|
|
211
|
+
);
|
|
212
|
+
expect(parsed.executions.map((record) => record.uuid)).toContain(
|
|
213
|
+
executingRecord.uuid
|
|
214
|
+
);
|
|
215
|
+
const executing = parsed.executions.find(
|
|
216
|
+
(record) => record.uuid === executingRecord.uuid
|
|
217
|
+
);
|
|
218
|
+
expect(executing.status).toBe('executing');
|
|
219
|
+
expect(executing.processIds).toEqual({ wrapperPid: 99999 });
|
|
220
|
+
expect(executing.currentTime).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should show count zero when no records are stored', () => {
|
|
224
|
+
cleanupTestDir();
|
|
225
|
+
|
|
226
|
+
const result = runCli(['--list']);
|
|
227
|
+
|
|
228
|
+
expect(result.exitCode).toBe(0);
|
|
229
|
+
expect(result.stdout).toMatch(/^executions\n/);
|
|
230
|
+
expect(result.stdout).toContain(' count 0');
|
|
231
|
+
expect(result.stdout).toContain(' records ()');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should show error when tracking is disabled', () => {
|
|
235
|
+
const result = runCli(['--list'], {
|
|
236
|
+
START_DISABLE_TRACKING: '1',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.exitCode).toBe(1);
|
|
240
|
+
expect(result.stderr).toContain('tracking is disabled');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
171
244
|
describe('executing status', () => {
|
|
172
245
|
it('should show executing status for ongoing commands', () => {
|
|
173
246
|
// Create an executing (not completed) record
|
|
@@ -225,11 +225,10 @@ function runTests() {
|
|
|
225
225
|
console.log(` Expected: "${test.expected}"`);
|
|
226
226
|
console.log(` Got: "${actual}"`);
|
|
227
227
|
});
|
|
228
|
-
|
|
228
|
+
throw new Error(`${failed} substitution tests failed`);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
console.log('\n✓ All tests passed!\n');
|
|
232
|
-
process.exit(0);
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
// Run tests
|
|
@@ -17,6 +17,8 @@ const {
|
|
|
17
17
|
getUserInfo,
|
|
18
18
|
} = require('../src/lib/user-manager');
|
|
19
19
|
|
|
20
|
+
const isWindows = process.platform === 'win32';
|
|
21
|
+
|
|
20
22
|
describe('user-manager', () => {
|
|
21
23
|
describe('getCurrentUser', () => {
|
|
22
24
|
it('should return a non-empty string', () => {
|
|
@@ -27,6 +29,11 @@ describe('user-manager', () => {
|
|
|
27
29
|
|
|
28
30
|
it('should return a valid username format', () => {
|
|
29
31
|
const user = getCurrentUser();
|
|
32
|
+
if (isWindows) {
|
|
33
|
+
assert.ok(!/[\r\n]/.test(user));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
// Username should contain only valid characters
|
|
31
38
|
assert.ok(/^[a-zA-Z0-9_-]+$/.test(user));
|
|
32
39
|
});
|
|
@@ -55,6 +62,11 @@ describe('user-manager', () => {
|
|
|
55
62
|
describe('userExists', () => {
|
|
56
63
|
it('should return true for current user', () => {
|
|
57
64
|
const currentUser = getCurrentUser();
|
|
65
|
+
if (isWindows) {
|
|
66
|
+
assert.strictEqual(typeof userExists(currentUser), 'boolean');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
assert.strictEqual(userExists(currentUser), true);
|
|
59
71
|
});
|
|
60
72
|
|
|
@@ -130,6 +142,11 @@ describe('user-manager', () => {
|
|
|
130
142
|
it('should return user info for current user', () => {
|
|
131
143
|
const currentUser = getCurrentUser();
|
|
132
144
|
const info = getUserInfo(currentUser);
|
|
145
|
+
if (isWindows) {
|
|
146
|
+
assert.strictEqual(typeof info.exists, 'boolean');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
assert.strictEqual(info.exists, true);
|
|
134
151
|
});
|
|
135
152
|
|
package/test/cli.test.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the CLI
|
|
4
|
-
* Tests version flag and basic CLI behavior
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { describe, it } = require('node:test');
|
|
8
|
-
const assert = require('assert');
|
|
9
|
-
const { spawnSync } = require('child_process');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
|
|
13
|
-
// Path to the CLI script
|
|
14
|
-
const CLI_PATH = path.join(__dirname, '../src/bin/cli.js');
|
|
15
|
-
|
|
16
|
-
// Timeout for CLI operations - longer on Windows due to cold-start latency
|
|
17
|
-
const CLI_TIMEOUT = process.platform === 'win32' ? 30000 : 10000;
|
|
18
|
-
|
|
19
|
-
// Helper to run CLI with timeout
|
|
20
|
-
function runCLI(args = []) {
|
|
21
|
-
return spawnSync('bun', [CLI_PATH, ...args], {
|
|
22
|
-
encoding: 'utf8',
|
|
23
|
-
timeout: CLI_TIMEOUT,
|
|
24
|
-
env: {
|
|
25
|
-
...process.env,
|
|
26
|
-
START_DISABLE_AUTO_ISSUE: '1',
|
|
27
|
-
START_DISABLE_LOG_UPLOAD: '1',
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('CLI version flag', () => {
|
|
33
|
-
it('should display version with --version', () => {
|
|
34
|
-
const result = runCLI(['--version']);
|
|
35
|
-
|
|
36
|
-
// Check if process was killed (e.g., due to timeout)
|
|
37
|
-
assert.notStrictEqual(
|
|
38
|
-
result.status,
|
|
39
|
-
null,
|
|
40
|
-
`Process should complete (was killed with signal: ${result.signal})`
|
|
41
|
-
);
|
|
42
|
-
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
43
|
-
|
|
44
|
-
// Check for key elements in version output
|
|
45
|
-
assert.ok(
|
|
46
|
-
result.stdout.includes('start-command version:'),
|
|
47
|
-
'Should display start-command version'
|
|
48
|
-
);
|
|
49
|
-
assert.ok(result.stdout.includes('OS:'), 'Should display OS');
|
|
50
|
-
assert.ok(
|
|
51
|
-
result.stdout.includes('OS Version:'),
|
|
52
|
-
'Should display OS Version'
|
|
53
|
-
);
|
|
54
|
-
// Check for either Bun or Node.js version depending on runtime
|
|
55
|
-
const hasBunVersion = result.stdout.includes('Bun Version:');
|
|
56
|
-
const hasNodeVersion = result.stdout.includes('Node.js Version:');
|
|
57
|
-
assert.ok(
|
|
58
|
-
hasBunVersion || hasNodeVersion,
|
|
59
|
-
'Should display Bun Version or Node.js Version'
|
|
60
|
-
);
|
|
61
|
-
assert.ok(
|
|
62
|
-
result.stdout.includes('Architecture:'),
|
|
63
|
-
'Should display Architecture'
|
|
64
|
-
);
|
|
65
|
-
assert.ok(
|
|
66
|
-
result.stdout.includes('Isolation tools:'),
|
|
67
|
-
'Should display Isolation tools section'
|
|
68
|
-
);
|
|
69
|
-
assert.ok(
|
|
70
|
-
result.stdout.includes('screen:'),
|
|
71
|
-
'Should check for screen installation'
|
|
72
|
-
);
|
|
73
|
-
assert.ok(
|
|
74
|
-
result.stdout.includes('tmux:'),
|
|
75
|
-
'Should check for tmux installation'
|
|
76
|
-
);
|
|
77
|
-
assert.ok(
|
|
78
|
-
result.stdout.includes('docker:'),
|
|
79
|
-
'Should check for docker installation'
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should display version with -v', () => {
|
|
84
|
-
const result = runCLI(['-v']);
|
|
85
|
-
|
|
86
|
-
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
87
|
-
assert.ok(
|
|
88
|
-
result.stdout.includes('start-command version:'),
|
|
89
|
-
'Should display start-command version'
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should show correct package version', () => {
|
|
94
|
-
const result = runCLI(['--version']);
|
|
95
|
-
const packageJson = JSON.parse(
|
|
96
|
-
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
assert.ok(
|
|
100
|
-
result.stdout.includes(`start-command version: ${packageJson.version}`),
|
|
101
|
-
`Should display version ${packageJson.version}`
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('CLI basic behavior', () => {
|
|
107
|
-
it('should show usage when no arguments provided', () => {
|
|
108
|
-
const result = runCLI([]);
|
|
109
|
-
|
|
110
|
-
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
111
|
-
assert.ok(result.stdout.includes('Usage:'), 'Should display usage');
|
|
112
|
-
assert.ok(
|
|
113
|
-
result.stdout.includes('--version'),
|
|
114
|
-
'Usage should mention --version flag'
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should show usage when no command provided after --', () => {
|
|
119
|
-
const result = runCLI(['--']);
|
|
120
|
-
|
|
121
|
-
assert.strictEqual(result.status, 0, 'Exit code should be 0');
|
|
122
|
-
assert.ok(result.stdout.includes('Usage:'), 'Should display usage');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('CLI isolation output (issue #67)', () => {
|
|
127
|
-
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
128
|
-
|
|
129
|
-
it('should display screen session name when using screen isolation', async () => {
|
|
130
|
-
if (!isCommandAvailable('screen')) {
|
|
131
|
-
console.log(' Skipping: screen not installed');
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const result = runCLI(['-i', 'screen', '--', 'echo', 'hello']);
|
|
136
|
-
|
|
137
|
-
// The output should contain the screen session name (in format screen-timestamp-random)
|
|
138
|
-
// Check that the session UUID is displayed
|
|
139
|
-
assert.ok(
|
|
140
|
-
result.stdout.includes('│ session'),
|
|
141
|
-
'Should display session UUID'
|
|
142
|
-
);
|
|
143
|
-
// Check that screen isolation info is displayed
|
|
144
|
-
assert.ok(
|
|
145
|
-
result.stdout.includes('│ isolation screen'),
|
|
146
|
-
'Should display screen isolation'
|
|
147
|
-
);
|
|
148
|
-
// Check that the actual screen session name is displayed (issue #67 fix)
|
|
149
|
-
assert.ok(
|
|
150
|
-
result.stdout.includes('│ screen screen-'),
|
|
151
|
-
'Should display actual screen session name for reconnection (issue #67)'
|
|
152
|
-
);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should display tmux session name when using tmux isolation', async () => {
|
|
156
|
-
if (!isCommandAvailable('tmux')) {
|
|
157
|
-
console.log(' Skipping: tmux not installed');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const result = runCLI(['-i', 'tmux', '--', 'echo', 'hello']);
|
|
162
|
-
|
|
163
|
-
// The output should contain the tmux session name
|
|
164
|
-
assert.ok(
|
|
165
|
-
result.stdout.includes('│ session'),
|
|
166
|
-
'Should display session UUID'
|
|
167
|
-
);
|
|
168
|
-
assert.ok(
|
|
169
|
-
result.stdout.includes('│ isolation tmux'),
|
|
170
|
-
'Should display tmux isolation'
|
|
171
|
-
);
|
|
172
|
-
// Check that the actual tmux session name is displayed (issue #67 fix)
|
|
173
|
-
assert.ok(
|
|
174
|
-
result.stdout.includes('│ tmux tmux-'),
|
|
175
|
-
'Should display actual tmux session name for reconnection (issue #67)'
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should display docker container name when using docker isolation', async () => {
|
|
180
|
-
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
181
|
-
|
|
182
|
-
if (!canRunLinuxDockerImages()) {
|
|
183
|
-
console.log(
|
|
184
|
-
' Skipping: docker not available or cannot run Linux images'
|
|
185
|
-
);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const result = runCLI([
|
|
190
|
-
'-i',
|
|
191
|
-
'docker',
|
|
192
|
-
'--image',
|
|
193
|
-
'alpine:latest',
|
|
194
|
-
'--',
|
|
195
|
-
'echo',
|
|
196
|
-
'hello',
|
|
197
|
-
]);
|
|
198
|
-
|
|
199
|
-
// The output should contain the docker container name
|
|
200
|
-
assert.ok(
|
|
201
|
-
result.stdout.includes('│ session'),
|
|
202
|
-
'Should display session UUID'
|
|
203
|
-
);
|
|
204
|
-
assert.ok(
|
|
205
|
-
result.stdout.includes('│ isolation docker'),
|
|
206
|
-
'Should display docker isolation'
|
|
207
|
-
);
|
|
208
|
-
assert.ok(
|
|
209
|
-
result.stdout.includes('│ image alpine:latest'),
|
|
210
|
-
'Should display docker image'
|
|
211
|
-
);
|
|
212
|
-
// Check that the actual container name is displayed (issue #67 fix)
|
|
213
|
-
assert.ok(
|
|
214
|
-
result.stdout.includes('│ container docker-'),
|
|
215
|
-
'Should display actual container name for reconnection (issue #67)'
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
});
|