start-command 0.36.0 → 0.37.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/CHANGELOG.md +14 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/bin/cli.js +5 -0
- package/src/lib/args-parser-queries.js +14 -0
- package/src/lib/args-parser.js +20 -1
- package/src/lib/command-builder.js +3 -0
- package/src/lib/cpu-penalty-monitor.js +209 -87
- package/src/lib/cpu-penalty.js +30 -1
- package/src/lib/docker-cleanup.js +12 -5
- package/src/lib/docker-command-handoff.js +75 -0
- package/src/lib/docker-labels.js +33 -0
- package/src/lib/docker-runtime-args.js +8 -0
- package/src/lib/docker-snapshot-safety.js +164 -0
- package/src/lib/execution-recovery.js +8 -1
- package/src/lib/execution-resume.js +184 -37
- package/src/lib/failure-handler.js +15 -5
- package/src/lib/isolation.js +20 -4
- package/src/lib/log-sanitizer.js +267 -0
- package/src/lib/log-uploader.js +38 -19
- package/src/lib/query-commands.js +4 -3
- package/src/lib/resume-resources.js +15 -3
- package/src/lib/shell-utils.js +67 -17
- package/src/lib/store-lock.js +54 -3
- package/src/lib/usage.js +5 -1
- package/test/ci-workflow-invariants.js +15 -14
- package/test/docker-command-handoff.js +107 -0
- package/test/docker-runtime-options.js +4 -0
- package/test/docker-snapshot-safety.js +106 -0
- package/test/execution-resume.js +10 -8
- package/test/failure-handler.js +11 -0
- package/test/javascript-first-ci.js +216 -0
- package/test/log-sanitizer.js +306 -0
- package/test/regression-176.js +7 -0
- package/test/regression-187.js +9 -1
- package/test/regression-191.js +9 -1
- package/test/regression-193.js +11 -1
- package/test/regression-198.js +281 -0
- package/test/regression-199.js +79 -0
- package/test/regression-201.js +25 -0
- package/test/regression-202.js +92 -0
- package/test/regression-207.js +249 -0
- package/test/regression-91.js +23 -20
- package/test/shared-golden.js +77 -0
- package/test/status-query.js +97 -15
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const { it, expect } = require('bun:test');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { initialState, transitionLog } = require('../src/lib/cpu-penalty');
|
|
7
|
+
const cpu = require('../src/lib/cpu-penalty-monitor');
|
|
8
|
+
|
|
9
|
+
it('restarted monitors retain an observed base absent from launch flags', () => {
|
|
10
|
+
const saved = { ...initialState(4, 0), phase: 'penalized', limitCpus: 2 };
|
|
11
|
+
expect(initialState(null, 100, saved).baseCpus).toBe(4);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('saved observing limits preserve the base after a daemon-clamped lift', () => {
|
|
15
|
+
const saved = { ...initialState(4, 0), limitCpus: 1 };
|
|
16
|
+
const { observeBase } = require('../src/lib/cpu-penalty');
|
|
17
|
+
expect(observeBase(initialState(null, 100, saved), 1, 100).baseCpus).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for (const mode of [
|
|
21
|
+
'nano',
|
|
22
|
+
'quota',
|
|
23
|
+
'restart',
|
|
24
|
+
'zombie',
|
|
25
|
+
'reused',
|
|
26
|
+
'supervise',
|
|
27
|
+
'publish-fault',
|
|
28
|
+
'graceful',
|
|
29
|
+
'override',
|
|
30
|
+
]) {
|
|
31
|
+
it(`CPU monitor recovers external base and idle release: ${mode}`, () => {
|
|
32
|
+
if (
|
|
33
|
+
process.platform === 'win32' ||
|
|
34
|
+
(['zombie', 'reused'].includes(mode) && process.platform !== 'linux')
|
|
35
|
+
) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-monitor-207-'));
|
|
39
|
+
try {
|
|
40
|
+
const logPath = path.join(dir, 'task.log');
|
|
41
|
+
fs.writeFileSync(logPath, '');
|
|
42
|
+
const record = {
|
|
43
|
+
logPath,
|
|
44
|
+
options: {
|
|
45
|
+
sessionName: 'cpu-task',
|
|
46
|
+
isolated: 'docker',
|
|
47
|
+
cpuPenaltyConfig: {
|
|
48
|
+
cpus: 2,
|
|
49
|
+
trigger: 95,
|
|
50
|
+
release: 65,
|
|
51
|
+
triggerWindowMs: 400,
|
|
52
|
+
releaseWindowMs: 400,
|
|
53
|
+
},
|
|
54
|
+
baseResourceLimits: [],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const command = [
|
|
58
|
+
process.execPath,
|
|
59
|
+
require.resolve('../src/lib/cpu-penalty-monitor'),
|
|
60
|
+
'',
|
|
61
|
+
'cpu-task',
|
|
62
|
+
'1',
|
|
63
|
+
JSON.stringify(record),
|
|
64
|
+
];
|
|
65
|
+
if (mode === 'supervise') {
|
|
66
|
+
const script = `${cpu.startSnippet(null, 'cpu-task', 1, { ...record.options, logPath })}; read done; ${cpu.stopSnippet()}`;
|
|
67
|
+
command.splice(0, command.length, 'sh', '-c', script);
|
|
68
|
+
}
|
|
69
|
+
const result = spawnSync(
|
|
70
|
+
'python3',
|
|
71
|
+
[
|
|
72
|
+
path.resolve(__dirname, '../../experiments/issue-207-monitor.py'),
|
|
73
|
+
mode,
|
|
74
|
+
logPath,
|
|
75
|
+
...command,
|
|
76
|
+
],
|
|
77
|
+
{ encoding: 'utf8' }
|
|
78
|
+
);
|
|
79
|
+
expect(result.status, result.stderr).toBe(0);
|
|
80
|
+
const state = cpu.readState(record);
|
|
81
|
+
const expected = mode === 'override' ? 3 : 4;
|
|
82
|
+
expect(state.baseCpus).toBe(expected);
|
|
83
|
+
expect(state.phase).toBe('observing');
|
|
84
|
+
expect(state.limitCpus).toBe(expected);
|
|
85
|
+
if (mode !== 'override') {
|
|
86
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain(
|
|
87
|
+
'CPU penalty lifted: 4'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const { ExecutionRecord } = require('../src/lib/execution-store');
|
|
91
|
+
const {
|
|
92
|
+
enrichDetachedStatus,
|
|
93
|
+
formatRecord,
|
|
94
|
+
} = require('../src/lib/status-formatter');
|
|
95
|
+
const statusRecord = new ExecutionRecord({
|
|
96
|
+
...record,
|
|
97
|
+
options: { ...record.options, isolated: undefined },
|
|
98
|
+
});
|
|
99
|
+
const status = JSON.parse(
|
|
100
|
+
formatRecord(enrichDetachedStatus(statusRecord), 'json')
|
|
101
|
+
);
|
|
102
|
+
expect(status.cpuPenalty.baseCpus).toBe(expected);
|
|
103
|
+
expect(status.cpuPenalty.limitCpus).toBe(expected);
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}, 15000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const failure of ['inspect', 'stats', 'update', 'publish']) {
|
|
111
|
+
it(`an exception in ${failure} resets coverage and monitoring continues`, async () => {
|
|
112
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-tick-207-'));
|
|
113
|
+
const logPath = path.join(dir, 'task.log');
|
|
114
|
+
const record = {
|
|
115
|
+
logPath,
|
|
116
|
+
options: {
|
|
117
|
+
sessionName: 'cpu-task',
|
|
118
|
+
cpuPenaltyConfig: {
|
|
119
|
+
cpus: 2,
|
|
120
|
+
trigger: 95,
|
|
121
|
+
release: 65,
|
|
122
|
+
triggerWindowMs: 100,
|
|
123
|
+
releaseWindowMs: 100,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
let ticks = 0;
|
|
128
|
+
let injected = false,
|
|
129
|
+
limit = 4,
|
|
130
|
+
lifted = false;
|
|
131
|
+
const filesystem = Object.create(fs);
|
|
132
|
+
filesystem.renameSync = (from, to) => {
|
|
133
|
+
if (
|
|
134
|
+
failure === 'publish' &&
|
|
135
|
+
!injected &&
|
|
136
|
+
JSON.parse(fs.readFileSync(from)).phase === 'penalized'
|
|
137
|
+
) {
|
|
138
|
+
injected = true;
|
|
139
|
+
throw new Error('injected ENOSPC');
|
|
140
|
+
}
|
|
141
|
+
fs.renameSync(from, to);
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
await cpu.monitor('', 'cpu-task', 1, JSON.stringify(record), {
|
|
145
|
+
fs: filesystem,
|
|
146
|
+
run: (_command, args) => {
|
|
147
|
+
if (args[0] === failure && !injected) {
|
|
148
|
+
injected = true;
|
|
149
|
+
throw new Error(`injected ${failure}`);
|
|
150
|
+
}
|
|
151
|
+
const result = (stdout) => ({ success: true, stdout });
|
|
152
|
+
if (args[0] === 'inspect') {
|
|
153
|
+
if (!args[2].includes('HostConfig') && ++ticks > 60) {
|
|
154
|
+
lifted = false;
|
|
155
|
+
return result('false');
|
|
156
|
+
}
|
|
157
|
+
return result(
|
|
158
|
+
args[2].includes('HostConfig')
|
|
159
|
+
? JSON.stringify({ NanoCpus: limit * 1e9 })
|
|
160
|
+
: String(!lifted)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (args[0] === 'info') {
|
|
164
|
+
return result('{"NCPU":6}');
|
|
165
|
+
}
|
|
166
|
+
if (args[0] === 'stats') {
|
|
167
|
+
return result(
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
Name: 'cpu-task',
|
|
170
|
+
CPUPerc: limit === 2 ? '20%' : '400%',
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (args[0] === 'update') {
|
|
175
|
+
limit = Number(args[2]);
|
|
176
|
+
lifted = limit === 4;
|
|
177
|
+
return result('');
|
|
178
|
+
}
|
|
179
|
+
throw new Error('unexpected command');
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
expect(injected).toBe(true);
|
|
183
|
+
expect(lifted).toBe(true);
|
|
184
|
+
expect(cpu.readState(record).baseCpus).toBe(4);
|
|
185
|
+
} finally {
|
|
186
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
}, 5000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it('stopped resume uses the observed base rather than the temporary quota', () => {
|
|
192
|
+
const { restoreBaseCpu } = require('../src/lib/resume-resources');
|
|
193
|
+
expect(
|
|
194
|
+
restoreBaseCpu(
|
|
195
|
+
['--memory=512m', '--cpus=2'],
|
|
196
|
+
{ cpuPenaltyConfig: {}, baseResourceLimits: [] },
|
|
197
|
+
{ baseCpus: 4 }
|
|
198
|
+
)
|
|
199
|
+
).toEqual(['--memory=512m', '--cpus=4']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('stopped resume preserves a later supervisor override instead of the stale cap', () => {
|
|
203
|
+
const { restoreBaseCpu } = require('../src/lib/resume-resources');
|
|
204
|
+
const options = { cpuPenaltyConfig: {}, baseResourceLimits: [] };
|
|
205
|
+
const saved = { phase: 'penalized', baseCpus: 4, limitCpus: 2 };
|
|
206
|
+
expect(restoreBaseCpu(['--cpus=2'], options, saved, true)).toEqual([
|
|
207
|
+
'--cpus=4',
|
|
208
|
+
]);
|
|
209
|
+
expect(restoreBaseCpu(['--cpus=3'], options, saved, true)).toEqual([
|
|
210
|
+
'--cpus=3',
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('transition logs explain the window and phase denominator', () => {
|
|
215
|
+
const config = { cpus: 2, triggerWindowMs: 900000, releaseWindowMs: 400 };
|
|
216
|
+
expect(
|
|
217
|
+
transitionLog({ kind: 'apply', cpus: 2, capacity: 4, average: 100 }, config)
|
|
218
|
+
).toContain('of 4 CPUs for 15m');
|
|
219
|
+
expect(
|
|
220
|
+
transitionLog({ kind: 'lift', cpus: 4, capacity: 4, average: 10 }, config)
|
|
221
|
+
).toContain('of 2 CPUs for 400ms');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('process birth identity reclaims reused PIDs without evicting a live lock', () => {
|
|
225
|
+
if (process.platform !== 'linux') {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const { LockManager } = require('../src/lib/store-lock');
|
|
229
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-lock-207-'));
|
|
230
|
+
const file = path.join(dir, 'lock');
|
|
231
|
+
const first = new LockManager(file);
|
|
232
|
+
const replacement = new LockManager(file);
|
|
233
|
+
try {
|
|
234
|
+
expect(first.acquire(100)).toBe(true);
|
|
235
|
+
const data = JSON.parse(fs.readFileSync(file));
|
|
236
|
+
expect(data.processStart).toBeTruthy();
|
|
237
|
+
expect(replacement.acquire(100)).toBe(false);
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
file,
|
|
240
|
+
JSON.stringify({ ...data, processStart: 'different-birth' })
|
|
241
|
+
);
|
|
242
|
+
expect(replacement.acquire(100)).toBe(true);
|
|
243
|
+
first.release();
|
|
244
|
+
expect(fs.existsSync(file)).toBe(true);
|
|
245
|
+
} finally {
|
|
246
|
+
replacement.release();
|
|
247
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
package/test/regression-91.js
CHANGED
|
@@ -69,7 +69,7 @@ function buildAttachedCmdArgs(command, shellToUse = '/bin/bash') {
|
|
|
69
69
|
describe('isShellInvocationWithArgs (issue #91)', () => {
|
|
70
70
|
it('should return true for "bash -i -c nvm --version"', () => {
|
|
71
71
|
assert.strictEqual(
|
|
72
|
-
isShellInvocationWithArgs('bash -i -c nvm --version'),
|
|
72
|
+
isShellInvocationWithArgs('bash -i -c "nvm --version"'),
|
|
73
73
|
true
|
|
74
74
|
);
|
|
75
75
|
});
|
|
@@ -79,11 +79,14 @@ describe('isShellInvocationWithArgs (issue #91)', () => {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it('should return true for "bash -c echo hello"', () => {
|
|
82
|
-
assert.strictEqual(isShellInvocationWithArgs('bash -c echo hello'), true);
|
|
82
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -c "echo hello"'), true);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('should return true for "zsh -c nvm --version"', () => {
|
|
86
|
-
assert.strictEqual(
|
|
86
|
+
assert.strictEqual(
|
|
87
|
+
isShellInvocationWithArgs('zsh -c "nvm --version"'),
|
|
88
|
+
true
|
|
89
|
+
);
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
it('should return true for "sh -c ls"', () => {
|
|
@@ -115,12 +118,12 @@ describe('isShellInvocationWithArgs (issue #91)', () => {
|
|
|
115
118
|
|
|
116
119
|
describe('buildShellWithArgsCmdArgs (issue #91)', () => {
|
|
117
120
|
it('should reconstruct "bash -i -c nvm --version" correctly', () => {
|
|
118
|
-
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
121
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c "nvm --version"');
|
|
119
122
|
assert.deepStrictEqual(result, ['bash', '-i', '-c', 'nvm --version']);
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
it('should reconstruct "bash -c echo hello" correctly', () => {
|
|
123
|
-
const result = buildShellWithArgsCmdArgs('bash -c echo hello');
|
|
126
|
+
const result = buildShellWithArgsCmdArgs('bash -c "echo hello"');
|
|
124
127
|
assert.deepStrictEqual(result, ['bash', '-c', 'echo hello']);
|
|
125
128
|
});
|
|
126
129
|
|
|
@@ -130,13 +133,13 @@ describe('buildShellWithArgsCmdArgs (issue #91)', () => {
|
|
|
130
133
|
});
|
|
131
134
|
|
|
132
135
|
it('should handle zsh with -c', () => {
|
|
133
|
-
const result = buildShellWithArgsCmdArgs('zsh -c nvm --version');
|
|
136
|
+
const result = buildShellWithArgsCmdArgs('zsh -c "nvm --version"');
|
|
134
137
|
assert.deepStrictEqual(result, ['zsh', '-c', 'nvm --version']);
|
|
135
138
|
});
|
|
136
139
|
|
|
137
140
|
it('should handle /bin/bash -i -c with multi-word script', () => {
|
|
138
141
|
const result = buildShellWithArgsCmdArgs(
|
|
139
|
-
'/bin/bash -i -c node -e process.version'
|
|
142
|
+
'/bin/bash -i -c "node -e process.version"'
|
|
140
143
|
);
|
|
141
144
|
assert.deepStrictEqual(result, [
|
|
142
145
|
'/bin/bash',
|
|
@@ -147,7 +150,7 @@ describe('buildShellWithArgsCmdArgs (issue #91)', () => {
|
|
|
147
150
|
});
|
|
148
151
|
|
|
149
152
|
it('should not include -c argument inside script argument (no double -c)', () => {
|
|
150
|
-
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
153
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c "nvm --version"');
|
|
151
154
|
// The script arg must be 'nvm --version', not '-c nvm --version'
|
|
152
155
|
assert.strictEqual(result[result.length - 1], 'nvm --version');
|
|
153
156
|
assert.strictEqual(result.indexOf('-c'), 2);
|
|
@@ -159,13 +162,13 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
|
159
162
|
// Each test verifies that `bash -i -c "cmd"` style commands are NOT re-wrapped
|
|
160
163
|
// in another outer shell -c invocation.
|
|
161
164
|
//
|
|
162
|
-
// Before fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
163
|
-
// → ['/bin/bash', '-i', '-c', 'bash -i -c nvm --version'] (WRONG: double-wrap)
|
|
164
|
-
// After fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
165
|
+
// Before fix: buildAttachedCmdArgs('bash -i -c "nvm --version"')
|
|
166
|
+
// → ['/bin/bash', '-i', '-c', 'bash -i -c "nvm --version"'] (WRONG: double-wrap)
|
|
167
|
+
// After fix: buildAttachedCmdArgs('bash -i -c "nvm --version"')
|
|
165
168
|
// → ['bash', '-i', '-c', 'nvm --version'] (CORRECT: direct pass)
|
|
166
169
|
|
|
167
170
|
it('should pass "bash -i -c nvm --version" directly without outer shell wrapper', () => {
|
|
168
|
-
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
171
|
+
const args = buildAttachedCmdArgs('bash -i -c "nvm --version"');
|
|
169
172
|
// Must start with 'bash', not with '/bin/bash' (the outer shellToUse)
|
|
170
173
|
assert.strictEqual(args[0], 'bash');
|
|
171
174
|
// Must not wrap in outer bash -i -c
|
|
@@ -182,7 +185,7 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
|
182
185
|
});
|
|
183
186
|
|
|
184
187
|
it('should pass "zsh -c nvm --version" directly without outer shell wrapper', () => {
|
|
185
|
-
const args = buildAttachedCmdArgs('zsh -c nvm --version', '/bin/zsh');
|
|
188
|
+
const args = buildAttachedCmdArgs('zsh -c "nvm --version"', '/bin/zsh');
|
|
186
189
|
assert.deepStrictEqual(args, ['zsh', '-c', 'nvm --version']);
|
|
187
190
|
assert.ok(
|
|
188
191
|
args.filter((a) => a === '-c').length === 1,
|
|
@@ -191,12 +194,12 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
|
191
194
|
});
|
|
192
195
|
|
|
193
196
|
it('should pass "bash -c echo hello" directly', () => {
|
|
194
|
-
const args = buildAttachedCmdArgs('bash -c echo hello');
|
|
197
|
+
const args = buildAttachedCmdArgs('bash -c "echo hello"');
|
|
195
198
|
assert.deepStrictEqual(args, ['bash', '-c', 'echo hello']);
|
|
196
199
|
});
|
|
197
200
|
|
|
198
201
|
it('should not introduce a second bash layer (no shell-inside-shell)', () => {
|
|
199
|
-
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
202
|
+
const args = buildAttachedCmdArgs('bash -i -c "nvm --version"');
|
|
200
203
|
// The first element of the docker image command args must NOT be the outer shell
|
|
201
204
|
// i.e., must not be ['/bin/bash', '-i', '-c', 'bash ...']
|
|
202
205
|
assert.notStrictEqual(
|
|
@@ -206,7 +209,7 @@ describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
|
206
209
|
);
|
|
207
210
|
// Must not contain the original full command string as an argument
|
|
208
211
|
assert.ok(
|
|
209
|
-
!args.includes('bash -i -c nvm --version'),
|
|
212
|
+
!args.includes('bash -i -c "nvm --version"'),
|
|
210
213
|
'Must not contain the full command string as a single argument (double-wrap)'
|
|
211
214
|
);
|
|
212
215
|
});
|
|
@@ -241,7 +244,7 @@ describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShel
|
|
|
241
244
|
'bash',
|
|
242
245
|
'bash -i',
|
|
243
246
|
'bash --norc',
|
|
244
|
-
'bash -i -c nvm --version',
|
|
247
|
+
'bash -i -c "nvm --version"',
|
|
245
248
|
'bash -c echo hi',
|
|
246
249
|
'zsh -c ls',
|
|
247
250
|
'nvm --version',
|
|
@@ -263,14 +266,14 @@ describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShel
|
|
|
263
266
|
describe('buildDisplayCommand (issue #91 display fix)', () => {
|
|
264
267
|
it('should quote the -c script argument when it contains spaces', () => {
|
|
265
268
|
assert.strictEqual(
|
|
266
|
-
buildDisplayCommand('bash -i -c nvm --version'),
|
|
269
|
+
buildDisplayCommand('bash -i -c "nvm --version"'),
|
|
267
270
|
'bash -i -c "nvm --version"'
|
|
268
271
|
);
|
|
269
272
|
});
|
|
270
273
|
|
|
271
274
|
it('should quote the -c script argument for plain "bash -c echo hello"', () => {
|
|
272
275
|
assert.strictEqual(
|
|
273
|
-
buildDisplayCommand('bash -c echo hello'),
|
|
276
|
+
buildDisplayCommand('bash -c "echo hello"'),
|
|
274
277
|
'bash -c "echo hello"'
|
|
275
278
|
);
|
|
276
279
|
});
|
|
@@ -289,7 +292,7 @@ describe('buildDisplayCommand (issue #91 display fix)', () => {
|
|
|
289
292
|
|
|
290
293
|
it('should handle zsh -c with spaces', () => {
|
|
291
294
|
assert.strictEqual(
|
|
292
|
-
buildDisplayCommand('zsh -c echo hello world'),
|
|
295
|
+
buildDisplayCommand('zsh -c "echo hello world"'),
|
|
293
296
|
'zsh -c "echo hello world"'
|
|
294
297
|
);
|
|
295
298
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { readFileSync } = require('node:fs');
|
|
4
|
+
const { resolve } = require('node:path');
|
|
5
|
+
const { parseArgs } = require('../src/lib/args-parser');
|
|
6
|
+
const { buildDockerRuntimeArgs } = require('../src/lib/docker-runtime-args');
|
|
7
|
+
const { ExecutionRecord } = require('../src/lib/execution-store');
|
|
8
|
+
const { formatRecord } = require('../src/lib/status-formatter');
|
|
9
|
+
const fixtures = JSON.parse(
|
|
10
|
+
readFileSync(resolve(__dirname, '../../parity/fixtures/contracts.json'))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe('shared JavaScript and Rust golden contracts', () => {
|
|
14
|
+
for (const fixture of fixtures['cli-parsing']) {
|
|
15
|
+
it(`CLI parsing: ${fixture.name}`, () => {
|
|
16
|
+
if (fixture.error) {
|
|
17
|
+
assert.throws(() => parseArgs(fixture.argv), new RegExp(fixture.error));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const parsed = parseArgs(fixture.argv);
|
|
21
|
+
assert.equal(parsed.command, fixture.command);
|
|
22
|
+
assert.deepEqual(parsed.rawCommand, fixture.rawCommand);
|
|
23
|
+
for (const [key, value] of Object.entries(fixture.options)) {
|
|
24
|
+
assert.deepEqual(parsed.wrapperOptions[key], value, key);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const fixture of fixtures['isolation-arguments']) {
|
|
29
|
+
it(`isolation argv: ${fixture.name}`, () => {
|
|
30
|
+
assert.deepEqual(buildDockerRuntimeArgs(fixture.options), fixture.argv);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
it('uses the same JSON and text status output', () => {
|
|
34
|
+
const fixture = fixtures['status-output'];
|
|
35
|
+
const record = new ExecutionRecord(fixture.record);
|
|
36
|
+
assert.deepEqual(JSON.parse(formatRecord(record, 'json')), fixture.json);
|
|
37
|
+
assert.equal(formatRecord(record, 'text'), fixture.text);
|
|
38
|
+
});
|
|
39
|
+
for (const fixture of fixtures['execution-record-format']) {
|
|
40
|
+
it(`preserves the complete execution record format (exit ${fixture.exitCode})`, () => {
|
|
41
|
+
assert.deepEqual(ExecutionRecord.fromObject(fixture).toObject(), fixture);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
it('executes the JavaScript source of the generated Rust parity policy', async () => {
|
|
45
|
+
const { minimumRustTestCount } =
|
|
46
|
+
await import('../../scripts/parity-threshold.mjs');
|
|
47
|
+
for (const fixture of fixtures['parity-threshold']) {
|
|
48
|
+
assert.equal(
|
|
49
|
+
minimumRustTestCount(fixture.javascript),
|
|
50
|
+
fixture.minimumRust
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
for (const fixture of fixtures['cpu-penalty-state']) {
|
|
55
|
+
it(`CPU state: ${fixture.name}`, () => {
|
|
56
|
+
const {
|
|
57
|
+
initialState,
|
|
58
|
+
observeBase,
|
|
59
|
+
evaluate,
|
|
60
|
+
} = require('../src/lib/cpu-penalty');
|
|
61
|
+
let state = initialState(fixture.baseCpus, 0, fixture.saved);
|
|
62
|
+
for (const step of fixture.steps) {
|
|
63
|
+
state = observeBase(state, step.baseCpus, step.now);
|
|
64
|
+
const decision = evaluate(
|
|
65
|
+
state,
|
|
66
|
+
{ now: step.now, cores: step.cores, daemonCpus: 6, maxGapMs: 50 },
|
|
67
|
+
fixture.config
|
|
68
|
+
);
|
|
69
|
+
assert.equal(decision.action?.kind || null, step.action);
|
|
70
|
+
state = decision.state;
|
|
71
|
+
}
|
|
72
|
+
for (const [key, value] of Object.entries(fixture.expected)) {
|
|
73
|
+
assert.deepEqual(state[key], value, key);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
package/test/status-query.js
CHANGED
|
@@ -53,19 +53,35 @@ function createExecutable(filePath, content) {
|
|
|
53
53
|
fs.chmodSync(filePath, 0o755);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function fakeUploaderScript(fakeBin, outputPrefix) {
|
|
57
|
+
const fixturePath = path.join(fakeBin, 'capture-upload.js');
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
fixturePath,
|
|
60
|
+
[
|
|
61
|
+
"const fs = require('fs');",
|
|
62
|
+
'const args = process.argv.slice(2);',
|
|
63
|
+
'fs.writeFileSync(process.env.UPLOAD_CAPTURE, JSON.stringify({',
|
|
64
|
+
' args, content: fs.readFileSync(args[0], "utf8"),',
|
|
65
|
+
' mode: fs.statSync(args[0]).mode & 0o777,',
|
|
66
|
+
'}));',
|
|
67
|
+
`console.log(${JSON.stringify(outputPrefix)} + ': ' + args[0]);`,
|
|
68
|
+
'',
|
|
69
|
+
].join('\n')
|
|
70
|
+
);
|
|
71
|
+
if (process.platform === 'win32') {
|
|
72
|
+
return `@echo off\r\n"${process.execPath}" "${fixturePath}" %*\r\n`;
|
|
73
|
+
}
|
|
74
|
+
return `#!/bin/sh\n"${process.execPath}" "${fixturePath}" "$@"\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
function createFakeUploader(fakeBin, outputPrefix) {
|
|
78
|
+
const content = fakeUploaderScript(fakeBin, outputPrefix);
|
|
57
79
|
if (process.platform === 'win32') {
|
|
58
|
-
createExecutable(
|
|
59
|
-
path.join(fakeBin, 'gh-upload-log.cmd'),
|
|
60
|
-
`@echo off\r\necho ${outputPrefix}: %1\r\n`
|
|
61
|
-
);
|
|
80
|
+
createExecutable(path.join(fakeBin, 'gh-upload-log.cmd'), content);
|
|
62
81
|
return;
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
createExecutable(
|
|
66
|
-
path.join(fakeBin, 'gh-upload-log'),
|
|
67
|
-
`#!/bin/sh\necho "${outputPrefix}: $1"\n`
|
|
68
|
-
);
|
|
84
|
+
createExecutable(path.join(fakeBin, 'gh-upload-log'), content);
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
describe('--status query functionality', () => {
|
|
@@ -262,10 +278,14 @@ describe('--status query functionality', () => {
|
|
|
262
278
|
});
|
|
263
279
|
|
|
264
280
|
describe('--upload-log functionality', () => {
|
|
265
|
-
it('should
|
|
281
|
+
it('should upload a private sanitized copy and preserve the stored log', () => {
|
|
266
282
|
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'upload-log-bin-'));
|
|
267
283
|
const logPath = path.join(TEST_APP_FOLDER, 'command.log');
|
|
268
|
-
|
|
284
|
+
const capturePath = path.join(fakeBin, 'capture.json');
|
|
285
|
+
const token = ['gh', 'p_', 'a'.repeat(36)].join('');
|
|
286
|
+
const customSecret = 'fake-custom-credential';
|
|
287
|
+
const original = `captured command output\n${token}\n${customSecret}\n`;
|
|
288
|
+
fs.writeFileSync(logPath, original, 'utf8');
|
|
269
289
|
createFakeUploader(fakeBin, 'fake uploader received');
|
|
270
290
|
|
|
271
291
|
testRecord.logPath = logPath;
|
|
@@ -274,11 +294,28 @@ describe('--status query functionality', () => {
|
|
|
274
294
|
const result = runCli(['--upload-log', testRecord.uuid], {
|
|
275
295
|
PATH: fakeBin,
|
|
276
296
|
HOME: fakeBin,
|
|
297
|
+
UPLOAD_CAPTURE: capturePath,
|
|
298
|
+
EXAMPLE_API_KEY: customSecret,
|
|
299
|
+
GH_UPLOAD_PUBLIC: 'true',
|
|
277
300
|
});
|
|
278
301
|
|
|
279
302
|
expect(result.exitCode).toBe(0);
|
|
280
|
-
|
|
303
|
+
const captured = JSON.parse(fs.readFileSync(capturePath, 'utf8'));
|
|
304
|
+
expect(captured.args[0]).not.toBe(logPath);
|
|
305
|
+
expect(captured.args[1]).toBe('--private');
|
|
306
|
+
expect(captured.content).toContain('captured command output');
|
|
307
|
+
expect(captured.content).toContain('[REDACTED]');
|
|
308
|
+
expect(captured.content).not.toContain(token);
|
|
309
|
+
expect(captured.content).not.toContain(customSecret);
|
|
310
|
+
if (process.platform !== 'win32') {
|
|
311
|
+
expect(captured.mode).toBe(0o600);
|
|
312
|
+
}
|
|
313
|
+
expect(result.stdout).toContain(
|
|
314
|
+
`fake uploader received: ${captured.args[0]}`
|
|
315
|
+
);
|
|
281
316
|
expect(result.stderr).toBe('');
|
|
317
|
+
expect(fs.readFileSync(logPath, 'utf8')).toBe(original);
|
|
318
|
+
expect(fs.existsSync(captured.args[0])).toBe(false);
|
|
282
319
|
|
|
283
320
|
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
284
321
|
});
|
|
@@ -293,8 +330,15 @@ describe('--status query functionality', () => {
|
|
|
293
330
|
path.join(os.tmpdir(), 'upload-log-install-bin-')
|
|
294
331
|
);
|
|
295
332
|
const installMarker = path.join(fakeBin, 'install.log');
|
|
333
|
+
const capturePath = path.join(fakeBin, 'capture.json');
|
|
296
334
|
const logPath = path.join(TEST_APP_FOLDER, 'install-command.log');
|
|
297
|
-
|
|
335
|
+
const token = ['gh', 'p_', 'b'.repeat(36)].join('');
|
|
336
|
+
const original = `captured command output\n${token}\n`;
|
|
337
|
+
fs.writeFileSync(logPath, original, 'utf8');
|
|
338
|
+
const uploaderScript = fakeUploaderScript(
|
|
339
|
+
fakeBin,
|
|
340
|
+
'installed uploader received'
|
|
341
|
+
);
|
|
298
342
|
|
|
299
343
|
createExecutable(
|
|
300
344
|
path.join(fakeBin, 'bun'),
|
|
@@ -302,8 +346,7 @@ describe('--status query functionality', () => {
|
|
|
302
346
|
'#!/bin/sh',
|
|
303
347
|
`echo "$@" > "${installMarker}"`,
|
|
304
348
|
`cat > "${path.join(fakeBin, 'gh-upload-log')}" <<'SCRIPT'`,
|
|
305
|
-
|
|
306
|
-
'echo "installed uploader received: $1"',
|
|
349
|
+
uploaderScript,
|
|
307
350
|
'SCRIPT',
|
|
308
351
|
`chmod +x "${path.join(fakeBin, 'gh-upload-log')}"`,
|
|
309
352
|
'exit 0',
|
|
@@ -317,6 +360,7 @@ describe('--status query functionality', () => {
|
|
|
317
360
|
const result = runCli(['--upload-log', testRecord.uuid], {
|
|
318
361
|
PATH: `${fakeBin}${path.delimiter}/usr/bin${path.delimiter}/bin`,
|
|
319
362
|
HOME: fakeBin,
|
|
363
|
+
UPLOAD_CAPTURE: capturePath,
|
|
320
364
|
});
|
|
321
365
|
|
|
322
366
|
expect(result.exitCode).toBe(0);
|
|
@@ -324,10 +368,48 @@ describe('--status query functionality', () => {
|
|
|
324
368
|
'install -g gh-upload-log'
|
|
325
369
|
);
|
|
326
370
|
expect(result.stdout).toContain('gh-upload-log not found');
|
|
371
|
+
const captured = JSON.parse(fs.readFileSync(capturePath, 'utf8'));
|
|
372
|
+
expect(captured.args[0]).not.toBe(logPath);
|
|
373
|
+
expect(captured.args[1]).toBe('--private');
|
|
374
|
+
expect(captured.content).toContain('captured command output');
|
|
375
|
+
expect(captured.content).toContain('[REDACTED]');
|
|
376
|
+
expect(captured.content).not.toContain(token);
|
|
377
|
+
expect(captured.mode).toBe(0o600);
|
|
327
378
|
expect(result.stdout).toContain(
|
|
328
|
-
`installed uploader received: ${
|
|
379
|
+
`installed uploader received: ${captured.args[0]}`
|
|
380
|
+
);
|
|
381
|
+
expect(fs.readFileSync(logPath, 'utf8')).toBe(original);
|
|
382
|
+
expect(fs.existsSync(captured.args[0])).toBe(false);
|
|
383
|
+
|
|
384
|
+
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should upload the original privately only with explicit --no-sanitize', () => {
|
|
388
|
+
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'upload-raw-bin-'));
|
|
389
|
+
const capturePath = path.join(fakeBin, 'capture.json');
|
|
390
|
+
const logPath = path.join(TEST_APP_FOLDER, 'raw-command.log');
|
|
391
|
+
const token = ['gh', 'p_', 'c'.repeat(36)].join('');
|
|
392
|
+
const original = `captured command output\n${token}\n`;
|
|
393
|
+
fs.writeFileSync(logPath, original, 'utf8');
|
|
394
|
+
createFakeUploader(fakeBin, 'raw uploader received');
|
|
395
|
+
testRecord.logPath = logPath;
|
|
396
|
+
store.save(testRecord);
|
|
397
|
+
|
|
398
|
+
const result = runCli(
|
|
399
|
+
['--upload-log', testRecord.uuid, '--no-sanitize'],
|
|
400
|
+
{
|
|
401
|
+
PATH: fakeBin,
|
|
402
|
+
HOME: fakeBin,
|
|
403
|
+
UPLOAD_CAPTURE: capturePath,
|
|
404
|
+
GH_UPLOAD_PUBLIC: 'true',
|
|
405
|
+
}
|
|
329
406
|
);
|
|
330
407
|
|
|
408
|
+
expect(result.exitCode).toBe(0);
|
|
409
|
+
const captured = JSON.parse(fs.readFileSync(capturePath, 'utf8'));
|
|
410
|
+
expect(captured.args).toEqual([logPath, '--private']);
|
|
411
|
+
expect(captured.content).toBe(original);
|
|
412
|
+
expect(fs.readFileSync(logPath, 'utf8')).toBe(original);
|
|
331
413
|
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
332
414
|
});
|
|
333
415
|
|