start-command 0.26.0 → 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 +11 -1
- package/README.md +1 -1
- package/bunfig.toml +2 -2
- package/eslint.config.mjs +1 -1
- package/package.json +2 -2
- package/src/bin/cli.js +30 -0
- package/src/lib/args-parser.js +72 -6
- package/src/lib/execution-control.js +317 -0
- package/src/lib/isolation.js +22 -4
- package/src/lib/status-formatter.js +46 -2
- package/src/lib/usage.js +5 -1
- package/test/args-parser-control.js +71 -0
- package/test/{args-parser.test.js → args-parser.js} +1 -1
- 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} +2 -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,253 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for detached execution control helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
ControlAction,
|
|
14
|
+
collectProcessIds,
|
|
15
|
+
controlExecution,
|
|
16
|
+
getControlCommand,
|
|
17
|
+
parseScreenPid,
|
|
18
|
+
} = require('../src/lib/execution-control');
|
|
19
|
+
const {
|
|
20
|
+
ExecutionRecord,
|
|
21
|
+
ExecutionStatus,
|
|
22
|
+
ExecutionStore,
|
|
23
|
+
} = require('../src/lib/execution-store');
|
|
24
|
+
|
|
25
|
+
const TEST_APP_FOLDER = path.join(
|
|
26
|
+
os.tmpdir(),
|
|
27
|
+
`execution-control-test-${Date.now()}`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function cleanupTestDir() {
|
|
31
|
+
if (fs.existsSync(TEST_APP_FOLDER)) {
|
|
32
|
+
fs.rmSync(TEST_APP_FOLDER, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createStore() {
|
|
37
|
+
return new ExecutionStore({
|
|
38
|
+
appFolder: TEST_APP_FOLDER,
|
|
39
|
+
useLinks: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createDetachedRecord(overrides = {}) {
|
|
44
|
+
return new ExecutionRecord({
|
|
45
|
+
uuid: 'control-test-uuid',
|
|
46
|
+
command: 'sleep 100',
|
|
47
|
+
pid: 12345,
|
|
48
|
+
logPath: '/tmp/control-test.log',
|
|
49
|
+
status: ExecutionStatus.EXECUTING,
|
|
50
|
+
options: {
|
|
51
|
+
isolated: 'screen',
|
|
52
|
+
isolationMode: 'detached',
|
|
53
|
+
sessionName: 'screen-session',
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createRunner(responses = {}) {
|
|
60
|
+
const calls = [];
|
|
61
|
+
const runner = (command, args) => {
|
|
62
|
+
calls.push({ command, args });
|
|
63
|
+
const key = `${command} ${args.join(' ')}`;
|
|
64
|
+
const response = responses[key] || responses[command];
|
|
65
|
+
if (typeof response === 'function') {
|
|
66
|
+
return response(command, args);
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
response || {
|
|
70
|
+
success: true,
|
|
71
|
+
stdout: '',
|
|
72
|
+
stderr: '',
|
|
73
|
+
status: 0,
|
|
74
|
+
error: null,
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
runner.calls = calls;
|
|
79
|
+
return runner;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('execution control', () => {
|
|
83
|
+
beforeEach(cleanupTestDir);
|
|
84
|
+
afterEach(cleanupTestDir);
|
|
85
|
+
|
|
86
|
+
it('should map screen stop to CTRL+C injection', () => {
|
|
87
|
+
const record = createDetachedRecord();
|
|
88
|
+
const command = getControlCommand(record, ControlAction.STOP);
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(command.command, 'screen');
|
|
91
|
+
assert.deepStrictEqual(command.args, [
|
|
92
|
+
'-S',
|
|
93
|
+
'screen-session',
|
|
94
|
+
'-X',
|
|
95
|
+
'stuff',
|
|
96
|
+
'\x03',
|
|
97
|
+
]);
|
|
98
|
+
assert.strictEqual(command.method, 'CTRL_C');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should send stop command to a detached screen session', () => {
|
|
102
|
+
const store = createStore();
|
|
103
|
+
store.save(createDetachedRecord());
|
|
104
|
+
const runner = createRunner({
|
|
105
|
+
'screen -ls': {
|
|
106
|
+
success: true,
|
|
107
|
+
stdout: '\t111.screen-session\t(Detached)\n',
|
|
108
|
+
stderr: '',
|
|
109
|
+
status: 0,
|
|
110
|
+
error: null,
|
|
111
|
+
},
|
|
112
|
+
'pgrep -P 111': {
|
|
113
|
+
success: true,
|
|
114
|
+
stdout: '222\n',
|
|
115
|
+
stderr: '',
|
|
116
|
+
status: 0,
|
|
117
|
+
error: null,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = controlExecution(
|
|
122
|
+
store,
|
|
123
|
+
'screen-session',
|
|
124
|
+
ControlAction.STOP,
|
|
125
|
+
runner
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assert.strictEqual(result.success, true);
|
|
129
|
+
assert.match(result.output, /executionControl/);
|
|
130
|
+
assert.match(result.output, /action stop/);
|
|
131
|
+
assert.match(result.output, /method CTRL_C/);
|
|
132
|
+
assert.match(result.output, /screenPid 111/);
|
|
133
|
+
assert.deepStrictEqual(runner.calls[0], {
|
|
134
|
+
command: 'screen',
|
|
135
|
+
args: ['-S', 'screen-session', '-X', 'stuff', '\x03'],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should send docker terminate through docker kill', () => {
|
|
140
|
+
const store = createStore();
|
|
141
|
+
store.save(
|
|
142
|
+
createDetachedRecord({
|
|
143
|
+
uuid: 'docker-control-uuid',
|
|
144
|
+
options: {
|
|
145
|
+
isolated: 'docker',
|
|
146
|
+
isolationMode: 'detached',
|
|
147
|
+
sessionName: 'docker-session',
|
|
148
|
+
containerId: 'abc123',
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
const runner = createRunner({
|
|
153
|
+
'docker inspect -f {{.Id}} {{.State.Pid}} docker-session': {
|
|
154
|
+
success: true,
|
|
155
|
+
stdout: 'abcdef 444\n',
|
|
156
|
+
stderr: '',
|
|
157
|
+
status: 0,
|
|
158
|
+
error: null,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = controlExecution(
|
|
163
|
+
store,
|
|
164
|
+
'docker-control-uuid',
|
|
165
|
+
ControlAction.TERMINATE,
|
|
166
|
+
runner
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
assert.strictEqual(result.success, true);
|
|
170
|
+
assert.match(result.output, /action terminate/);
|
|
171
|
+
assert.match(result.output, /method SIGKILL/);
|
|
172
|
+
assert.match(result.output, /containerPid 444/);
|
|
173
|
+
assert.deepStrictEqual(runner.calls[0], {
|
|
174
|
+
command: 'docker',
|
|
175
|
+
args: ['kill', 'docker-session'],
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should reject non-detached records', () => {
|
|
180
|
+
const store = createStore();
|
|
181
|
+
store.save(
|
|
182
|
+
createDetachedRecord({
|
|
183
|
+
options: {
|
|
184
|
+
isolated: 'screen',
|
|
185
|
+
isolationMode: 'attached',
|
|
186
|
+
sessionName: 'screen-session',
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const result = controlExecution(
|
|
192
|
+
store,
|
|
193
|
+
'screen-session',
|
|
194
|
+
ControlAction.STOP,
|
|
195
|
+
createRunner()
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
assert.strictEqual(result.success, false);
|
|
199
|
+
assert.match(result.error, /Only detached isolated executions/);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('process id collection', () => {
|
|
204
|
+
it('should parse a GNU Screen process id from screen -ls output', () => {
|
|
205
|
+
const pid = parseScreenPid(
|
|
206
|
+
'There is a screen on:\n\t1234.my-session\t(Detached)\n',
|
|
207
|
+
'my-session'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.strictEqual(pid, 1234);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should collect wrapper, screen, and descendant process IDs', () => {
|
|
214
|
+
const runner = createRunner({
|
|
215
|
+
'screen -ls': {
|
|
216
|
+
success: true,
|
|
217
|
+
stdout: '\t111.screen-session\t(Detached)\n',
|
|
218
|
+
stderr: '',
|
|
219
|
+
status: 0,
|
|
220
|
+
error: null,
|
|
221
|
+
},
|
|
222
|
+
'pgrep -P 111': {
|
|
223
|
+
success: true,
|
|
224
|
+
stdout: '222\n333\n',
|
|
225
|
+
stderr: '',
|
|
226
|
+
status: 0,
|
|
227
|
+
error: null,
|
|
228
|
+
},
|
|
229
|
+
'pgrep -P 222': {
|
|
230
|
+
success: true,
|
|
231
|
+
stdout: '',
|
|
232
|
+
stderr: '',
|
|
233
|
+
status: 1,
|
|
234
|
+
error: null,
|
|
235
|
+
},
|
|
236
|
+
'pgrep -P 333': {
|
|
237
|
+
success: true,
|
|
238
|
+
stdout: '',
|
|
239
|
+
stderr: '',
|
|
240
|
+
status: 1,
|
|
241
|
+
error: null,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const processIds = collectProcessIds(createDetachedRecord(), runner);
|
|
246
|
+
|
|
247
|
+
assert.deepStrictEqual(processIds, {
|
|
248
|
+
wrapperPid: 12345,
|
|
249
|
+
screenPid: 111,
|
|
250
|
+
commandPids: [222, 333],
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const { describe, it } = require('node:test');
|
|
9
9
|
const assert = require('assert');
|
|
10
|
+
const path = require('path');
|
|
10
11
|
const {
|
|
11
12
|
isCommandAvailable,
|
|
12
13
|
hasTTY,
|
|
@@ -525,7 +526,7 @@ describe('Isolation Runner with Available Backends', () => {
|
|
|
525
526
|
} = require('../src/lib/isolation');
|
|
526
527
|
const { execSync } = require('child_process');
|
|
527
528
|
|
|
528
|
-
// Screen integration tests moved to screen-integration.
|
|
529
|
+
// Screen integration tests moved to screen-integration.js
|
|
529
530
|
// to keep file under the 1000-line limit.
|
|
530
531
|
|
|
531
532
|
describe('runInTmux (if available)', () => {
|
|
@@ -668,8 +669,9 @@ describe('detectShellInEnvironment', () => {
|
|
|
668
669
|
{ image: 'alpine:latest' },
|
|
669
670
|
'auto'
|
|
670
671
|
);
|
|
672
|
+
const shellName = path.basename(result);
|
|
671
673
|
assert.ok(
|
|
672
|
-
['bash', 'zsh', 'sh'].includes(
|
|
674
|
+
['bash', 'zsh', 'sh'].includes(shellName),
|
|
673
675
|
`Expected a valid shell (bash/zsh/sh), got: ${result}`
|
|
674
676
|
);
|
|
675
677
|
console.log(` Detected shell in alpine:latest: ${result}`);
|
|
@@ -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
|
|
|
@@ -215,6 +216,7 @@ describe('--status query functionality', () => {
|
|
|
215
216
|
(record) => record.uuid === executingRecord.uuid
|
|
216
217
|
);
|
|
217
218
|
expect(executing.status).toBe('executing');
|
|
219
|
+
expect(executing.processIds).toEqual({ wrapperPid: 99999 });
|
|
218
220
|
expect(executing.currentTime).toBeDefined();
|
|
219
221
|
});
|
|
220
222
|
|
|
@@ -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
|
|