start-command 0.24.7 → 0.24.8
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 +10 -0
- package/package.json +1 -1
- package/test/echo-integration.test.js +9 -0
- package/test/failure-handler.test.js +103 -0
- package/test/isolation-log-utils.test.js +234 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1195fc1: Add CI/CD coverage enforcement and Rust/JS test parity checks (issue #93)
|
|
8
|
+
- Add `scripts/check-test-parity.mjs` script to enforce Rust/JS test count within 10%
|
|
9
|
+
- Add coverage job to JavaScript CI/CD workflow (80% minimum threshold)
|
|
10
|
+
- Update `ARCHITECTURE.md` to document dual-language sync requirements
|
|
11
|
+
- Update `REQUIREMENTS.md` to document test coverage requirements and parity rules
|
|
12
|
+
|
|
3
13
|
## 0.24.7
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ const path = require('path');
|
|
|
24
24
|
const {
|
|
25
25
|
isCommandAvailable,
|
|
26
26
|
canRunLinuxDockerImages,
|
|
27
|
+
hasTTY,
|
|
27
28
|
} = require('../src/lib/isolation');
|
|
28
29
|
|
|
29
30
|
// Path to the CLI
|
|
@@ -537,6 +538,14 @@ describe('Echo Integration Tests - Issue #55', () => {
|
|
|
537
538
|
}
|
|
538
539
|
|
|
539
540
|
describe('Attached Mode', () => {
|
|
541
|
+
if (!hasTTY()) {
|
|
542
|
+
it('should skip attached docker tests when no TTY is available', () => {
|
|
543
|
+
console.log(' ⚠ no TTY available, skipping attached docker tests');
|
|
544
|
+
assert.ok(true);
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
540
549
|
it('should execute echo hi in attached docker mode with proper formatting', () => {
|
|
541
550
|
const containerName = `test-docker-attached-${Date.now()}`;
|
|
542
551
|
const result = runCli(
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for failure-handler module
|
|
4
|
+
* Tests pure functions: parseGitUrl and handleFailure early-exit behavior
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it } = require('node:test');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const { parseGitUrl, handleFailure } = require('../src/lib/failure-handler');
|
|
10
|
+
|
|
11
|
+
describe('failure-handler', () => {
|
|
12
|
+
describe('parseGitUrl', () => {
|
|
13
|
+
it('should parse HTTPS GitHub URL', () => {
|
|
14
|
+
const result = parseGitUrl('https://github.com/owner/my-repo');
|
|
15
|
+
assert.ok(result !== null);
|
|
16
|
+
assert.strictEqual(result.owner, 'owner');
|
|
17
|
+
assert.strictEqual(result.repo, 'my-repo');
|
|
18
|
+
assert.strictEqual(result.url, 'https://github.com/owner/my-repo');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse HTTPS URL with .git suffix', () => {
|
|
22
|
+
const result = parseGitUrl('https://github.com/owner/my-repo.git');
|
|
23
|
+
assert.ok(result !== null);
|
|
24
|
+
assert.strictEqual(result.owner, 'owner');
|
|
25
|
+
assert.strictEqual(result.repo, 'my-repo');
|
|
26
|
+
assert.strictEqual(result.url, 'https://github.com/owner/my-repo');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should parse SSH git@ URL', () => {
|
|
30
|
+
const result = parseGitUrl('git@github.com:owner/my-repo.git');
|
|
31
|
+
assert.ok(result !== null);
|
|
32
|
+
assert.strictEqual(result.owner, 'owner');
|
|
33
|
+
assert.strictEqual(result.repo, 'my-repo');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should parse git+https URL format', () => {
|
|
37
|
+
const result = parseGitUrl('git+https://github.com/owner/repo.git');
|
|
38
|
+
assert.ok(result !== null);
|
|
39
|
+
assert.strictEqual(result.owner, 'owner');
|
|
40
|
+
assert.strictEqual(result.repo, 'repo');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return null for empty string', () => {
|
|
44
|
+
const result = parseGitUrl('');
|
|
45
|
+
assert.strictEqual(result, null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return null for null/undefined input', () => {
|
|
49
|
+
assert.strictEqual(parseGitUrl(null), null);
|
|
50
|
+
assert.strictEqual(parseGitUrl(undefined), null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return null for non-github URL', () => {
|
|
54
|
+
const result = parseGitUrl('https://gitlab.com/owner/repo');
|
|
55
|
+
assert.strictEqual(result, null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return null for invalid/random string', () => {
|
|
59
|
+
const result = parseGitUrl('not-a-url-at-all');
|
|
60
|
+
assert.strictEqual(result, null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should normalize URL to https://github.com format', () => {
|
|
64
|
+
const result = parseGitUrl('git@github.com:myorg/myrepo');
|
|
65
|
+
assert.ok(result !== null);
|
|
66
|
+
assert.ok(result.url.startsWith('https://github.com/'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle URL with subdirectory (only owner/repo captured)', () => {
|
|
70
|
+
const result = parseGitUrl('https://github.com/myorg/myrepo/issues');
|
|
71
|
+
assert.ok(result !== null);
|
|
72
|
+
assert.strictEqual(result.owner, 'myorg');
|
|
73
|
+
assert.strictEqual(result.repo, 'myrepo');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return object with owner, repo, url keys', () => {
|
|
77
|
+
const result = parseGitUrl('https://github.com/test/project');
|
|
78
|
+
assert.ok(result !== null);
|
|
79
|
+
assert.ok(Object.prototype.hasOwnProperty.call(result, 'owner'));
|
|
80
|
+
assert.ok(Object.prototype.hasOwnProperty.call(result, 'repo'));
|
|
81
|
+
assert.ok(Object.prototype.hasOwnProperty.call(result, 'url'));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('handleFailure', () => {
|
|
86
|
+
it('should return early when disableAutoIssue is true', () => {
|
|
87
|
+
// This should not throw and should return without calling external processes
|
|
88
|
+
const config = { disableAutoIssue: true };
|
|
89
|
+
// If it tries to call external tools, it would either throw or hang;
|
|
90
|
+
// returning cleanly means the early-exit path was taken.
|
|
91
|
+
assert.doesNotThrow(() => {
|
|
92
|
+
handleFailure(config, 'someCmd', 'someCmd --flag', 1, '/tmp/fake.log');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return early when disableAutoIssue is true (verbose mode)', () => {
|
|
97
|
+
const config = { disableAutoIssue: true, verbose: true };
|
|
98
|
+
assert.doesNotThrow(() => {
|
|
99
|
+
handleFailure(config, 'cmd', 'cmd arg', 2, '/tmp/fake.log');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for isolation-log-utils module
|
|
4
|
+
* Tests pure utility functions for log file management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it } = require('node:test');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const {
|
|
11
|
+
getTimestamp,
|
|
12
|
+
generateLogFilename,
|
|
13
|
+
createLogHeader,
|
|
14
|
+
createLogFooter,
|
|
15
|
+
getLogDir,
|
|
16
|
+
createLogPath,
|
|
17
|
+
} = require('../src/lib/isolation-log-utils');
|
|
18
|
+
|
|
19
|
+
describe('isolation-log-utils', () => {
|
|
20
|
+
describe('getTimestamp', () => {
|
|
21
|
+
it('should return a non-empty string', () => {
|
|
22
|
+
const ts = getTimestamp();
|
|
23
|
+
assert.strictEqual(typeof ts, 'string');
|
|
24
|
+
assert.ok(ts.length > 0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return a timestamp without T or Z (ISO-like but space-separated)', () => {
|
|
28
|
+
const ts = getTimestamp();
|
|
29
|
+
assert.ok(!ts.includes('T'), 'Should not contain ISO T separator');
|
|
30
|
+
assert.ok(!ts.endsWith('Z'), 'Should not end with Z');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should contain date-like content (numbers and dashes)', () => {
|
|
34
|
+
const ts = getTimestamp();
|
|
35
|
+
// Expect format like "2024-01-15 10:30:45.123"
|
|
36
|
+
assert.match(ts, /\d{4}-\d{2}-\d{2}/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return different values on successive calls (or same within same ms)', () => {
|
|
40
|
+
const ts1 = getTimestamp();
|
|
41
|
+
assert.strictEqual(typeof ts1, 'string');
|
|
42
|
+
// Just verify it's callable multiple times without error
|
|
43
|
+
const ts2 = getTimestamp();
|
|
44
|
+
assert.strictEqual(typeof ts2, 'string');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('generateLogFilename', () => {
|
|
49
|
+
it('should return a string ending with .log', () => {
|
|
50
|
+
const filename = generateLogFilename('screen');
|
|
51
|
+
assert.ok(filename.endsWith('.log'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should include the environment name in the filename', () => {
|
|
55
|
+
const filename = generateLogFilename('docker');
|
|
56
|
+
assert.ok(filename.includes('docker'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should start with "start-command-"', () => {
|
|
60
|
+
const filename = generateLogFilename('tmux');
|
|
61
|
+
assert.ok(filename.startsWith('start-command-'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should generate unique filenames on successive calls', () => {
|
|
65
|
+
const f1 = generateLogFilename('screen');
|
|
66
|
+
const f2 = generateLogFilename('screen');
|
|
67
|
+
// Due to random component, should be different
|
|
68
|
+
assert.notStrictEqual(f1, f2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle different environment names', () => {
|
|
72
|
+
const environments = ['screen', 'tmux', 'docker', 'user', 'none'];
|
|
73
|
+
for (const env of environments) {
|
|
74
|
+
const filename = generateLogFilename(env);
|
|
75
|
+
assert.ok(
|
|
76
|
+
filename.includes(env),
|
|
77
|
+
`Filename should include environment "${env}"`
|
|
78
|
+
);
|
|
79
|
+
assert.ok(filename.endsWith('.log'));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('createLogHeader', () => {
|
|
85
|
+
const baseParams = {
|
|
86
|
+
command: 'npm test',
|
|
87
|
+
environment: 'screen',
|
|
88
|
+
mode: 'attached',
|
|
89
|
+
sessionName: 'test-session-123',
|
|
90
|
+
startTime: '2024-01-15 10:30:00.000',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
it('should return a non-empty string', () => {
|
|
94
|
+
const header = createLogHeader(baseParams);
|
|
95
|
+
assert.strictEqual(typeof header, 'string');
|
|
96
|
+
assert.ok(header.length > 0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should include the command in the header', () => {
|
|
100
|
+
const header = createLogHeader(baseParams);
|
|
101
|
+
assert.ok(header.includes('npm test'));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should include the environment in the header', () => {
|
|
105
|
+
const header = createLogHeader(baseParams);
|
|
106
|
+
assert.ok(header.includes('screen'));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should include the session name in the header', () => {
|
|
110
|
+
const header = createLogHeader(baseParams);
|
|
111
|
+
assert.ok(header.includes('test-session-123'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should include the mode in the header', () => {
|
|
115
|
+
const header = createLogHeader(baseParams);
|
|
116
|
+
assert.ok(header.includes('attached'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should include image field when provided', () => {
|
|
120
|
+
const params = { ...baseParams, image: 'node:20-alpine' };
|
|
121
|
+
const header = createLogHeader(params);
|
|
122
|
+
assert.ok(header.includes('node:20-alpine'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should include user field when provided', () => {
|
|
126
|
+
const params = { ...baseParams, user: 'isolateduser' };
|
|
127
|
+
const header = createLogHeader(params);
|
|
128
|
+
assert.ok(header.includes('isolateduser'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should NOT include Image line when image is not provided', () => {
|
|
132
|
+
const header = createLogHeader(baseParams);
|
|
133
|
+
assert.ok(!header.includes('Image:'));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should contain separator line', () => {
|
|
137
|
+
const header = createLogHeader(baseParams);
|
|
138
|
+
assert.ok(header.includes('==='));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('createLogFooter', () => {
|
|
143
|
+
it('should return a non-empty string', () => {
|
|
144
|
+
const footer = createLogFooter('2024-01-15 10:35:00.000', 0);
|
|
145
|
+
assert.strictEqual(typeof footer, 'string');
|
|
146
|
+
assert.ok(footer.length > 0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should include the exit code', () => {
|
|
150
|
+
const footer = createLogFooter('2024-01-15 10:35:00.000', 42);
|
|
151
|
+
assert.ok(footer.includes('42'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should include exit code 0', () => {
|
|
155
|
+
const footer = createLogFooter('2024-01-15 10:35:00.000', 0);
|
|
156
|
+
assert.ok(footer.includes('0'));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should include the end time', () => {
|
|
160
|
+
const endTime = '2024-01-15 10:35:00.000';
|
|
161
|
+
const footer = createLogFooter(endTime, 1);
|
|
162
|
+
assert.ok(footer.includes(endTime));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should contain separator line', () => {
|
|
166
|
+
const footer = createLogFooter('2024-01-15 10:35:00.000', 0);
|
|
167
|
+
assert.ok(footer.includes('='));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('getLogDir', () => {
|
|
172
|
+
it('should return a string', () => {
|
|
173
|
+
const dir = getLogDir();
|
|
174
|
+
assert.strictEqual(typeof dir, 'string');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return a non-empty path', () => {
|
|
178
|
+
const dir = getLogDir();
|
|
179
|
+
assert.ok(dir.length > 0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should use START_LOG_DIR env var when set', () => {
|
|
183
|
+
const original = process.env.START_LOG_DIR;
|
|
184
|
+
process.env.START_LOG_DIR = '/tmp/custom-log-dir';
|
|
185
|
+
try {
|
|
186
|
+
const dir = getLogDir();
|
|
187
|
+
assert.strictEqual(dir, '/tmp/custom-log-dir');
|
|
188
|
+
} finally {
|
|
189
|
+
if (original === undefined) {
|
|
190
|
+
delete process.env.START_LOG_DIR;
|
|
191
|
+
} else {
|
|
192
|
+
process.env.START_LOG_DIR = original;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should fall back to os.tmpdir() when START_LOG_DIR is not set', () => {
|
|
198
|
+
const os = require('os');
|
|
199
|
+
const original = process.env.START_LOG_DIR;
|
|
200
|
+
delete process.env.START_LOG_DIR;
|
|
201
|
+
try {
|
|
202
|
+
const dir = getLogDir();
|
|
203
|
+
assert.strictEqual(dir, os.tmpdir());
|
|
204
|
+
} finally {
|
|
205
|
+
if (original !== undefined) {
|
|
206
|
+
process.env.START_LOG_DIR = original;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('createLogPath', () => {
|
|
213
|
+
it('should return a string ending with .log', () => {
|
|
214
|
+
const logPath = createLogPath('screen');
|
|
215
|
+
assert.ok(logPath.endsWith('.log'));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should return an absolute path', () => {
|
|
219
|
+
const logPath = createLogPath('tmux');
|
|
220
|
+
assert.ok(path.isAbsolute(logPath));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should include the environment name', () => {
|
|
224
|
+
const logPath = createLogPath('docker');
|
|
225
|
+
assert.ok(logPath.includes('docker'));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should be under the log directory', () => {
|
|
229
|
+
const logDir = getLogDir();
|
|
230
|
+
const logPath = createLogPath('screen');
|
|
231
|
+
assert.ok(logPath.startsWith(logDir));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|