start-command 0.32.1 → 0.33.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 +21 -0
- package/README.md +3 -0
- package/eslint.config.mjs +8 -0
- package/package.json +7 -3
- package/src/bin/cli.js +23 -136
- package/src/lib/args-parser-queries.js +228 -0
- package/src/lib/args-parser.js +38 -179
- package/src/lib/docker-cleanup.js +80 -5
- package/src/lib/execution-attach.js +245 -0
- package/src/lib/execution-resume-all.js +210 -0
- package/src/lib/execution-resume.js +454 -0
- package/src/lib/execution-store.js +47 -4
- package/src/lib/exit-reason.js +234 -0
- package/src/lib/failure-handler.js +43 -41
- package/src/lib/isolation-log-utils.js +58 -0
- package/src/lib/isolation.js +13 -10
- package/src/lib/query-commands.js +202 -0
- package/src/lib/screen-isolation.js +2 -1
- package/src/lib/session-probe.js +121 -0
- package/src/lib/shell-utils.js +164 -10
- package/src/lib/status-formatter.js +130 -57
- package/src/lib/usage.js +13 -1
- package/src/lib/user-manager.js +18 -1
- package/test/args-parser-attach-resume.js +159 -0
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/check-file-size.js +91 -0
- package/test/ci-workflow-invariants.js +191 -0
- package/test/echo-integration.js +20 -13
- package/test/execution-attach.js +221 -0
- package/test/execution-resume.js +433 -0
- package/test/execution-store.js +52 -0
- package/test/exit-reason.js +209 -0
- package/test/failure-handler.js +107 -0
- package/test/load-command-stream.mjs +83 -0
- package/test/regression-162.js +320 -0
- package/test/regression-164.js +308 -0
- package/test/regression-165.js +310 -0
- package/test/regression-84.js +8 -3
- package/test/release-name.mjs +57 -0
- package/test/session-probe.js +150 -0
- package/test/user-manager.js +23 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for session liveness probing (issue #162)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { test, describe } = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const {
|
|
8
|
+
SessionState,
|
|
9
|
+
mapDockerStatusToState,
|
|
10
|
+
parseScreenSessionState,
|
|
11
|
+
probeSession,
|
|
12
|
+
} = require('../src/lib/session-probe');
|
|
13
|
+
|
|
14
|
+
function makeRecord(options) {
|
|
15
|
+
return { uuid: 'u-1', options: { isolationMode: 'detached', ...options } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeRunner(responses) {
|
|
19
|
+
return (command, args) => {
|
|
20
|
+
const key = [command, ...args].join(' ');
|
|
21
|
+
const response = responses[key];
|
|
22
|
+
if (!response) {
|
|
23
|
+
return { success: false, stdout: '', stderr: '', status: 1, error: null };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
stdout: '',
|
|
28
|
+
stderr: '',
|
|
29
|
+
status: 0,
|
|
30
|
+
error: null,
|
|
31
|
+
...response,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('parseScreenSessionState', () => {
|
|
37
|
+
const listing = [
|
|
38
|
+
'There are screens on:',
|
|
39
|
+
'\t12345.other-session\t(Detached)',
|
|
40
|
+
'\t67890.my-session\t(Detached)',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
test('finds a detached session', () => {
|
|
44
|
+
assert.strictEqual(
|
|
45
|
+
parseScreenSessionState(listing, 'my-session'),
|
|
46
|
+
SessionState.RUNNING
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('reports missing sessions', () => {
|
|
51
|
+
assert.strictEqual(
|
|
52
|
+
parseScreenSessionState(listing, 'gone'),
|
|
53
|
+
SessionState.MISSING
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('does not match a partial session name', () => {
|
|
58
|
+
assert.strictEqual(
|
|
59
|
+
parseScreenSessionState(listing, 'session'),
|
|
60
|
+
SessionState.MISSING
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('mapDockerStatusToState', () => {
|
|
66
|
+
test('maps running to RUNNING', () => {
|
|
67
|
+
assert.strictEqual(mapDockerStatusToState('running'), SessionState.RUNNING);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('maps exited and created to STOPPED', () => {
|
|
71
|
+
assert.strictEqual(mapDockerStatusToState('exited'), SessionState.STOPPED);
|
|
72
|
+
assert.strictEqual(mapDockerStatusToState('created'), SessionState.STOPPED);
|
|
73
|
+
assert.strictEqual(mapDockerStatusToState('dead'), SessionState.STOPPED);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('maps an unknown or empty status to UNKNOWN', () => {
|
|
77
|
+
assert.strictEqual(mapDockerStatusToState(''), SessionState.UNKNOWN);
|
|
78
|
+
assert.strictEqual(mapDockerStatusToState(null), SessionState.UNKNOWN);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('probeSession', () => {
|
|
83
|
+
test('detects a running docker container', () => {
|
|
84
|
+
const record = makeRecord({ isolated: 'docker', sessionName: 'box' });
|
|
85
|
+
const runner = makeRunner({
|
|
86
|
+
'docker inspect -f {{.State.Status}} box': { stdout: 'running\n' },
|
|
87
|
+
});
|
|
88
|
+
const probe = probeSession(record, runner);
|
|
89
|
+
assert.strictEqual(probe.state, SessionState.RUNNING);
|
|
90
|
+
assert.strictEqual(probe.alive, true);
|
|
91
|
+
assert.strictEqual(probe.containerStatus, 'running');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('detects a stopped docker container', () => {
|
|
95
|
+
const record = makeRecord({ isolated: 'docker', sessionName: 'box' });
|
|
96
|
+
const runner = makeRunner({
|
|
97
|
+
'docker inspect -f {{.State.Status}} box': { stdout: 'exited\n' },
|
|
98
|
+
});
|
|
99
|
+
const probe = probeSession(record, runner);
|
|
100
|
+
assert.strictEqual(probe.state, SessionState.STOPPED);
|
|
101
|
+
assert.strictEqual(probe.alive, false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('detects a removed docker container', () => {
|
|
105
|
+
const record = makeRecord({ isolated: 'docker', sessionName: 'box' });
|
|
106
|
+
const probe = probeSession(record, makeRunner({}));
|
|
107
|
+
assert.strictEqual(probe.state, SessionState.MISSING);
|
|
108
|
+
assert.strictEqual(probe.alive, false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('detects a live tmux session', () => {
|
|
112
|
+
const record = makeRecord({ isolated: 'tmux', sessionName: 'work' });
|
|
113
|
+
const runner = makeRunner({ 'tmux has-session -t work': {} });
|
|
114
|
+
assert.strictEqual(
|
|
115
|
+
probeSession(record, runner).state,
|
|
116
|
+
SessionState.RUNNING
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('detects a dead tmux session', () => {
|
|
121
|
+
const record = makeRecord({ isolated: 'tmux', sessionName: 'work' });
|
|
122
|
+
assert.strictEqual(
|
|
123
|
+
probeSession(record, makeRunner({})).state,
|
|
124
|
+
SessionState.MISSING
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('detects a live screen session', () => {
|
|
129
|
+
const record = makeRecord({ isolated: 'screen', sessionName: 'work' });
|
|
130
|
+
const runner = makeRunner({
|
|
131
|
+
'screen -ls': { stdout: '\t4242.work\t(Detached)\n', success: false },
|
|
132
|
+
});
|
|
133
|
+
assert.strictEqual(
|
|
134
|
+
probeSession(record, runner).state,
|
|
135
|
+
SessionState.RUNNING
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('reports ssh sessions as unknown', () => {
|
|
140
|
+
const record = makeRecord({ isolated: 'ssh', sessionName: 'remote' });
|
|
141
|
+
const probe = probeSession(record, makeRunner({}));
|
|
142
|
+
assert.strictEqual(probe.state, SessionState.UNKNOWN);
|
|
143
|
+
assert.strictEqual(probe.alive, false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('reports records without a session name as unknown', () => {
|
|
147
|
+
const probe = probeSession({ uuid: 'u', options: {} }, makeRunner({}));
|
|
148
|
+
assert.strictEqual(probe.state, SessionState.UNKNOWN);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/test/user-manager.js
CHANGED
|
@@ -125,6 +125,29 @@ describe('user-manager', () => {
|
|
|
125
125
|
assert.ok(name.length <= 31);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it('should draw the suffix from a CSPRNG, not Math.random', () => {
|
|
129
|
+
// The 4-character suffix is the only unguessable part of the account
|
|
130
|
+
// name; Math.random made it predictable (CodeQL js/insecure-randomness,
|
|
131
|
+
// issue #168). With Math.random pinned, names minted inside a single
|
|
132
|
+
// millisecond were byte-identical; a CSPRNG keeps them apart.
|
|
133
|
+
const originalRandom = Math.random;
|
|
134
|
+
Math.random = () => 0.5;
|
|
135
|
+
try {
|
|
136
|
+
const suffixes = new Set();
|
|
137
|
+
for (let index = 0; index < 200; index++) {
|
|
138
|
+
const name = generateIsolatedUsername();
|
|
139
|
+
assert.match(name, /^start-[0-9a-z]+$/);
|
|
140
|
+
suffixes.add(name.slice(-4));
|
|
141
|
+
}
|
|
142
|
+
assert.ok(
|
|
143
|
+
suffixes.size > 50,
|
|
144
|
+
`suffix does not vary while Math.random is pinned (${suffixes.size} distinct)`
|
|
145
|
+
);
|
|
146
|
+
} finally {
|
|
147
|
+
Math.random = originalRandom;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
128
151
|
it('should handle long prefix by truncating', () => {
|
|
129
152
|
const longPrefix = 'this-is-a-very-long-prefix';
|
|
130
153
|
const name = generateIsolatedUsername(longPrefix);
|