@reconcrap/boss-recommend-mcp 1.1.4 → 1.1.6

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.
@@ -1,245 +1,413 @@
1
- const assert = require("node:assert/strict");
2
- const fs = require("node:fs");
3
- const os = require("node:os");
4
- const path = require("node:path");
5
-
6
- const { RecommendScreenCli, __testables } = require("./boss-recommend-screen-cli.cjs");
7
- const { __testables: captureTestables } = require("./scripts/capture-full-resume-canvas.cjs");
8
-
9
- class FakeRecommendScreenCli extends RecommendScreenCli {
10
- constructor(args, options = {}) {
11
- super(args);
12
- this.testCandidates = options.candidates || [];
13
- this.captureOutcomes = options.captureOutcomes || new Map();
14
- this.screeningByKey = options.screeningByKey || new Map();
15
- this.discoveryCalls = 0;
16
- this.lastCapturedCandidateKey = null;
17
- }
18
-
19
- async connect() {}
20
-
21
- async disconnect() {}
22
-
23
- async getDetailClosedState() {
24
- return { closed: true, reason: "test" };
25
- }
26
-
27
- async closeDetailPage() {
28
- return true;
29
- }
30
-
31
- async waitForListReady() {
32
- return true;
33
- }
34
-
35
- async ensureHealthyListViewport() {
36
- return {
37
- ok: true,
38
- state: { ok: true }
39
- };
40
- }
41
-
42
- async discoverCandidates() {
43
- if (this.discoveryCalls === 0) {
44
- for (const candidate of this.testCandidates) {
45
- this.candidateByKey.set(candidate.key, candidate);
46
- this.discoveredKeys.add(candidate.key);
47
- this.candidateQueue.push(candidate.key);
48
- this.insertCounter += 1;
49
- this.insertedAt.set(candidate.key, this.insertCounter);
50
- }
51
- this.discoveryCalls += 1;
52
- return {
53
- ok: true,
54
- added: this.testCandidates.length,
55
- candidate_count: this.testCandidates.length,
56
- total_cards: this.testCandidates.length
57
- };
58
- }
59
- this.discoveryCalls += 1;
60
- return {
61
- ok: true,
62
- added: 0,
63
- candidate_count: this.testCandidates.length,
64
- total_cards: this.testCandidates.length
65
- };
66
- }
67
-
68
- async scrollAndLoadMore() {
69
- return {
70
- before: {
71
- candidateCount: this.testCandidates.length,
72
- scrollTop: 0,
73
- scrollHeight: 100
74
- },
75
- after: {
76
- candidateCount: this.testCandidates.length,
77
- scrollTop: 0,
78
- scrollHeight: 100
79
- },
80
- bottom: {
81
- isBottom: true
82
- }
83
- };
84
- }
85
-
86
- async clickCandidate() {}
87
-
88
- async ensureDetailOpen() {
89
- return true;
90
- }
91
-
92
- async captureResumeImage(candidate) {
93
- const outcome = this.captureOutcomes.get(candidate.key);
94
- if (outcome instanceof Error) {
95
- throw outcome;
96
- }
97
- this.lastCapturedCandidateKey = candidate.key;
98
- return outcome || {
99
- stitchedImage: path.join(os.tmpdir(), `${candidate.key}.png`)
100
- };
101
- }
102
-
103
- async callVisionModel() {
104
- return this.screeningByKey.get(this.lastCapturedCandidateKey) || {
105
- passed: false,
106
- reason: "not matched",
107
- summary: "not matched"
108
- };
109
- }
110
-
111
- async favoriteCandidate() {
112
- return { actionTaken: "favorite" };
113
- }
114
-
115
- async greetCandidate() {
116
- return { actionTaken: "greet" };
117
- }
118
-
119
- async takeBreakIfNeeded() {}
120
-
121
- saveCsv() {}
122
-
123
- saveCheckpoint() {}
124
- }
125
-
126
- function createResumeCaptureError(message = "Resume canvas not found") {
127
- const error = new Error(message);
128
- error.code = "RESUME_CAPTURE_FAILED";
129
- error.retryable = true;
130
- return error;
131
- }
132
-
133
- function createArgs(tempDir) {
134
- return {
135
- baseUrl: "https://example.invalid/v1",
136
- apiKey: "test-key",
137
- model: "test-model",
138
- criteria: "test criteria",
139
- targetCount: null,
140
- maxGreetCount: null,
141
- port: 9222,
142
- output: path.join(tempDir, "result.csv"),
143
- checkpointPath: path.join(tempDir, "checkpoint.json"),
144
- pauseControlPath: path.join(tempDir, "pause.json"),
145
- resume: false,
146
- postAction: "none",
147
- postActionConfirmed: true,
148
- help: false,
149
- __provided: {
150
- baseUrl: true,
151
- apiKey: true,
152
- model: true,
153
- criteria: true,
154
- targetCount: true,
155
- maxGreetCount: false,
156
- port: true,
157
- postAction: true,
158
- postActionConfirmed: true
159
- }
160
- };
161
- }
162
-
163
- function testShouldAbortResumeProbeEarly() {
164
- const probe = {
165
- ok: false,
166
- reason: "NO_CRESUME_IFRAME",
167
- debug: {
168
- activeScopeCount: 0,
169
- totalResumeIframes: 0,
170
- visibleResumeIframes: 0
171
- }
172
- };
173
- const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
174
- probe,
175
- stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
176
- elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
177
- waitResumeMs: 60000
178
- });
179
- assert.equal(shouldAbort, true);
180
- }
181
-
182
- async function testSingleResumeCaptureFailureIsSkipped() {
183
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
184
- const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
185
- const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
186
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
187
- candidates: [badCandidate, goodCandidate],
188
- captureOutcomes: new Map([
189
- ["bad", createResumeCaptureError()],
190
- ["good", { stitchedImage: path.join(tempDir, "good.png") }]
191
- ]),
192
- screeningByKey: new Map([
193
- ["good", { passed: true, reason: "matched", summary: "matched" }]
194
- ])
195
- });
196
-
197
- const result = await cli.run();
198
- assert.equal(result.status, "COMPLETED");
199
- assert.equal(result.result.processed_count, 2);
200
- assert.equal(result.result.passed_count, 1);
201
- assert.equal(result.result.skipped_count, 1);
202
- assert.equal(cli.consecutiveResumeCaptureFailures, 0);
203
- }
204
-
205
- async function testConsecutiveResumeCaptureFailuresStillAbort() {
206
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
207
- const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
208
- const candidates = Array.from({ length: maxFailures }, (_, index) => ({
209
- key: `fail-${index + 1}`,
210
- geek_id: `fail-${index + 1}`,
211
- name: `fail-${index + 1}`
212
- }));
213
- const captureOutcomes = new Map(
214
- candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
215
- );
216
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
217
- candidates,
218
- captureOutcomes
219
- });
220
-
221
- await assert.rejects(
222
- () => cli.run(),
223
- (error) => {
224
- assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
225
- assert.match(error.message, /连续 .* 位候选人简历捕获失败/);
226
- assert.equal(error.rollback?.rollback_count, maxFailures);
227
- assert.equal(error.partial_result?.processed_count, 0);
228
- assert.equal(error.partial_result?.skipped_count, 0);
229
- assert.deepEqual(Array.from(cli.processedKeys), []);
230
- return true;
231
- }
232
- );
233
- }
234
-
235
- async function main() {
236
- testShouldAbortResumeProbeEarly();
237
- await testSingleResumeCaptureFailureIsSkipped();
238
- await testConsecutiveResumeCaptureFailuresStillAbort();
239
- console.log("recoverable resume failure tests passed");
240
- }
241
-
242
- main().catch((error) => {
243
- console.error(error);
244
- process.exit(1);
245
- });
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const sharp = require("sharp");
6
+
7
+ const { RecommendScreenCli, __testables } = require("./boss-recommend-screen-cli.cjs");
8
+ const { __testables: captureTestables } = require("./scripts/capture-full-resume-canvas.cjs");
9
+
10
+ class FakeRecommendScreenCli extends RecommendScreenCli {
11
+ constructor(args, options = {}) {
12
+ super(args);
13
+ this.testCandidates = options.candidates || [];
14
+ this.captureOutcomes = options.captureOutcomes || new Map();
15
+ this.screeningByKey = options.screeningByKey || new Map();
16
+ this.discoveryCalls = 0;
17
+ this.lastCapturedCandidateKey = null;
18
+ }
19
+
20
+ async connect() {}
21
+
22
+ async disconnect() {}
23
+
24
+ async getDetailClosedState() {
25
+ return { closed: true, reason: "test" };
26
+ }
27
+
28
+ async closeDetailPage() {
29
+ return true;
30
+ }
31
+
32
+ async waitForListReady() {
33
+ return true;
34
+ }
35
+
36
+ async ensureHealthyListViewport() {
37
+ return {
38
+ ok: true,
39
+ state: { ok: true }
40
+ };
41
+ }
42
+
43
+ async discoverCandidates() {
44
+ if (this.discoveryCalls === 0) {
45
+ for (const candidate of this.testCandidates) {
46
+ this.candidateByKey.set(candidate.key, candidate);
47
+ this.discoveredKeys.add(candidate.key);
48
+ this.candidateQueue.push(candidate.key);
49
+ this.insertCounter += 1;
50
+ this.insertedAt.set(candidate.key, this.insertCounter);
51
+ }
52
+ this.discoveryCalls += 1;
53
+ return {
54
+ ok: true,
55
+ added: this.testCandidates.length,
56
+ candidate_count: this.testCandidates.length,
57
+ total_cards: this.testCandidates.length
58
+ };
59
+ }
60
+ this.discoveryCalls += 1;
61
+ return {
62
+ ok: true,
63
+ added: 0,
64
+ candidate_count: this.testCandidates.length,
65
+ total_cards: this.testCandidates.length
66
+ };
67
+ }
68
+
69
+ async scrollAndLoadMore() {
70
+ return {
71
+ before: {
72
+ candidateCount: this.testCandidates.length,
73
+ scrollTop: 0,
74
+ scrollHeight: 100
75
+ },
76
+ after: {
77
+ candidateCount: this.testCandidates.length,
78
+ scrollTop: 0,
79
+ scrollHeight: 100
80
+ },
81
+ bottom: {
82
+ isBottom: true
83
+ }
84
+ };
85
+ }
86
+
87
+ async clickCandidate() {}
88
+
89
+ async ensureDetailOpen() {
90
+ return true;
91
+ }
92
+
93
+ async captureResumeImage(candidate) {
94
+ const outcome = this.captureOutcomes.get(candidate.key);
95
+ if (outcome instanceof Error) {
96
+ throw outcome;
97
+ }
98
+ this.lastCapturedCandidateKey = candidate.key;
99
+ return outcome || {
100
+ stitchedImage: path.join(os.tmpdir(), `${candidate.key}.png`)
101
+ };
102
+ }
103
+
104
+ async callVisionModel() {
105
+ return this.screeningByKey.get(this.lastCapturedCandidateKey) || {
106
+ passed: false,
107
+ reason: "not matched",
108
+ summary: "not matched"
109
+ };
110
+ }
111
+
112
+ async favoriteCandidate() {
113
+ return { actionTaken: "favorite" };
114
+ }
115
+
116
+ async greetCandidate() {
117
+ return { actionTaken: "greet" };
118
+ }
119
+
120
+ async takeBreakIfNeeded() {}
121
+
122
+ saveCsv() {}
123
+
124
+ saveCheckpoint() {}
125
+ }
126
+
127
+ function createResumeCaptureError(message = "Resume canvas not found") {
128
+ const error = new Error(message);
129
+ error.code = "RESUME_CAPTURE_FAILED";
130
+ error.retryable = true;
131
+ return error;
132
+ }
133
+
134
+ function createArgs(tempDir) {
135
+ return {
136
+ baseUrl: "https://example.invalid/v1",
137
+ apiKey: "test-key",
138
+ model: "test-model",
139
+ criteria: "test criteria",
140
+ targetCount: null,
141
+ maxGreetCount: null,
142
+ port: 9222,
143
+ output: path.join(tempDir, "result.csv"),
144
+ checkpointPath: path.join(tempDir, "checkpoint.json"),
145
+ pauseControlPath: path.join(tempDir, "pause.json"),
146
+ resume: false,
147
+ postAction: "none",
148
+ postActionConfirmed: true,
149
+ help: false,
150
+ __provided: {
151
+ baseUrl: true,
152
+ apiKey: true,
153
+ model: true,
154
+ criteria: true,
155
+ targetCount: true,
156
+ maxGreetCount: false,
157
+ port: true,
158
+ postAction: true,
159
+ postActionConfirmed: true
160
+ }
161
+ };
162
+ }
163
+
164
+ function testShouldAbortResumeProbeEarly() {
165
+ const probe = {
166
+ ok: false,
167
+ reason: "NO_CRESUME_IFRAME",
168
+ debug: {
169
+ activeScopeCount: 0,
170
+ totalResumeIframes: 0,
171
+ visibleResumeIframes: 0
172
+ }
173
+ };
174
+ const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
175
+ probe,
176
+ stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
177
+ elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
178
+ waitResumeMs: 60000
179
+ });
180
+ assert.equal(shouldAbort, true);
181
+ }
182
+
183
+ async function testSingleResumeCaptureFailureIsSkipped() {
184
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
185
+ const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
186
+ const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
187
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
188
+ candidates: [badCandidate, goodCandidate],
189
+ captureOutcomes: new Map([
190
+ ["bad", createResumeCaptureError()],
191
+ ["good", { stitchedImage: path.join(tempDir, "good.png") }]
192
+ ]),
193
+ screeningByKey: new Map([
194
+ ["good", { passed: true, reason: "matched", summary: "matched" }]
195
+ ])
196
+ });
197
+
198
+ const result = await cli.run();
199
+ assert.equal(result.status, "COMPLETED");
200
+ assert.equal(result.result.processed_count, 2);
201
+ assert.equal(result.result.passed_count, 1);
202
+ assert.equal(result.result.skipped_count, 1);
203
+ assert.equal(cli.consecutiveResumeCaptureFailures, 0);
204
+ }
205
+
206
+ async function testConsecutiveResumeCaptureFailuresStillAbort() {
207
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
208
+ const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
209
+ const candidates = Array.from({ length: maxFailures }, (_, index) => ({
210
+ key: `fail-${index + 1}`,
211
+ geek_id: `fail-${index + 1}`,
212
+ name: `fail-${index + 1}`
213
+ }));
214
+ const captureOutcomes = new Map(
215
+ candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
216
+ );
217
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
218
+ candidates,
219
+ captureOutcomes
220
+ });
221
+
222
+ await assert.rejects(
223
+ () => cli.run(),
224
+ (error) => {
225
+ assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
226
+ assert.match(error.message, /连续 .* 位候选人简历捕获失败/);
227
+ assert.equal(error.rollback?.rollback_count, maxFailures);
228
+ assert.equal(error.partial_result?.processed_count, 0);
229
+ assert.equal(error.partial_result?.skipped_count, 0);
230
+ assert.deepEqual(Array.from(cli.processedKeys), []);
231
+ return true;
232
+ }
233
+ );
234
+ }
235
+
236
+ async function testPageExhaustedBeforeTargetShouldRaiseRecoverableError() {
237
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-exhausted-"));
238
+ const args = createArgs(tempDir);
239
+ args.targetCount = 5;
240
+ const cli = new FakeRecommendScreenCli(args);
241
+ cli.scrollAndLoadMore = async () => ({
242
+ before: {
243
+ candidateCount: 0,
244
+ scrollTop: 120,
245
+ scrollHeight: 900
246
+ },
247
+ after: {
248
+ candidateCount: 0,
249
+ scrollTop: 900,
250
+ scrollHeight: 900
251
+ },
252
+ bottom: {
253
+ isBottom: true,
254
+ finished_wrap_visible: true,
255
+ refresh_button_visible: true,
256
+ refresh_button_text: "刷新"
257
+ }
258
+ });
259
+
260
+ await assert.rejects(
261
+ () => cli.run(),
262
+ (error) => {
263
+ assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
264
+ assert.equal(error.retryable, true);
265
+ assert.equal(error.partial_result?.processed_count, 0);
266
+ assert.equal(error.partial_result?.output_csv, args.output);
267
+ assert.equal(error.partial_result?.checkpoint_path, args.checkpointPath);
268
+ assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
269
+ assert.equal(error.page_exhaustion?.reason, "bottom_reached");
270
+ assert.equal(error.page_exhaustion?.bottom?.finished_wrap_visible, true);
271
+ assert.equal(error.page_exhaustion?.bottom?.refresh_button_visible, true);
272
+ return true;
273
+ }
274
+ );
275
+ }
276
+
277
+ async function testPageExhaustedWithoutTargetShouldStillComplete() {
278
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-complete-"));
279
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir));
280
+ cli.scrollAndLoadMore = async () => ({
281
+ before: {
282
+ candidateCount: 0,
283
+ scrollTop: 120,
284
+ scrollHeight: 900
285
+ },
286
+ after: {
287
+ candidateCount: 0,
288
+ scrollTop: 900,
289
+ scrollHeight: 900
290
+ },
291
+ bottom: {
292
+ isBottom: true,
293
+ finished_wrap_visible: true,
294
+ refresh_button_visible: true,
295
+ refresh_button_text: "刷新"
296
+ }
297
+ });
298
+
299
+ const result = await cli.run();
300
+ assert.equal(result.status, "COMPLETED");
301
+ assert.equal(result.result.processed_count, 0);
302
+ assert.equal(result.result.output_csv, cli.args.output);
303
+ assert.equal(result.result.checkpoint_path, cli.args.checkpointPath);
304
+ assert.equal(result.result.completion_reason, "page_exhausted");
305
+ }
306
+
307
+ async function testStitchWithSharpShouldComposeExpectedImage() {
308
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-sharp-stitch-"));
309
+ const chunkA = path.join(tempDir, "chunk_000.png");
310
+ const chunkB = path.join(tempDir, "chunk_001.png");
311
+ const chunkC = path.join(tempDir, "chunk_002.png");
312
+ const metadataPath = path.join(tempDir, "chunks.json");
313
+ const outputPath = path.join(tempDir, "stitched.png");
314
+
315
+ await sharp({
316
+ create: { width: 20, height: 100, channels: 3, background: { r: 255, g: 0, b: 0 } }
317
+ }).png().toFile(chunkA);
318
+ await sharp({
319
+ create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 255, b: 0 } }
320
+ }).png().toFile(chunkB);
321
+ await sharp({
322
+ create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 0, b: 255 } }
323
+ }).png().toFile(chunkC);
324
+
325
+ fs.writeFileSync(
326
+ metadataPath,
327
+ JSON.stringify({
328
+ chunks: [
329
+ { index: 0, file: chunkA, scrollTop: 0, clipHeightCss: 100 },
330
+ { index: 1, file: chunkB, scrollTop: 80, clipHeightCss: 100 },
331
+ { index: 2, file: chunkC, scrollTop: 160, clipHeightCss: 100 }
332
+ ]
333
+ }),
334
+ "utf8"
335
+ );
336
+
337
+ const stitched = await captureTestables.stitchWithSharp(metadataPath, outputPath);
338
+ const outputMeta = await sharp(outputPath).metadata();
339
+
340
+ assert.equal(stitched.ok, true);
341
+ assert.equal(stitched.engine, "sharp");
342
+ assert.equal(stitched.segments, 3);
343
+ assert.equal(outputMeta.width, 20);
344
+ assert.equal(outputMeta.height, 260);
345
+ assert.equal(Array.isArray(stitched.used), true);
346
+ assert.equal(stitched.used.length, 3);
347
+ }
348
+
349
+ function testStitchWithAvailablePythonShouldFallbackToPython() {
350
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-fallback-"));
351
+ const stitchScript = path.join(tempDir, "stitch.py");
352
+ fs.writeFileSync(stitchScript, "print('ok')", "utf8");
353
+ const calls = [];
354
+ const result = captureTestables.stitchWithAvailablePython(
355
+ stitchScript,
356
+ path.join(tempDir, "meta.json"),
357
+ path.join(tempDir, "out.png"),
358
+ (command) => {
359
+ calls.push(command);
360
+ if (command === "python3") {
361
+ return {
362
+ status: 1,
363
+ signal: null,
364
+ error: null,
365
+ stderr: "python3 failed",
366
+ stdout: ""
367
+ };
368
+ }
369
+ return {
370
+ status: 0,
371
+ signal: null,
372
+ error: null,
373
+ stderr: "",
374
+ stdout: "ok"
375
+ };
376
+ }
377
+ );
378
+
379
+ assert.equal(result.ok, true);
380
+ assert.equal(result.command, "python");
381
+ assert.deepEqual(calls, ["python3", "python"]);
382
+ }
383
+
384
+ function testStitchWithAvailablePythonShouldFailWhenScriptMissing() {
385
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-missing-"));
386
+ const result = captureTestables.stitchWithAvailablePython(
387
+ path.join(tempDir, "missing.py"),
388
+ path.join(tempDir, "meta.json"),
389
+ path.join(tempDir, "out.png")
390
+ );
391
+
392
+ assert.equal(result.ok, false);
393
+ assert.equal(Array.isArray(result.attempts), true);
394
+ assert.equal(result.attempts.length, 2);
395
+ assert.equal(result.attempts[0].command, "python3");
396
+ }
397
+
398
+ async function main() {
399
+ testShouldAbortResumeProbeEarly();
400
+ await testSingleResumeCaptureFailureIsSkipped();
401
+ await testConsecutiveResumeCaptureFailuresStillAbort();
402
+ await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
403
+ await testPageExhaustedWithoutTargetShouldStillComplete();
404
+ await testStitchWithSharpShouldComposeExpectedImage();
405
+ testStitchWithAvailablePythonShouldFallbackToPython();
406
+ testStitchWithAvailablePythonShouldFailWhenScriptMissing();
407
+ console.log("recoverable resume failure tests passed");
408
+ }
409
+
410
+ main().catch((error) => {
411
+ console.error(error);
412
+ process.exit(1);
413
+ });