start-command 0.38.0 → 0.39.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 +28 -0
- package/README.md +4 -2
- package/bunfig.toml +4 -0
- package/eslint.config.mjs +1 -0
- package/package.json +4 -4
- package/src/lib/args-parser.js +9 -0
- package/src/lib/cgroup-memory.js +120 -24
- package/src/lib/command-builder.js +4 -0
- package/src/lib/container-removal.js +4 -5
- package/src/lib/detached-finalize.js +1 -0
- package/src/lib/docker-init-option.js +65 -0
- package/src/lib/docker-inspect-batch.js +176 -0
- package/src/lib/docker-resource-options.js +21 -2
- package/src/lib/docker-runtime-args.js +12 -3
- package/src/lib/execution-control.js +8 -6
- package/src/lib/execution-recovery.js +4 -1
- package/src/lib/execution-resume.js +4 -1
- package/src/lib/execution-store.js +19 -1
- package/src/lib/exit-evidence.js +139 -7
- package/src/lib/output-blocks.js +11 -0
- package/src/lib/resume-resources.js +2 -2
- package/src/lib/screen-isolation.js +72 -42
- package/src/lib/status-formatter.js +34 -130
- package/src/lib/status-probe.js +149 -0
- package/src/lib/store-decode-cache.js +141 -0
- package/src/lib/usage.js +3 -0
- package/test/cargo-lock-version.mjs +121 -0
- package/test/check-rust-coverage.mjs +71 -0
- package/test/check-web-archive.mjs +154 -0
- package/test/ci-workflow-invariants.js +204 -0
- package/test/command-builder-docker-cleanup.js +24 -0
- package/test/command-exit-codes.mjs +91 -0
- package/test/docker-runtime-options.js +2 -0
- package/test/docker-snapshot-safety.js +39 -0
- package/test/execution-resume.js +2 -0
- package/test/fixtures/fake-npm.sh +64 -0
- package/test/fixtures/lychee-report.md +39 -0
- package/test/isolation.js +32 -0
- package/test/javascript-first-ci.js +69 -0
- package/test/links-workflow.mjs +127 -0
- package/test/lint-gate.mjs +56 -0
- package/test/npm-publish.mjs +181 -0
- package/test/recheck-broken-links.mjs +452 -0
- package/test/regression-176.js +2 -0
- package/test/regression-187.js +2 -0
- package/test/regression-191.js +4 -0
- package/test/regression-193.js +2 -0
- package/test/regression-198.js +2 -0
- package/test/regression-207.js +8 -2
- package/test/regression-224.js +2 -0
- package/test/regression-236.js +250 -0
- package/test/regression-238.js +129 -0
- package/test/regression-241.js +254 -0
- package/test/regression-242.js +241 -0
- package/test/regression-243.js +493 -0
- package/test/regression-244.js +471 -0
- package/test/regression-247.js +140 -0
- package/test/release-push-retry.mjs +172 -0
- package/test/run-js-tests.mjs +15 -3
- package/test/setup/private-snapshot-lock.js +17 -0
- package/test/shared-golden.js +29 -2
- package/test/transient-links.mjs +139 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { describe, expect, it } from 'bun:test';
|
|
13
|
+
import {
|
|
14
|
+
allFailuresRecovered,
|
|
15
|
+
extractLycheeRequestOptions,
|
|
16
|
+
parseAcceptRanges,
|
|
17
|
+
parseLycheeFailures,
|
|
18
|
+
recheckUnanswered,
|
|
19
|
+
} from '../../scripts/recheck-broken-links.mjs';
|
|
20
|
+
import { splitRecoveredUrls } from '../../scripts/check-web-archive.mjs';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const root = path.resolve(
|
|
24
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
+
'../..'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const fixtureReport = readFileSync(
|
|
29
|
+
path.join(root, 'js/test/fixtures/lychee-report.md'),
|
|
30
|
+
'utf8'
|
|
31
|
+
);
|
|
32
|
+
const linksWorkflow = readFileSync(
|
|
33
|
+
path.join(root, '.github/workflows/links.yml'),
|
|
34
|
+
'utf8'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function once(fn) {
|
|
38
|
+
let called = false;
|
|
39
|
+
return (...args) => {
|
|
40
|
+
if (called) {
|
|
41
|
+
throw new Error('fetch called twice');
|
|
42
|
+
}
|
|
43
|
+
called = true;
|
|
44
|
+
return fn(...args);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('lychee report classification', () => {
|
|
49
|
+
it('marks a failure carrying a status code as final', () => {
|
|
50
|
+
const failures = parseLycheeFailures(fixtureReport);
|
|
51
|
+
const answered = failures.filter((failure) => failure.answered);
|
|
52
|
+
|
|
53
|
+
expect(answered.length).toBe(2);
|
|
54
|
+
expect(answered.map((failure) => failure.url)).toEqual([
|
|
55
|
+
'https://example.com/csharp/',
|
|
56
|
+
'https://example.com/rust/link_cli/',
|
|
57
|
+
]);
|
|
58
|
+
expect(answered[0].detail).toContain('Rejected status code');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('marks ERROR, TIMEOUT, and UNKNOWN markers as never answered', () => {
|
|
62
|
+
const report = [
|
|
63
|
+
'- [ERROR] <https://example.com/reset> | Connection reset by peer',
|
|
64
|
+
'- [TIMEOUT] <https://example.com/slow> | Timeout',
|
|
65
|
+
'- [UNKNOWN] <https://example.com/dark> | Unknown error',
|
|
66
|
+
].join('\n');
|
|
67
|
+
const failures = parseLycheeFailures(report);
|
|
68
|
+
|
|
69
|
+
expect(failures.every((failure) => !failure.answered)).toBe(true);
|
|
70
|
+
expect(failures.map((failure) => failure.marker)).toEqual([
|
|
71
|
+
'ERROR',
|
|
72
|
+
'TIMEOUT',
|
|
73
|
+
'UNKNOWN',
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('treats a rejected status code as an answer whatever the marker', () => {
|
|
78
|
+
const report =
|
|
79
|
+
'- [ERROR] <https://example.com/gone> | Rejected status code: 503 Service Unavailable';
|
|
80
|
+
|
|
81
|
+
expect(parseLycheeFailures(report)[0].answered).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps non-http failures out of the re-check even when unanswered', () => {
|
|
85
|
+
const failures = parseLycheeFailures(fixtureReport);
|
|
86
|
+
const unanswerable = failures.filter(
|
|
87
|
+
(failure) => !failure.answered && !/^https?:\/\//i.test(failure.url)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(unanswerable.map((failure) => failure.url).sort()).toEqual([
|
|
91
|
+
'error:',
|
|
92
|
+
'file:///home/runner/work/repo/repo/csharp/docs/api/Foundation.Data.Doublets.Cli.yml',
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('lychee request option sharing', () => {
|
|
98
|
+
// The re-check must judge a URL by the same rules lychee used. Neither
|
|
99
|
+
// flag is set in links.yml today, so lychee's documented defaults apply on
|
|
100
|
+
// both sides; if either side changes, this test fails until the other
|
|
101
|
+
// follows.
|
|
102
|
+
it('reads the same accept list and user agent as the lychee step', () => {
|
|
103
|
+
const options = extractLycheeRequestOptions(linksWorkflow);
|
|
104
|
+
|
|
105
|
+
expect(options.accept).toBe('100..=103,200..=299');
|
|
106
|
+
expect(options.userAgent).toBe('lychee');
|
|
107
|
+
expect(linksWorkflow).not.toContain('--accept');
|
|
108
|
+
expect(linksWorkflow).not.toContain('--user-agent');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('extracts the flags when the workflow sets them', () => {
|
|
112
|
+
const workflow = [
|
|
113
|
+
' args: >-',
|
|
114
|
+
' --verbose',
|
|
115
|
+
' --accept 200..=299,429',
|
|
116
|
+
' --user-agent my-checker/2.0',
|
|
117
|
+
].join('\n');
|
|
118
|
+
const options = extractLycheeRequestOptions(workflow);
|
|
119
|
+
|
|
120
|
+
expect(options.accept).toBe('200..=299,429');
|
|
121
|
+
expect(options.userAgent).toBe('my-checker/2.0');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('accept list parsing', () => {
|
|
126
|
+
it('honours ranges, singles, and stray spaces', () => {
|
|
127
|
+
const accepted = parseAcceptRanges(' 100..=103 , 200..=299, 429 ');
|
|
128
|
+
|
|
129
|
+
expect(accepted(100)).toBe(true);
|
|
130
|
+
expect(accepted(103)).toBe(true);
|
|
131
|
+
expect(accepted(200)).toBe(true);
|
|
132
|
+
expect(accepted(299)).toBe(true);
|
|
133
|
+
expect(accepted(429)).toBe(true);
|
|
134
|
+
expect(accepted(104)).toBe(false);
|
|
135
|
+
expect(accepted(404)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('complete report recovery verdict', () => {
|
|
140
|
+
it('requires every failure in a non-empty unanswered-only report to recover', () => {
|
|
141
|
+
expect(
|
|
142
|
+
allFailuresRecovered({
|
|
143
|
+
finalFailureCount: 0,
|
|
144
|
+
stillBrokenCount: 0,
|
|
145
|
+
unansweredCount: 2,
|
|
146
|
+
recoveredCount: 2,
|
|
147
|
+
})
|
|
148
|
+
).toBe(true);
|
|
149
|
+
|
|
150
|
+
for (const counts of [
|
|
151
|
+
{
|
|
152
|
+
finalFailureCount: 1,
|
|
153
|
+
stillBrokenCount: 0,
|
|
154
|
+
unansweredCount: 1,
|
|
155
|
+
recoveredCount: 1,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
finalFailureCount: 0,
|
|
159
|
+
stillBrokenCount: 1,
|
|
160
|
+
unansweredCount: 2,
|
|
161
|
+
recoveredCount: 1,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
finalFailureCount: 0,
|
|
165
|
+
stillBrokenCount: 0,
|
|
166
|
+
unansweredCount: 0,
|
|
167
|
+
recoveredCount: 0,
|
|
168
|
+
},
|
|
169
|
+
]) {
|
|
170
|
+
expect(allFailuresRecovered(counts)).toBe(false);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('re-check requests', () => {
|
|
176
|
+
it('recovers a URL that answers accepted', async () => {
|
|
177
|
+
const result = await recheckUnanswered(['https://a.example/x'], {
|
|
178
|
+
accept: '200..=299',
|
|
179
|
+
userAgent: 'lychee',
|
|
180
|
+
fetchImpl: once(() => Promise.resolve({ status: 200 })),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result.recovered).toEqual(['https://a.example/x']);
|
|
184
|
+
expect(result.stillBroken).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('stops at the first rejected answer and never retries it', async () => {
|
|
188
|
+
let calls = 0;
|
|
189
|
+
const result = await recheckUnanswered(['https://a.example/x'], {
|
|
190
|
+
accept: '200..=299',
|
|
191
|
+
userAgent: 'lychee',
|
|
192
|
+
budgetSeconds: 5,
|
|
193
|
+
initialWaitMs: 1,
|
|
194
|
+
fetchImpl: () => {
|
|
195
|
+
calls += 1;
|
|
196
|
+
return Promise.resolve({ status: 404 });
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(calls).toBe(1);
|
|
201
|
+
expect(result.recovered).toEqual([]);
|
|
202
|
+
expect(result.stillBroken).toEqual([
|
|
203
|
+
{
|
|
204
|
+
url: 'https://a.example/x',
|
|
205
|
+
status: 404,
|
|
206
|
+
reason: 'answered 404, which lychee does not accept',
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('retries a URL that never answers and takes a later success', async () => {
|
|
212
|
+
let calls = 0;
|
|
213
|
+
const result = await recheckUnanswered(['https://a.example/x'], {
|
|
214
|
+
accept: '200..=299',
|
|
215
|
+
userAgent: 'lychee',
|
|
216
|
+
budgetSeconds: 30,
|
|
217
|
+
initialWaitMs: 1,
|
|
218
|
+
fetchImpl: () => {
|
|
219
|
+
calls += 1;
|
|
220
|
+
if (calls < 3) {
|
|
221
|
+
return Promise.reject(new Error('ECONNRESET'));
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve({ status: 204 });
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(calls).toBe(3);
|
|
228
|
+
expect(result.recovered).toEqual(['https://a.example/x']);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('gives up without an answer once the budget expires', async () => {
|
|
232
|
+
const result = await recheckUnanswered(['https://a.example/x'], {
|
|
233
|
+
accept: '200..=299',
|
|
234
|
+
userAgent: 'lychee',
|
|
235
|
+
budgetSeconds: 0,
|
|
236
|
+
fetchImpl: () => Promise.reject(new Error('ECONNRESET')),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.recovered).toEqual([]);
|
|
240
|
+
expect(result.stillBroken[0].status).toBe(null);
|
|
241
|
+
expect(result.stillBroken[0].reason).toContain('no answer');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('asks a duplicated URL only once', async () => {
|
|
245
|
+
let calls = 0;
|
|
246
|
+
const result = await recheckUnanswered(
|
|
247
|
+
['https://a.example/x', 'https://a.example/x'],
|
|
248
|
+
{
|
|
249
|
+
accept: '200..=299',
|
|
250
|
+
userAgent: 'lychee',
|
|
251
|
+
fetchImpl: once(() => {
|
|
252
|
+
calls += 1;
|
|
253
|
+
return Promise.resolve({ status: 200 });
|
|
254
|
+
}),
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(calls).toBe(1);
|
|
259
|
+
expect(result.recovered).toEqual(['https://a.example/x']);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('sends the request the way lychee was configured to', async () => {
|
|
263
|
+
let seen;
|
|
264
|
+
await recheckUnanswered(['https://a.example/x'], {
|
|
265
|
+
accept: '200..=299',
|
|
266
|
+
userAgent: 'my-checker/2.0',
|
|
267
|
+
fetchImpl: (url, init) => {
|
|
268
|
+
seen = { url, init };
|
|
269
|
+
return Promise.resolve({ status: 200 });
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(seen.url).toBe('https://a.example/x');
|
|
274
|
+
expect(seen.init.method).toBe('HEAD');
|
|
275
|
+
expect(seen.init.headers['user-agent']).toBe('my-checker/2.0');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('re-check step end to end', () => {
|
|
280
|
+
function writeReport(dir, entries) {
|
|
281
|
+
const reportPath = path.join(dir, 'out.md');
|
|
282
|
+
writeFileSync(reportPath, `## Errors per input\n\n${entries.join('\n')}\n`);
|
|
283
|
+
return reportPath;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function runRecheck(env) {
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
const child = spawn('node', ['scripts/recheck-broken-links.mjs'], {
|
|
289
|
+
cwd: root,
|
|
290
|
+
env: { ...process.env, ...env },
|
|
291
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
292
|
+
});
|
|
293
|
+
let output = '';
|
|
294
|
+
child.stdout.on('data', (chunk) => {
|
|
295
|
+
output += chunk;
|
|
296
|
+
});
|
|
297
|
+
child.stderr.on('data', (chunk) => {
|
|
298
|
+
output += chunk;
|
|
299
|
+
});
|
|
300
|
+
child.on('close', (code) => resolve({ code, output }));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
it('recovers only the URLs that answer healthy and never re-asks a 404', async () => {
|
|
305
|
+
const requests = [];
|
|
306
|
+
const server = createServer((request, response) => {
|
|
307
|
+
requests.push(request.url);
|
|
308
|
+
if (request.url === '/healthy') {
|
|
309
|
+
response.writeHead(200);
|
|
310
|
+
} else {
|
|
311
|
+
response.writeHead(404);
|
|
312
|
+
}
|
|
313
|
+
response.end();
|
|
314
|
+
});
|
|
315
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
316
|
+
const { port } = server.address();
|
|
317
|
+
const base = `http://127.0.0.1:${port}`;
|
|
318
|
+
|
|
319
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'recheck-'));
|
|
320
|
+
try {
|
|
321
|
+
const reportPath = writeReport(dir, [
|
|
322
|
+
`- [ERROR] <${base}/healthy> | Connection reset by peer`,
|
|
323
|
+
`- [ERROR] <${base}/dead> | Connection reset by peer`,
|
|
324
|
+
'- [404] <https://example.com/final/> | Rejected status code: 404 Not Found',
|
|
325
|
+
]);
|
|
326
|
+
const recoveredPath = path.join(dir, 'recovered.txt');
|
|
327
|
+
const outputPath = path.join(dir, 'github-output.txt');
|
|
328
|
+
|
|
329
|
+
const { code, output } = await runRecheck({
|
|
330
|
+
LYCHEE_OUTPUT: reportPath,
|
|
331
|
+
RECOVERED_OUTPUT: recoveredPath,
|
|
332
|
+
GITHUB_OUTPUT: outputPath,
|
|
333
|
+
RECHECK_WAIT_MS: '10',
|
|
334
|
+
RECHECK_BUDGET_SECONDS: '30',
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(code).toBe(0);
|
|
338
|
+
expect(readFileSync(recoveredPath, 'utf8')).toBe(`${base}/healthy\n`);
|
|
339
|
+
expect(requests.sort()).toEqual(['/dead', '/healthy']);
|
|
340
|
+
// The 404 was already an answer; re-asking it would be wrong.
|
|
341
|
+
expect(requests).not.toContain('/final');
|
|
342
|
+
// One link stayed broken, so the gate must not be released.
|
|
343
|
+
expect(existsSync(outputPath)).toBe(false);
|
|
344
|
+
expect(output).toContain('still without an answer');
|
|
345
|
+
} finally {
|
|
346
|
+
server.close();
|
|
347
|
+
rmSync(dir, { recursive: true, force: true });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('releases the gate when a cached 503 recovers with a fresh success', async () => {
|
|
352
|
+
const server = createServer((request, response) => {
|
|
353
|
+
response.writeHead(200);
|
|
354
|
+
response.end();
|
|
355
|
+
});
|
|
356
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
357
|
+
const { port } = server.address();
|
|
358
|
+
|
|
359
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'recheck-'));
|
|
360
|
+
try {
|
|
361
|
+
const reportPath = writeReport(dir, [
|
|
362
|
+
`- [503] <http://127.0.0.1:${port}/slow> | Error (cached)`,
|
|
363
|
+
]);
|
|
364
|
+
const recoveredPath = path.join(dir, 'recovered.txt');
|
|
365
|
+
const outputPath = path.join(dir, 'github-output.txt');
|
|
366
|
+
|
|
367
|
+
const { code } = await runRecheck({
|
|
368
|
+
LYCHEE_OUTPUT: reportPath,
|
|
369
|
+
RECOVERED_OUTPUT: recoveredPath,
|
|
370
|
+
GITHUB_OUTPUT: outputPath,
|
|
371
|
+
RECHECK_WAIT_MS: '10',
|
|
372
|
+
RECHECK_BUDGET_SECONDS: '30',
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(code).toBe(0);
|
|
376
|
+
expect(readFileSync(outputPath, 'utf8')).toContain('all_recovered=true');
|
|
377
|
+
} finally {
|
|
378
|
+
server.close();
|
|
379
|
+
rmSync(dir, { recursive: true, force: true });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('does not release the gate when a 404 remains beside a recovered link', async () => {
|
|
384
|
+
const server = createServer((request, response) => {
|
|
385
|
+
response.writeHead(200);
|
|
386
|
+
response.end();
|
|
387
|
+
});
|
|
388
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
389
|
+
const { port } = server.address();
|
|
390
|
+
|
|
391
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'recheck-'));
|
|
392
|
+
try {
|
|
393
|
+
const reportPath = writeReport(dir, [
|
|
394
|
+
'- [404] <https://example.com/final/> | Rejected status code: 404 Not Found',
|
|
395
|
+
`- [ERROR] <http://127.0.0.1:${port}/recovered> | Connection reset by peer`,
|
|
396
|
+
]);
|
|
397
|
+
const recoveredPath = path.join(dir, 'recovered.txt');
|
|
398
|
+
const outputPath = path.join(dir, 'github-output.txt');
|
|
399
|
+
|
|
400
|
+
const { code } = await runRecheck({
|
|
401
|
+
LYCHEE_OUTPUT: reportPath,
|
|
402
|
+
RECOVERED_OUTPUT: recoveredPath,
|
|
403
|
+
GITHUB_OUTPUT: outputPath,
|
|
404
|
+
RECHECK_WAIT_MS: '10',
|
|
405
|
+
RECHECK_BUDGET_SECONDS: '30',
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(code).toBe(0);
|
|
409
|
+
expect(readFileSync(recoveredPath, 'utf8')).toContain('/recovered');
|
|
410
|
+
expect(existsSync(outputPath)).toBe(false);
|
|
411
|
+
} finally {
|
|
412
|
+
server.close();
|
|
413
|
+
rmSync(dir, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('exits 0 even when the report is missing entirely', async () => {
|
|
418
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'recheck-'));
|
|
419
|
+
try {
|
|
420
|
+
const { code, output } = await runRecheck({
|
|
421
|
+
LYCHEE_OUTPUT: path.join(dir, 'missing.md'),
|
|
422
|
+
RECOVERED_OUTPUT: path.join(dir, 'recovered.txt'),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(code).toBe(0);
|
|
426
|
+
expect(output).toContain('treating as no recovery');
|
|
427
|
+
} finally {
|
|
428
|
+
rmSync(dir, { recursive: true, force: true });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe('web archive fallback honouring the re-check', () => {
|
|
434
|
+
it('drops recovered URLs from the archive lookup', () => {
|
|
435
|
+
const { remaining, recovered } = splitRecoveredUrls(
|
|
436
|
+
['https://a.example/x', 'https://b.example/y'],
|
|
437
|
+
'https://b.example/y\n'
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(remaining).toEqual(['https://a.example/x']);
|
|
441
|
+
expect(recovered).toEqual(['https://b.example/y']);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('skips nothing when the recovered file is missing or empty', () => {
|
|
445
|
+
const urls = ['https://a.example/x'];
|
|
446
|
+
const fromMissing = splitRecoveredUrls(urls, '');
|
|
447
|
+
const fromNull = splitRecoveredUrls(urls, null);
|
|
448
|
+
|
|
449
|
+
expect(fromMissing.remaining).toEqual(urls);
|
|
450
|
+
expect(fromNull.remaining).toEqual(urls);
|
|
451
|
+
});
|
|
452
|
+
});
|
package/test/regression-176.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* container, under the same execution UUID and log, up to N times.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
// Own snapshot lock even without the bunfig.toml preload (issue #234).
|
|
15
|
+
require('./setup/private-snapshot-lock');
|
|
14
16
|
const { describe, it, expect, afterEach } = require('bun:test');
|
|
15
17
|
const { spawnSync } = require('child_process');
|
|
16
18
|
const fs = require('fs');
|
package/test/regression-187.js
CHANGED
package/test/regression-191.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Own snapshot lock even without the bunfig.toml preload (issue #234).
|
|
2
|
+
require('./setup/private-snapshot-lock');
|
|
1
3
|
const { it, expect } = require('bun:test');
|
|
2
4
|
const fs = require('fs');
|
|
3
5
|
const os = require('os');
|
|
@@ -49,6 +51,7 @@ function fixture() {
|
|
|
49
51
|
stdout: JSON.stringify({
|
|
50
52
|
Memory: 67108864,
|
|
51
53
|
MemorySwap: 67108864,
|
|
54
|
+
MemoryReservation: 33554432,
|
|
52
55
|
NanoCpus: 3000000000,
|
|
53
56
|
}),
|
|
54
57
|
};
|
|
@@ -139,6 +142,7 @@ for (const [exitCode, oomKilled, sample, change, marker] of [
|
|
|
139
142
|
if (change) {
|
|
140
143
|
expect(result.recovered).toBe(true);
|
|
141
144
|
expect(update).toContain('--memory=805306368');
|
|
145
|
+
expect(update).toContain('--memory-reservation=32m');
|
|
142
146
|
expect(f.calls.indexOf(update)).toBeLessThan(
|
|
143
147
|
f.calls.findIndex((a) => a[0] === 'start')
|
|
144
148
|
);
|
package/test/regression-193.js
CHANGED
package/test/regression-198.js
CHANGED
package/test/regression-207.js
CHANGED
|
@@ -25,6 +25,8 @@ for (const mode of [
|
|
|
25
25
|
'reused',
|
|
26
26
|
'supervise',
|
|
27
27
|
'publish-fault',
|
|
28
|
+
// publish-fault with the monitor's republish window forced open (#234).
|
|
29
|
+
'publish-race',
|
|
28
30
|
'graceful',
|
|
29
31
|
'override',
|
|
30
32
|
]) {
|
|
@@ -66,15 +68,19 @@ for (const mode of [
|
|
|
66
68
|
const script = `${cpu.startSnippet(null, 'cpu-task', 1, { ...record.options, logPath })}; read done; ${cpu.stopSnippet()}`;
|
|
67
69
|
command.splice(0, command.length, 'sh', '-c', script);
|
|
68
70
|
}
|
|
71
|
+
const env = { ...process.env };
|
|
72
|
+
if (mode === 'publish-race') {
|
|
73
|
+
env.START_FIXTURE_UNLINK_DELAY = '1.2';
|
|
74
|
+
}
|
|
69
75
|
const result = spawnSync(
|
|
70
76
|
'python3',
|
|
71
77
|
[
|
|
72
78
|
path.resolve(__dirname, '../../experiments/issue-207-monitor.py'),
|
|
73
|
-
mode,
|
|
79
|
+
mode === 'publish-race' ? 'publish-fault' : mode,
|
|
74
80
|
logPath,
|
|
75
81
|
...command,
|
|
76
82
|
],
|
|
77
|
-
{ encoding: 'utf8' }
|
|
83
|
+
{ encoding: 'utf8', env }
|
|
78
84
|
);
|
|
79
85
|
expect(result.status, result.stderr).toBe(0);
|
|
80
86
|
const state = cpu.readState(record);
|
package/test/regression-224.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* `docker inspect --size`, and had no hook to shrink the layer first.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// Own snapshot lock even without the bunfig.toml preload (issue #234).
|
|
11
|
+
require('./setup/private-snapshot-lock');
|
|
10
12
|
const { describe, it, expect, afterEach } = require('bun:test');
|
|
11
13
|
const { spawn, spawnSync } = require('child_process');
|
|
12
14
|
const fs = require('fs');
|