@reconcrap/boss-recommend-mcp 1.2.4 → 1.2.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,832 +1,880 @@
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, parseArgs, __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
- pageScope: "recommend",
143
- port: 9222,
144
- output: path.join(tempDir, "result.csv"),
145
- checkpointPath: path.join(tempDir, "checkpoint.json"),
146
- pauseControlPath: path.join(tempDir, "pause.json"),
147
- resume: false,
148
- postAction: "none",
149
- postActionConfirmed: true,
150
- help: false,
151
- __provided: {
152
- baseUrl: true,
153
- apiKey: true,
154
- model: true,
155
- criteria: true,
156
- targetCount: true,
157
- maxGreetCount: false,
158
- pageScope: true,
159
- port: true,
160
- postAction: true,
161
- postActionConfirmed: true
162
- }
163
- };
164
- }
165
-
166
- function testShouldAbortResumeProbeEarly() {
167
- const probe = {
168
- ok: false,
169
- reason: "NO_CRESUME_IFRAME",
170
- debug: {
171
- activeScopeCount: 0,
172
- totalResumeIframes: 0,
173
- visibleResumeIframes: 0
174
- }
175
- };
176
- const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
177
- probe,
178
- stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
179
- elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
180
- waitResumeMs: 60000
181
- });
182
- assert.equal(shouldAbort, true);
183
- }
184
-
185
- async function testSingleResumeCaptureFailureIsSkipped() {
186
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
187
- const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
188
- const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
189
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
190
- candidates: [badCandidate, goodCandidate],
191
- captureOutcomes: new Map([
192
- ["bad", createResumeCaptureError()],
193
- ["good", { stitchedImage: path.join(tempDir, "good.png") }]
194
- ]),
195
- screeningByKey: new Map([
196
- ["good", { passed: true, reason: "matched", summary: "matched" }]
197
- ])
198
- });
199
-
200
- const result = await cli.run();
201
- assert.equal(result.status, "COMPLETED");
202
- assert.equal(result.result.processed_count, 2);
203
- assert.equal(result.result.passed_count, 1);
204
- assert.equal(result.result.skipped_count, 1);
205
- assert.equal(cli.consecutiveResumeCaptureFailures, 0);
206
- }
207
-
208
- async function testConsecutiveResumeCaptureFailuresStillAbort() {
209
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
210
- const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
211
- const candidates = Array.from({ length: maxFailures }, (_, index) => ({
212
- key: `fail-${index + 1}`,
213
- geek_id: `fail-${index + 1}`,
214
- name: `fail-${index + 1}`
215
- }));
216
- const captureOutcomes = new Map(
217
- candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
218
- );
219
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
220
- candidates,
221
- captureOutcomes
222
- });
223
-
224
- await assert.rejects(
225
- () => cli.run(),
226
- (error) => {
227
- assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
228
- assert.match(error.message, /连续 .* 位候选人简历捕获失败/);
229
- assert.equal(error.rollback?.rollback_count, maxFailures);
230
- assert.equal(error.partial_result?.processed_count, 0);
231
- assert.equal(error.partial_result?.skipped_count, 0);
232
- assert.deepEqual(Array.from(cli.processedKeys), []);
233
- return true;
234
- }
235
- );
236
- }
237
-
238
- async function testPageExhaustedBeforeTargetShouldRaiseRecoverableError() {
239
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-exhausted-"));
240
- const args = createArgs(tempDir);
241
- args.targetCount = 5;
242
- const cli = new FakeRecommendScreenCli(args);
243
- cli.scrollAndLoadMore = async () => ({
244
- before: {
245
- candidateCount: 0,
246
- scrollTop: 120,
247
- scrollHeight: 900
248
- },
249
- after: {
250
- candidateCount: 0,
251
- scrollTop: 900,
252
- scrollHeight: 900
253
- },
254
- bottom: {
255
- isBottom: true,
256
- finished_wrap_visible: true,
257
- refresh_button_visible: true,
258
- refresh_button_text: "刷新"
259
- }
260
- });
261
-
262
- await assert.rejects(
263
- () => cli.run(),
264
- (error) => {
265
- assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
266
- assert.equal(error.retryable, true);
267
- assert.equal(error.partial_result?.processed_count, 0);
268
- assert.equal(error.partial_result?.output_csv, args.output);
269
- assert.equal(error.partial_result?.checkpoint_path, args.checkpointPath);
270
- assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
271
- assert.equal(error.page_exhaustion?.reason, "bottom_reached");
272
- assert.equal(error.page_exhaustion?.bottom?.finished_wrap_visible, true);
273
- assert.equal(error.page_exhaustion?.bottom?.refresh_button_visible, true);
274
- return true;
275
- }
276
- );
277
- }
278
-
279
- async function testPageExhaustedWithoutTargetShouldStillComplete() {
280
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-complete-"));
281
- const cli = new FakeRecommendScreenCli(createArgs(tempDir));
282
- cli.scrollAndLoadMore = async () => ({
283
- before: {
284
- candidateCount: 0,
285
- scrollTop: 120,
286
- scrollHeight: 900
287
- },
288
- after: {
289
- candidateCount: 0,
290
- scrollTop: 900,
291
- scrollHeight: 900
292
- },
293
- bottom: {
294
- isBottom: true,
295
- finished_wrap_visible: true,
296
- refresh_button_visible: true,
297
- refresh_button_text: "刷新"
298
- }
299
- });
300
-
301
- const result = await cli.run();
302
- assert.equal(result.status, "COMPLETED");
303
- assert.equal(result.result.processed_count, 0);
304
- assert.equal(result.result.output_csv, cli.args.output);
305
- assert.equal(result.result.checkpoint_path, cli.args.checkpointPath);
306
- assert.equal(result.result.completion_reason, "page_exhausted");
307
- }
308
-
309
- async function testFeaturedShouldUseNetworkResumeOnly() {
310
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
311
- const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
312
- const args = createArgs(tempDir);
313
- args.pageScope = "featured";
314
- const cli = new FakeRecommendScreenCli(args, {
315
- candidates: [candidate]
316
- });
317
-
318
- cli.waitForNetworkResumeCandidateInfo = async () => ({
319
- name: "network candidate",
320
- school: "测试大学",
321
- major: "计算机",
322
- company: "OpenClaw",
323
- position: "工程师",
324
- resumeText: "有丰富 MCP 经验"
325
- });
326
- cli.callTextModel = async () => ({
327
- passed: true,
328
- reason: "network pass",
329
- summary: "network summary"
330
- });
331
- cli.captureResumeImage = async () => {
332
- throw new Error("capture should not be called");
333
- };
334
-
335
- const result = await cli.run();
336
- assert.equal(result.status, "COMPLETED");
337
- assert.equal(result.result.passed_count, 1);
338
- assert.equal(result.result.resume_source, "network");
339
- }
340
-
341
- async function testRecommendShouldKeepImageCaptureEvenWhenNetworkResumeExists() {
342
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-recommend-image-main-"));
343
- const candidate = { key: "img-main-1", geek_id: "img-main-1", name: "recommend image main candidate" };
344
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
345
- candidates: [candidate],
346
- captureOutcomes: new Map([
347
- ["img-main-1", { stitchedImage: path.join(tempDir, "img-main-1.png") }]
348
- ]),
349
- screeningByKey: new Map([
350
- ["img-main-1", { passed: true, reason: "image path used", summary: "image path used" }]
351
- ])
352
- });
353
- cli.waitForNetworkResumeCandidateInfo = async () => ({
354
- resumeText: "这段 network 文本在 recommend 页面不应被用于筛选"
355
- });
356
- cli.callTextModel = async () => {
357
- throw new Error("text model should not be called for recommend scope");
358
- };
359
-
360
- const result = await cli.run();
361
- assert.equal(result.status, "COMPLETED");
362
- assert.equal(result.result.passed_count, 1);
363
- assert.equal(result.result.resume_source, "image_fallback");
364
- }
365
-
366
- async function testNetworkMissShouldFallbackToImageCapture() {
367
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-fallback-"));
368
- const candidate = { key: "img-1", geek_id: "img-1", name: "image candidate" };
369
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
370
- candidates: [candidate],
371
- captureOutcomes: new Map([
372
- ["img-1", { stitchedImage: path.join(tempDir, "img-1.png") }]
373
- ]),
374
- screeningByKey: new Map([
375
- ["img-1", { passed: false, reason: "image path used", summary: "image path used" }]
376
- ])
377
- });
378
- cli.waitForNetworkResumeCandidateInfo = async () => null;
379
-
380
- const result = await cli.run();
381
- assert.equal(result.status, "COMPLETED");
382
- assert.equal(result.result.resume_source, "image_fallback");
383
- }
384
-
385
- async function testVisionModelFailureShouldSkipCandidateAndContinue() {
386
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-vision-failure-skip-"));
387
- const first = { key: "vision-fail-1", geek_id: "vision-fail-1", name: "vision-fail-1" };
388
- const second = { key: "vision-pass-2", geek_id: "vision-pass-2", name: "vision-pass-2" };
389
- const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
390
- candidates: [first, second],
391
- captureOutcomes: new Map([
392
- ["vision-fail-1", { stitchedImage: path.join(tempDir, "vision-fail-1.png") }],
393
- ["vision-pass-2", { stitchedImage: path.join(tempDir, "vision-pass-2.png") }]
394
- ]),
395
- screeningByKey: new Map([
396
- ["vision-pass-2", { passed: true, reason: "ok", summary: "ok" }]
397
- ])
398
- });
399
-
400
- cli.callVisionModel = async () => {
401
- if (cli.lastCapturedCandidateKey === "vision-fail-1") {
402
- const error = new Error("model backend timeout");
403
- error.code = "VISION_MODEL_FAILED";
404
- throw error;
405
- }
406
- return {
407
- passed: true,
408
- reason: "ok",
409
- summary: "ok"
410
- };
411
- };
412
-
413
- const result = await cli.run();
414
- assert.equal(result.status, "COMPLETED");
415
- assert.equal(result.result.processed_count, 2);
416
- assert.equal(result.result.passed_count, 1);
417
- assert.equal(result.result.skipped_count, 1);
418
- }
419
-
420
- async function testFeaturedNetworkMissShouldSkipWithoutImageCapture() {
421
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-network-only-"));
422
- const args = createArgs(tempDir);
423
- args.pageScope = "featured";
424
- const candidate = { key: "featured-no-network", geek_id: "featured-no-network", name: "featured no network" };
425
- const cli = new FakeRecommendScreenCli(args, {
426
- candidates: [candidate]
427
- });
428
- cli.waitForNetworkResumeCandidateInfo = async () => null;
429
- cli.captureResumeImage = async () => {
430
- throw new Error("capture should not be called for featured scope");
431
- };
432
-
433
- const result = await cli.run();
434
- assert.equal(result.status, "COMPLETED");
435
- assert.equal(result.result.processed_count, 1);
436
- assert.equal(result.result.passed_count, 0);
437
- assert.equal(result.result.skipped_count, 1);
438
- assert.equal(result.result.resume_source, "network");
439
- }
440
-
441
- async function testFeaturedFavoriteShouldNotUseDomFallback() {
442
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-"));
443
- const args = createArgs(tempDir);
444
- args.pageScope = "featured";
445
- const calibrationPath = path.join(tempDir, "favorite-calibration.json");
446
- fs.writeFileSync(calibrationPath, JSON.stringify({
447
- favoritePosition: {
448
- pageX: 120,
449
- pageY: 220,
450
- canvasX: 0,
451
- canvasY: 0
452
- }
453
- }, null, 2));
454
- args.calibrationPath = calibrationPath;
455
- const cli = new RecommendScreenCli(args);
456
- let evaluateCalls = 0;
457
- let clickCalls = 0;
458
- cli.evaluate = async () => {
459
- evaluateCalls += 1;
460
- return { ok: true };
461
- };
462
- cli.simulateHumanClick = async () => {
463
- clickCalls += 1;
464
- cli.favoriteActionEvents.push({ action: "add", ts: Date.now(), source: "test", url: "userMark/add" });
465
- };
466
- const result = await cli.favoriteCandidate();
467
- assert.equal(result.actionTaken, "favorite");
468
- assert.equal(clickCalls, 1);
469
- assert.equal(evaluateCalls, 0);
470
- }
471
-
472
- async function testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested() {
473
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-already-"));
474
- const args = createArgs(tempDir);
475
- args.pageScope = "featured";
476
- const calibrationPath = path.join(tempDir, "favorite-calibration.json");
477
- fs.writeFileSync(calibrationPath, JSON.stringify({
478
- favoritePosition: {
479
- pageX: 120,
480
- pageY: 220,
481
- canvasX: 0,
482
- canvasY: 0
483
- }
484
- }, null, 2));
485
- args.calibrationPath = calibrationPath;
486
- const cli = new RecommendScreenCli(args);
487
- let clickCalls = 0;
488
- cli.simulateHumanClick = async () => {
489
- clickCalls += 1;
490
- };
491
- const result = await cli.favoriteCandidate({ alreadyInterested: true });
492
- assert.equal(result.actionTaken, "already_favorited");
493
- assert.equal(result.source, "network_profile");
494
- assert.equal(clickCalls, 0);
495
- }
496
-
497
- async function testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd() {
498
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-del-add-"));
499
- const args = createArgs(tempDir);
500
- args.pageScope = "featured";
501
- const calibrationPath = path.join(tempDir, "favorite-calibration.json");
502
- fs.writeFileSync(calibrationPath, JSON.stringify({
503
- favoritePosition: {
504
- pageX: 120,
505
- pageY: 220,
506
- canvasX: 0,
507
- canvasY: 0
508
- }
509
- }, null, 2));
510
- args.calibrationPath = calibrationPath;
511
- const cli = new RecommendScreenCli(args);
512
- let clickCalls = 0;
513
- cli.simulateHumanClick = async () => {
514
- clickCalls += 1;
515
- cli.favoriteActionEvents.push({
516
- action: clickCalls === 1 ? "del" : "add",
517
- ts: Date.now(),
518
- source: "test",
519
- url: clickCalls === 1 ? "userMark/del" : "userMark/add"
520
- });
521
- };
522
- const result = await cli.favoriteCandidate();
523
- assert.equal(result.actionTaken, "already_favorited");
524
- assert.equal(result.re_favorited, true);
525
- assert.equal(clickCalls, 2);
526
- }
527
-
528
- async function testFeaturedFavoriteWithoutCalibrationShouldFail() {
529
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-missing-cal-"));
530
- const args = createArgs(tempDir);
531
- args.pageScope = "featured";
532
- args.calibrationPath = path.join(tempDir, "missing-calibration.json");
533
- const cli = new RecommendScreenCli(args);
534
- await assert.rejects(
535
- () => cli.favoriteCandidate(),
536
- (error) => {
537
- assert.equal(error.code, "FAVORITE_CALIBRATION_REQUIRED");
538
- return true;
539
- }
540
- );
541
- }
542
-
543
- function testFavoriteActionParserShouldSupportBodySignals() {
544
- const addFromJson = __testables.parseFavoriteActionFromPostData(JSON.stringify({
545
- action: "star-interest-click",
546
- p3: 1
547
- }));
548
- const delFromForm = __testables.parseFavoriteActionFromPostData("action=star-interest-click&p3=0");
549
- assert.equal(addFromJson, "add");
550
- assert.equal(delFromForm, "del");
551
- }
552
-
553
- function testFavoriteActionParserShouldSupportFallbackRequestShape() {
554
- const action = __testables.parseFavoriteActionFromRequest(
555
- "https://www.zhipin.com/wapi/zpgeek/favorite/operate",
556
- JSON.stringify({ op: "add", geekId: "abc" })
557
- );
558
- assert.equal(action, "add");
559
- }
560
-
561
- function testFavoriteActionParserShouldSupportWebSocketPayload() {
562
- const addFromWsJson = __testables.parseFavoriteActionFromWsPayload(JSON.stringify({
563
- action: "star-interest-click",
564
- p3: 1
565
- }));
566
- const delFromWsForm = __testables.parseFavoriteActionFromWsPayload("action=star-interest-click&p3=0");
567
- assert.equal(addFromWsJson, "add");
568
- assert.equal(delFromWsForm, "del");
569
- }
570
-
571
- function testFavoriteActionParserShouldOnlyTrustKnownRequestShapes() {
572
- const unknown = __testables.parseFavoriteActionFromKnownRequest(
573
- "https://www.zhipin.com/wapi/other/metrics",
574
- JSON.stringify({ action: "add", p3: 1 })
575
- );
576
- const actionLog = __testables.parseFavoriteActionFromKnownRequest(
577
- "https://www.zhipin.com/wapi/zplog/actionLog/common.json",
578
- JSON.stringify({ action: "star-interest-click", p3: 1 })
579
- );
580
- const userMark = __testables.parseFavoriteActionFromKnownRequest(
581
- "https://www.zhipin.com/wapi/zpgeek/userMark/add",
582
- ""
583
- );
584
- assert.equal(unknown, null);
585
- assert.equal(actionLog, "add");
586
- assert.equal(userMark, "add");
587
- }
588
-
589
- function testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom() {
590
- const loadMore = __testables.classifyFinishedWrapState("滚动加载更多", false);
591
- const loading = __testables.classifyFinishedWrapState("正在加载数据...", false);
592
- const noMore = __testables.classifyFinishedWrapState("没有更多人选", false);
593
- const refreshOnly = __testables.classifyFinishedWrapState("", true);
594
-
595
- assert.equal(loadMore.isBottom, false);
596
- assert.equal(loadMore.matched_load_more_keyword, "滚动加载更多");
597
- assert.equal(loading.isBottom, false);
598
- assert.equal(loading.matched_load_more_keyword, "正在加载");
599
- assert.equal(noMore.isBottom, true);
600
- assert.equal(noMore.matched_bottom_keyword, "没有更多");
601
- assert.equal(refreshOnly.isBottom, true);
602
- assert.equal(refreshOnly.reason, "refresh_button_visible");
603
- }
604
-
605
- async function testGetCenteredCandidateClickPointShouldSupportLatestSelector() {
606
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-latest-click-locator-"));
607
- const args = createArgs(tempDir);
608
- args.pageScope = "latest";
609
- const cli = new RecommendScreenCli(args);
610
-
611
- let expressionCaptured = "";
612
- cli.evaluate = async (expression) => {
613
- expressionCaptured = String(expression || "");
614
- return {
615
- ok: true,
616
- x: 100,
617
- y: 100,
618
- width: 120,
619
- height: 64
620
- };
621
- };
622
-
623
- const result = await cli.getCenteredCandidateClickPoint({
624
- key: "latest-test-key",
625
- geek_id: "latest-test-key"
626
- });
627
-
628
- assert.equal(result.ok, true);
629
- assert.equal(expressionCaptured.includes(".candidate-card-wrap .card-inner[data-geek]"), true);
630
- assert.equal(expressionCaptured.includes("getAttribute('data-geek')"), true);
631
- }
632
-
633
- async function testFeaturedPostActionFailureShouldStillRecordPassedCandidate() {
634
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-featured-action-failure-"));
635
- const args = createArgs(tempDir);
636
- args.pageScope = "featured";
637
- args.postAction = "favorite";
638
- const candidate = { key: "featured-fav-fail", geek_id: "featured-fav-fail", name: "featured candidate" };
639
- const cli = new FakeRecommendScreenCli(args, {
640
- candidates: [candidate]
641
- });
642
-
643
- cli.waitForNetworkResumeCandidateInfo = async () => ({
644
- name: "featured candidate",
645
- school: "测试大学",
646
- major: "人工智能",
647
- company: "测试公司",
648
- position: "算法工程师",
649
- resumeText: "满足测试标准"
650
- });
651
- cli.callTextModel = async () => ({
652
- passed: true,
653
- reason: "通过",
654
- summary: "通过"
655
- });
656
- cli.favoriteCandidate = async () => {
657
- const error = new Error("精选页收藏未检测到 network add 成功信号。");
658
- error.code = "FAVORITE_BUTTON_FAILED";
659
- throw error;
660
- };
661
-
662
- const result = await cli.run();
663
- assert.equal(result.status, "COMPLETED");
664
- assert.equal(result.result.processed_count, 1);
665
- assert.equal(result.result.passed_count, 1);
666
- assert.equal(result.result.skipped_count, 0);
667
- assert.equal(cli.passedCandidates.length, 1);
668
- assert.equal(cli.passedCandidates[0].action, "favorite_failed");
669
- assert.match(cli.passedCandidates[0].reason, /\[favorite失败]/);
670
- }
671
-
672
- async function testStitchWithSharpShouldComposeExpectedImage() {
673
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-sharp-stitch-"));
674
- const chunkA = path.join(tempDir, "chunk_000.png");
675
- const chunkB = path.join(tempDir, "chunk_001.png");
676
- const chunkC = path.join(tempDir, "chunk_002.png");
677
- const metadataPath = path.join(tempDir, "chunks.json");
678
- const outputPath = path.join(tempDir, "stitched.png");
679
-
680
- await sharp({
681
- create: { width: 20, height: 100, channels: 3, background: { r: 255, g: 0, b: 0 } }
682
- }).png().toFile(chunkA);
683
- await sharp({
684
- create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 255, b: 0 } }
685
- }).png().toFile(chunkB);
686
- await sharp({
687
- create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 0, b: 255 } }
688
- }).png().toFile(chunkC);
689
-
690
- fs.writeFileSync(
691
- metadataPath,
692
- JSON.stringify({
693
- chunks: [
694
- { index: 0, file: chunkA, scrollTop: 0, clipHeightCss: 100 },
695
- { index: 1, file: chunkB, scrollTop: 80, clipHeightCss: 100 },
696
- { index: 2, file: chunkC, scrollTop: 160, clipHeightCss: 100 }
697
- ]
698
- }),
699
- "utf8"
700
- );
701
-
702
- const stitched = await captureTestables.stitchWithSharp(metadataPath, outputPath);
703
- const outputMeta = await sharp(outputPath).metadata();
704
-
705
- assert.equal(stitched.ok, true);
706
- assert.equal(stitched.engine, "sharp");
707
- assert.equal(stitched.segments, 3);
708
- assert.equal(outputMeta.width, 20);
709
- assert.equal(outputMeta.height, 260);
710
- assert.equal(Array.isArray(stitched.used), true);
711
- assert.equal(stitched.used.length, 3);
712
- }
713
-
714
- function testStitchWithAvailablePythonShouldFallbackToPython() {
715
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-fallback-"));
716
- const stitchScript = path.join(tempDir, "stitch.py");
717
- fs.writeFileSync(stitchScript, "print('ok')", "utf8");
718
- const calls = [];
719
- const result = captureTestables.stitchWithAvailablePython(
720
- stitchScript,
721
- path.join(tempDir, "meta.json"),
722
- path.join(tempDir, "out.png"),
723
- (command) => {
724
- calls.push(command);
725
- if (command === "python3") {
726
- return {
727
- status: 1,
728
- signal: null,
729
- error: null,
730
- stderr: "python3 failed",
731
- stdout: ""
732
- };
733
- }
734
- return {
735
- status: 0,
736
- signal: null,
737
- error: null,
738
- stderr: "",
739
- stdout: "ok"
740
- };
741
- }
742
- );
743
-
744
- assert.equal(result.ok, true);
745
- assert.equal(result.command, "python");
746
- assert.deepEqual(calls, ["python3", "python"]);
747
- }
748
-
749
- function testStitchWithAvailablePythonShouldFailWhenScriptMissing() {
750
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-missing-"));
751
- const result = captureTestables.stitchWithAvailablePython(
752
- path.join(tempDir, "missing.py"),
753
- path.join(tempDir, "meta.json"),
754
- path.join(tempDir, "out.png")
755
- );
756
-
757
- assert.equal(result.ok, false);
758
- assert.equal(Array.isArray(result.attempts), true);
759
- assert.equal(result.attempts.length, 2);
760
- assert.equal(result.attempts[0].command, "python3");
761
- }
762
-
763
- function testParseArgsShouldSupportFeaturedAliasesAndInlinePort() {
764
- const parsed = parseArgs([
765
- "--criteria", "test criteria",
766
- "--baseurl", "https://example.com/v1",
767
- "--apikey", "key",
768
- "--model", "test-model",
769
- "--target-count", "3",
770
- "--pageScope", "featured",
771
- "--port=9222",
772
- "--postAction", "favorite",
773
- "--postActionConfirmed", "true"
774
- ]);
775
- assert.equal(parsed.pageScope, "featured");
776
- assert.equal(parsed.port, 9222);
777
- assert.equal(parsed.targetCount, 3);
778
- assert.equal(parsed.postAction, "favorite");
779
- assert.equal(parsed.postActionConfirmed, true);
780
- assert.equal(parsed.__provided.pageScope, true);
781
- assert.equal(parsed.__provided.port, true);
782
- }
783
-
784
- function testParseArgsShouldSupportLatestPageScope() {
785
- const parsed = parseArgs([
786
- "--criteria", "test criteria",
787
- "--baseurl", "https://example.com/v1",
788
- "--apikey", "key",
789
- "--model", "test-model",
790
- "--page-scope", "latest",
791
- "--port", "9222",
792
- "--post-action", "none",
793
- "--post-action-confirmed", "true"
794
- ]);
795
- assert.equal(parsed.pageScope, "latest");
796
- assert.equal(parsed.port, 9222);
797
- }
798
-
799
- async function main() {
800
- testShouldAbortResumeProbeEarly();
801
- await testSingleResumeCaptureFailureIsSkipped();
802
- await testConsecutiveResumeCaptureFailuresStillAbort();
803
- await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
804
- await testPageExhaustedWithoutTargetShouldStillComplete();
805
- await testFeaturedShouldUseNetworkResumeOnly();
806
- await testRecommendShouldKeepImageCaptureEvenWhenNetworkResumeExists();
807
- await testNetworkMissShouldFallbackToImageCapture();
808
- await testVisionModelFailureShouldSkipCandidateAndContinue();
809
- await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
810
- await testFeaturedFavoriteShouldNotUseDomFallback();
811
- await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
812
- await testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd();
813
- await testFeaturedFavoriteWithoutCalibrationShouldFail();
814
- testFavoriteActionParserShouldSupportBodySignals();
815
- testFavoriteActionParserShouldSupportFallbackRequestShape();
816
- testFavoriteActionParserShouldSupportWebSocketPayload();
817
- testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
818
- testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
819
- await testGetCenteredCandidateClickPointShouldSupportLatestSelector();
820
- await testFeaturedPostActionFailureShouldStillRecordPassedCandidate();
821
- await testStitchWithSharpShouldComposeExpectedImage();
822
- testStitchWithAvailablePythonShouldFallbackToPython();
823
- testStitchWithAvailablePythonShouldFailWhenScriptMissing();
824
- testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
825
- testParseArgsShouldSupportLatestPageScope();
826
- console.log("recoverable resume failure tests passed");
827
- }
828
-
829
- main().catch((error) => {
830
- console.error(error);
831
- process.exit(1);
832
- });
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, parseArgs, __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
+ pageScope: "recommend",
143
+ port: 9222,
144
+ output: path.join(tempDir, "result.csv"),
145
+ checkpointPath: path.join(tempDir, "checkpoint.json"),
146
+ pauseControlPath: path.join(tempDir, "pause.json"),
147
+ resume: false,
148
+ postAction: "none",
149
+ postActionConfirmed: true,
150
+ help: false,
151
+ __provided: {
152
+ baseUrl: true,
153
+ apiKey: true,
154
+ model: true,
155
+ criteria: true,
156
+ targetCount: true,
157
+ maxGreetCount: false,
158
+ pageScope: true,
159
+ port: true,
160
+ postAction: true,
161
+ postActionConfirmed: true
162
+ }
163
+ };
164
+ }
165
+
166
+ function testShouldAbortResumeProbeEarly() {
167
+ const probe = {
168
+ ok: false,
169
+ reason: "NO_CRESUME_IFRAME",
170
+ debug: {
171
+ activeScopeCount: 0,
172
+ totalResumeIframes: 0,
173
+ visibleResumeIframes: 0
174
+ }
175
+ };
176
+ const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
177
+ probe,
178
+ stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
179
+ elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
180
+ waitResumeMs: 60000
181
+ });
182
+ assert.equal(shouldAbort, true);
183
+ }
184
+
185
+ async function testSingleResumeCaptureFailureIsSkipped() {
186
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
187
+ const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
188
+ const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
189
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
190
+ candidates: [badCandidate, goodCandidate],
191
+ captureOutcomes: new Map([
192
+ ["bad", createResumeCaptureError()],
193
+ ["good", { stitchedImage: path.join(tempDir, "good.png") }]
194
+ ]),
195
+ screeningByKey: new Map([
196
+ ["good", { passed: true, reason: "matched", summary: "matched" }]
197
+ ])
198
+ });
199
+
200
+ const result = await cli.run();
201
+ assert.equal(result.status, "COMPLETED");
202
+ assert.equal(result.result.processed_count, 2);
203
+ assert.equal(result.result.passed_count, 1);
204
+ assert.equal(result.result.skipped_count, 1);
205
+ assert.equal(cli.consecutiveResumeCaptureFailures, 0);
206
+ }
207
+
208
+ async function testConsecutiveResumeCaptureFailuresStillAbort() {
209
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
210
+ const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
211
+ const candidates = Array.from({ length: maxFailures }, (_, index) => ({
212
+ key: `fail-${index + 1}`,
213
+ geek_id: `fail-${index + 1}`,
214
+ name: `fail-${index + 1}`
215
+ }));
216
+ const captureOutcomes = new Map(
217
+ candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
218
+ );
219
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
220
+ candidates,
221
+ captureOutcomes
222
+ });
223
+
224
+ await assert.rejects(
225
+ () => cli.run(),
226
+ (error) => {
227
+ assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
228
+ assert.match(error.message, /连续 .* 位候选人简历(?:捕获失败|获取失败(network \+ 截图))/);
229
+ assert.equal(error.rollback?.rollback_count, maxFailures);
230
+ assert.equal(error.partial_result?.processed_count, 0);
231
+ assert.equal(error.partial_result?.skipped_count, 0);
232
+ assert.deepEqual(Array.from(cli.processedKeys), []);
233
+ return true;
234
+ }
235
+ );
236
+ }
237
+
238
+ async function testPageExhaustedBeforeTargetShouldRaiseRecoverableError() {
239
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-exhausted-"));
240
+ const args = createArgs(tempDir);
241
+ args.targetCount = 5;
242
+ const cli = new FakeRecommendScreenCli(args);
243
+ cli.scrollAndLoadMore = async () => ({
244
+ before: {
245
+ candidateCount: 0,
246
+ scrollTop: 120,
247
+ scrollHeight: 900
248
+ },
249
+ after: {
250
+ candidateCount: 0,
251
+ scrollTop: 900,
252
+ scrollHeight: 900
253
+ },
254
+ bottom: {
255
+ isBottom: true,
256
+ finished_wrap_visible: true,
257
+ refresh_button_visible: true,
258
+ refresh_button_text: "刷新"
259
+ }
260
+ });
261
+
262
+ await assert.rejects(
263
+ () => cli.run(),
264
+ (error) => {
265
+ assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
266
+ assert.equal(error.retryable, true);
267
+ assert.equal(error.partial_result?.processed_count, 0);
268
+ assert.equal(error.partial_result?.output_csv, args.output);
269
+ assert.equal(error.partial_result?.checkpoint_path, args.checkpointPath);
270
+ assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
271
+ assert.equal(error.page_exhaustion?.reason, "bottom_reached");
272
+ assert.equal(error.page_exhaustion?.bottom?.finished_wrap_visible, true);
273
+ assert.equal(error.page_exhaustion?.bottom?.refresh_button_visible, true);
274
+ return true;
275
+ }
276
+ );
277
+ }
278
+
279
+ async function testPageExhaustedWithoutTargetShouldStillComplete() {
280
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-complete-"));
281
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir));
282
+ cli.scrollAndLoadMore = async () => ({
283
+ before: {
284
+ candidateCount: 0,
285
+ scrollTop: 120,
286
+ scrollHeight: 900
287
+ },
288
+ after: {
289
+ candidateCount: 0,
290
+ scrollTop: 900,
291
+ scrollHeight: 900
292
+ },
293
+ bottom: {
294
+ isBottom: true,
295
+ finished_wrap_visible: true,
296
+ refresh_button_visible: true,
297
+ refresh_button_text: "刷新"
298
+ }
299
+ });
300
+
301
+ const result = await cli.run();
302
+ assert.equal(result.status, "COMPLETED");
303
+ assert.equal(result.result.processed_count, 0);
304
+ assert.equal(result.result.output_csv, cli.args.output);
305
+ assert.equal(result.result.checkpoint_path, cli.args.checkpointPath);
306
+ assert.equal(result.result.completion_reason, "page_exhausted");
307
+ }
308
+
309
+ async function testFeaturedShouldUseNetworkResumeOnly() {
310
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
311
+ const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
312
+ const args = createArgs(tempDir);
313
+ args.pageScope = "featured";
314
+ const cli = new FakeRecommendScreenCli(args, {
315
+ candidates: [candidate]
316
+ });
317
+
318
+ cli.waitForNetworkResumeCandidateInfo = async () => ({
319
+ name: "network candidate",
320
+ school: "测试大学",
321
+ major: "计算机",
322
+ company: "OpenClaw",
323
+ position: "工程师",
324
+ resumeText: "有丰富 MCP 经验"
325
+ });
326
+ cli.callTextModel = async () => ({
327
+ passed: true,
328
+ reason: "network pass",
329
+ summary: "network summary"
330
+ });
331
+ cli.captureResumeImage = async () => {
332
+ throw new Error("capture should not be called");
333
+ };
334
+
335
+ const result = await cli.run();
336
+ assert.equal(result.status, "COMPLETED");
337
+ assert.equal(result.result.passed_count, 1);
338
+ assert.equal(result.result.resume_source, "network");
339
+ }
340
+
341
+ async function testRecommendShouldPreferNetworkResumeWhenAvailable() {
342
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-recommend-network-main-"));
343
+ const candidate = { key: "net-main-1", geek_id: "net-main-1", name: "recommend network main candidate" };
344
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
345
+ candidates: [candidate]
346
+ });
347
+ cli.waitForNetworkResumeCandidateInfo = async () => ({
348
+ resumeText: "这段 network 文本在 recommend 页面应优先用于筛选"
349
+ });
350
+ cli.callTextModel = async () => ({
351
+ passed: true,
352
+ reason: "network used",
353
+ summary: "network used"
354
+ });
355
+ cli.captureResumeImage = async () => {
356
+ throw new Error("capture should not be called when recommend network resume exists");
357
+ };
358
+
359
+ const result = await cli.run();
360
+ assert.equal(result.status, "COMPLETED");
361
+ assert.equal(result.result.passed_count, 1);
362
+ assert.equal(result.result.resume_source, "network");
363
+ }
364
+
365
+ async function testNetworkMissShouldFallbackToImageCapture() {
366
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-fallback-"));
367
+ const candidate = { key: "img-1", geek_id: "img-1", name: "image candidate" };
368
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
369
+ candidates: [candidate],
370
+ captureOutcomes: new Map([
371
+ ["img-1", { stitchedImage: path.join(tempDir, "img-1.png") }]
372
+ ]),
373
+ screeningByKey: new Map([
374
+ ["img-1", { passed: false, reason: "image path used", summary: "image path used" }]
375
+ ])
376
+ });
377
+ cli.waitForNetworkResumeCandidateInfo = async () => null;
378
+
379
+ const result = await cli.run();
380
+ assert.equal(result.status, "COMPLETED");
381
+ assert.equal(result.result.resume_source, "image_fallback");
382
+ }
383
+
384
+ async function testLatestShouldPreferNetworkResumeWhenAvailable() {
385
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-main-"));
386
+ const args = createArgs(tempDir);
387
+ args.pageScope = "latest";
388
+ const candidate = { key: "latest-net-1", geek_id: "latest-net-1", name: "latest network candidate" };
389
+ const cli = new FakeRecommendScreenCli(args, {
390
+ candidates: [candidate]
391
+ });
392
+ cli.waitForNetworkResumeCandidateInfo = async () => ({
393
+ resumeText: "最新页 network 简历可用"
394
+ });
395
+ cli.callTextModel = async () => ({
396
+ passed: true,
397
+ reason: "network used",
398
+ summary: "network used"
399
+ });
400
+ cli.captureResumeImage = async () => {
401
+ throw new Error("capture should not be called when latest network resume exists");
402
+ };
403
+
404
+ const result = await cli.run();
405
+ assert.equal(result.status, "COMPLETED");
406
+ assert.equal(result.result.passed_count, 1);
407
+ assert.equal(result.result.resume_source, "network");
408
+ }
409
+
410
+ async function testLatestNetworkMissShouldFallbackToImageCapture() {
411
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-fallback-"));
412
+ const args = createArgs(tempDir);
413
+ args.pageScope = "latest";
414
+ const candidate = { key: "latest-img-1", geek_id: "latest-img-1", name: "latest image candidate" };
415
+ const cli = new FakeRecommendScreenCli(args, {
416
+ candidates: [candidate],
417
+ captureOutcomes: new Map([
418
+ ["latest-img-1", { stitchedImage: path.join(tempDir, "latest-img-1.png") }]
419
+ ]),
420
+ screeningByKey: new Map([
421
+ ["latest-img-1", { passed: false, reason: "image fallback used", summary: "image fallback used" }]
422
+ ])
423
+ });
424
+ cli.waitForNetworkResumeCandidateInfo = async () => null;
425
+
426
+ const result = await cli.run();
427
+ assert.equal(result.status, "COMPLETED");
428
+ assert.equal(result.result.resume_source, "image_fallback");
429
+ }
430
+
431
+ async function testVisionModelFailureShouldSkipCandidateAndContinue() {
432
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-vision-failure-skip-"));
433
+ const first = { key: "vision-fail-1", geek_id: "vision-fail-1", name: "vision-fail-1" };
434
+ const second = { key: "vision-pass-2", geek_id: "vision-pass-2", name: "vision-pass-2" };
435
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
436
+ candidates: [first, second],
437
+ captureOutcomes: new Map([
438
+ ["vision-fail-1", { stitchedImage: path.join(tempDir, "vision-fail-1.png") }],
439
+ ["vision-pass-2", { stitchedImage: path.join(tempDir, "vision-pass-2.png") }]
440
+ ]),
441
+ screeningByKey: new Map([
442
+ ["vision-pass-2", { passed: true, reason: "ok", summary: "ok" }]
443
+ ])
444
+ });
445
+
446
+ cli.callVisionModel = async () => {
447
+ if (cli.lastCapturedCandidateKey === "vision-fail-1") {
448
+ const error = new Error("model backend timeout");
449
+ error.code = "VISION_MODEL_FAILED";
450
+ throw error;
451
+ }
452
+ return {
453
+ passed: true,
454
+ reason: "ok",
455
+ summary: "ok"
456
+ };
457
+ };
458
+
459
+ const result = await cli.run();
460
+ assert.equal(result.status, "COMPLETED");
461
+ assert.equal(result.result.processed_count, 2);
462
+ assert.equal(result.result.passed_count, 1);
463
+ assert.equal(result.result.skipped_count, 1);
464
+ }
465
+
466
+ async function testFeaturedNetworkMissShouldSkipWithoutImageCapture() {
467
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-network-only-"));
468
+ const args = createArgs(tempDir);
469
+ args.pageScope = "featured";
470
+ const candidate = { key: "featured-no-network", geek_id: "featured-no-network", name: "featured no network" };
471
+ const cli = new FakeRecommendScreenCli(args, {
472
+ candidates: [candidate]
473
+ });
474
+ cli.waitForNetworkResumeCandidateInfo = async () => null;
475
+ cli.captureResumeImage = async () => {
476
+ throw new Error("capture should not be called for featured scope");
477
+ };
478
+
479
+ const result = await cli.run();
480
+ assert.equal(result.status, "COMPLETED");
481
+ assert.equal(result.result.processed_count, 1);
482
+ assert.equal(result.result.passed_count, 0);
483
+ assert.equal(result.result.skipped_count, 1);
484
+ assert.equal(result.result.resume_source, "network");
485
+ }
486
+
487
+ async function testFeaturedFavoriteShouldNotUseDomFallback() {
488
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-"));
489
+ const args = createArgs(tempDir);
490
+ args.pageScope = "featured";
491
+ const calibrationPath = path.join(tempDir, "favorite-calibration.json");
492
+ fs.writeFileSync(calibrationPath, JSON.stringify({
493
+ favoritePosition: {
494
+ pageX: 120,
495
+ pageY: 220,
496
+ canvasX: 0,
497
+ canvasY: 0
498
+ }
499
+ }, null, 2));
500
+ args.calibrationPath = calibrationPath;
501
+ const cli = new RecommendScreenCli(args);
502
+ let evaluateCalls = 0;
503
+ let clickCalls = 0;
504
+ cli.evaluate = async () => {
505
+ evaluateCalls += 1;
506
+ return { ok: true };
507
+ };
508
+ cli.simulateHumanClick = async () => {
509
+ clickCalls += 1;
510
+ cli.favoriteActionEvents.push({ action: "add", ts: Date.now(), source: "test", url: "userMark/add" });
511
+ };
512
+ const result = await cli.favoriteCandidate();
513
+ assert.equal(result.actionTaken, "favorite");
514
+ assert.equal(clickCalls, 1);
515
+ assert.equal(evaluateCalls, 0);
516
+ }
517
+
518
+ async function testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested() {
519
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-already-"));
520
+ const args = createArgs(tempDir);
521
+ args.pageScope = "featured";
522
+ const calibrationPath = path.join(tempDir, "favorite-calibration.json");
523
+ fs.writeFileSync(calibrationPath, JSON.stringify({
524
+ favoritePosition: {
525
+ pageX: 120,
526
+ pageY: 220,
527
+ canvasX: 0,
528
+ canvasY: 0
529
+ }
530
+ }, null, 2));
531
+ args.calibrationPath = calibrationPath;
532
+ const cli = new RecommendScreenCli(args);
533
+ let clickCalls = 0;
534
+ cli.simulateHumanClick = async () => {
535
+ clickCalls += 1;
536
+ };
537
+ const result = await cli.favoriteCandidate({ alreadyInterested: true });
538
+ assert.equal(result.actionTaken, "already_favorited");
539
+ assert.equal(result.source, "network_profile");
540
+ assert.equal(clickCalls, 0);
541
+ }
542
+
543
+ async function testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd() {
544
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-del-add-"));
545
+ const args = createArgs(tempDir);
546
+ args.pageScope = "featured";
547
+ const calibrationPath = path.join(tempDir, "favorite-calibration.json");
548
+ fs.writeFileSync(calibrationPath, JSON.stringify({
549
+ favoritePosition: {
550
+ pageX: 120,
551
+ pageY: 220,
552
+ canvasX: 0,
553
+ canvasY: 0
554
+ }
555
+ }, null, 2));
556
+ args.calibrationPath = calibrationPath;
557
+ const cli = new RecommendScreenCli(args);
558
+ let clickCalls = 0;
559
+ cli.simulateHumanClick = async () => {
560
+ clickCalls += 1;
561
+ cli.favoriteActionEvents.push({
562
+ action: clickCalls === 1 ? "del" : "add",
563
+ ts: Date.now(),
564
+ source: "test",
565
+ url: clickCalls === 1 ? "userMark/del" : "userMark/add"
566
+ });
567
+ };
568
+ const result = await cli.favoriteCandidate();
569
+ assert.equal(result.actionTaken, "already_favorited");
570
+ assert.equal(result.re_favorited, true);
571
+ assert.equal(clickCalls, 2);
572
+ }
573
+
574
+ async function testFeaturedFavoriteWithoutCalibrationShouldFail() {
575
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-missing-cal-"));
576
+ const args = createArgs(tempDir);
577
+ args.pageScope = "featured";
578
+ args.calibrationPath = path.join(tempDir, "missing-calibration.json");
579
+ const cli = new RecommendScreenCli(args);
580
+ await assert.rejects(
581
+ () => cli.favoriteCandidate(),
582
+ (error) => {
583
+ assert.equal(error.code, "FAVORITE_CALIBRATION_REQUIRED");
584
+ return true;
585
+ }
586
+ );
587
+ }
588
+
589
+ function testFavoriteActionParserShouldSupportBodySignals() {
590
+ const addFromJson = __testables.parseFavoriteActionFromPostData(JSON.stringify({
591
+ action: "star-interest-click",
592
+ p3: 1
593
+ }));
594
+ const delFromForm = __testables.parseFavoriteActionFromPostData("action=star-interest-click&p3=0");
595
+ assert.equal(addFromJson, "add");
596
+ assert.equal(delFromForm, "del");
597
+ }
598
+
599
+ function testFavoriteActionParserShouldSupportFallbackRequestShape() {
600
+ const action = __testables.parseFavoriteActionFromRequest(
601
+ "https://www.zhipin.com/wapi/zpgeek/favorite/operate",
602
+ JSON.stringify({ op: "add", geekId: "abc" })
603
+ );
604
+ assert.equal(action, "add");
605
+ }
606
+
607
+ function testFavoriteActionParserShouldSupportWebSocketPayload() {
608
+ const addFromWsJson = __testables.parseFavoriteActionFromWsPayload(JSON.stringify({
609
+ action: "star-interest-click",
610
+ p3: 1
611
+ }));
612
+ const delFromWsForm = __testables.parseFavoriteActionFromWsPayload("action=star-interest-click&p3=0");
613
+ assert.equal(addFromWsJson, "add");
614
+ assert.equal(delFromWsForm, "del");
615
+ }
616
+
617
+ function testFavoriteActionParserShouldOnlyTrustKnownRequestShapes() {
618
+ const unknown = __testables.parseFavoriteActionFromKnownRequest(
619
+ "https://www.zhipin.com/wapi/other/metrics",
620
+ JSON.stringify({ action: "add", p3: 1 })
621
+ );
622
+ const actionLog = __testables.parseFavoriteActionFromKnownRequest(
623
+ "https://www.zhipin.com/wapi/zplog/actionLog/common.json",
624
+ JSON.stringify({ action: "star-interest-click", p3: 1 })
625
+ );
626
+ const userMark = __testables.parseFavoriteActionFromKnownRequest(
627
+ "https://www.zhipin.com/wapi/zpgeek/userMark/add",
628
+ ""
629
+ );
630
+ assert.equal(unknown, null);
631
+ assert.equal(actionLog, "add");
632
+ assert.equal(userMark, "add");
633
+ }
634
+
635
+ function testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom() {
636
+ const loadMore = __testables.classifyFinishedWrapState("滚动加载更多", false);
637
+ const loading = __testables.classifyFinishedWrapState("正在加载数据...", false);
638
+ const noMore = __testables.classifyFinishedWrapState("没有更多人选", false);
639
+ const refreshOnly = __testables.classifyFinishedWrapState("", true);
640
+
641
+ assert.equal(loadMore.isBottom, false);
642
+ assert.equal(loadMore.matched_load_more_keyword, "滚动加载更多");
643
+ assert.equal(loading.isBottom, false);
644
+ assert.equal(loading.matched_load_more_keyword, "正在加载");
645
+ assert.equal(noMore.isBottom, true);
646
+ assert.equal(noMore.matched_bottom_keyword, "没有更多");
647
+ assert.equal(refreshOnly.isBottom, true);
648
+ assert.equal(refreshOnly.reason, "refresh_button_visible");
649
+ }
650
+
651
+ async function testGetCenteredCandidateClickPointShouldSupportLatestSelector() {
652
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-latest-click-locator-"));
653
+ const args = createArgs(tempDir);
654
+ args.pageScope = "latest";
655
+ const cli = new RecommendScreenCli(args);
656
+
657
+ let expressionCaptured = "";
658
+ cli.evaluate = async (expression) => {
659
+ expressionCaptured = String(expression || "");
660
+ return {
661
+ ok: true,
662
+ x: 100,
663
+ y: 100,
664
+ width: 120,
665
+ height: 64
666
+ };
667
+ };
668
+
669
+ const result = await cli.getCenteredCandidateClickPoint({
670
+ key: "latest-test-key",
671
+ geek_id: "latest-test-key"
672
+ });
673
+
674
+ assert.equal(result.ok, true);
675
+ assert.equal(expressionCaptured.includes(".candidate-card-wrap .card-inner[data-geek]"), true);
676
+ assert.equal(expressionCaptured.includes("getAttribute('data-geek')"), true);
677
+ }
678
+
679
+ async function testFeaturedPostActionFailureShouldStillRecordPassedCandidate() {
680
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-featured-action-failure-"));
681
+ const args = createArgs(tempDir);
682
+ args.pageScope = "featured";
683
+ args.postAction = "favorite";
684
+ const candidate = { key: "featured-fav-fail", geek_id: "featured-fav-fail", name: "featured candidate" };
685
+ const cli = new FakeRecommendScreenCli(args, {
686
+ candidates: [candidate]
687
+ });
688
+
689
+ cli.waitForNetworkResumeCandidateInfo = async () => ({
690
+ name: "featured candidate",
691
+ school: "测试大学",
692
+ major: "人工智能",
693
+ company: "测试公司",
694
+ position: "算法工程师",
695
+ resumeText: "满足测试标准"
696
+ });
697
+ cli.callTextModel = async () => ({
698
+ passed: true,
699
+ reason: "通过",
700
+ summary: "通过"
701
+ });
702
+ cli.favoriteCandidate = async () => {
703
+ const error = new Error("精选页收藏未检测到 network add 成功信号。");
704
+ error.code = "FAVORITE_BUTTON_FAILED";
705
+ throw error;
706
+ };
707
+
708
+ const result = await cli.run();
709
+ assert.equal(result.status, "COMPLETED");
710
+ assert.equal(result.result.processed_count, 1);
711
+ assert.equal(result.result.passed_count, 1);
712
+ assert.equal(result.result.skipped_count, 0);
713
+ assert.equal(cli.passedCandidates.length, 1);
714
+ assert.equal(cli.passedCandidates[0].action, "favorite_failed");
715
+ assert.match(cli.passedCandidates[0].reason, /\[favorite失败]/);
716
+ }
717
+
718
+ async function testStitchWithSharpShouldComposeExpectedImage() {
719
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-sharp-stitch-"));
720
+ const chunkA = path.join(tempDir, "chunk_000.png");
721
+ const chunkB = path.join(tempDir, "chunk_001.png");
722
+ const chunkC = path.join(tempDir, "chunk_002.png");
723
+ const metadataPath = path.join(tempDir, "chunks.json");
724
+ const outputPath = path.join(tempDir, "stitched.png");
725
+
726
+ await sharp({
727
+ create: { width: 20, height: 100, channels: 3, background: { r: 255, g: 0, b: 0 } }
728
+ }).png().toFile(chunkA);
729
+ await sharp({
730
+ create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 255, b: 0 } }
731
+ }).png().toFile(chunkB);
732
+ await sharp({
733
+ create: { width: 20, height: 100, channels: 3, background: { r: 0, g: 0, b: 255 } }
734
+ }).png().toFile(chunkC);
735
+
736
+ fs.writeFileSync(
737
+ metadataPath,
738
+ JSON.stringify({
739
+ chunks: [
740
+ { index: 0, file: chunkA, scrollTop: 0, clipHeightCss: 100 },
741
+ { index: 1, file: chunkB, scrollTop: 80, clipHeightCss: 100 },
742
+ { index: 2, file: chunkC, scrollTop: 160, clipHeightCss: 100 }
743
+ ]
744
+ }),
745
+ "utf8"
746
+ );
747
+
748
+ const stitched = await captureTestables.stitchWithSharp(metadataPath, outputPath);
749
+ const outputMeta = await sharp(outputPath).metadata();
750
+
751
+ assert.equal(stitched.ok, true);
752
+ assert.equal(stitched.engine, "sharp");
753
+ assert.equal(stitched.segments, 3);
754
+ assert.equal(outputMeta.width, 20);
755
+ assert.equal(outputMeta.height, 260);
756
+ assert.equal(Array.isArray(stitched.used), true);
757
+ assert.equal(stitched.used.length, 3);
758
+ }
759
+
760
+ function testStitchWithAvailablePythonShouldFallbackToPython() {
761
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-fallback-"));
762
+ const stitchScript = path.join(tempDir, "stitch.py");
763
+ fs.writeFileSync(stitchScript, "print('ok')", "utf8");
764
+ const calls = [];
765
+ const result = captureTestables.stitchWithAvailablePython(
766
+ stitchScript,
767
+ path.join(tempDir, "meta.json"),
768
+ path.join(tempDir, "out.png"),
769
+ (command) => {
770
+ calls.push(command);
771
+ if (command === "python3") {
772
+ return {
773
+ status: 1,
774
+ signal: null,
775
+ error: null,
776
+ stderr: "python3 failed",
777
+ stdout: ""
778
+ };
779
+ }
780
+ return {
781
+ status: 0,
782
+ signal: null,
783
+ error: null,
784
+ stderr: "",
785
+ stdout: "ok"
786
+ };
787
+ }
788
+ );
789
+
790
+ assert.equal(result.ok, true);
791
+ assert.equal(result.command, "python");
792
+ assert.deepEqual(calls, ["python3", "python"]);
793
+ }
794
+
795
+ function testStitchWithAvailablePythonShouldFailWhenScriptMissing() {
796
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-python-missing-"));
797
+ const result = captureTestables.stitchWithAvailablePython(
798
+ path.join(tempDir, "missing.py"),
799
+ path.join(tempDir, "meta.json"),
800
+ path.join(tempDir, "out.png")
801
+ );
802
+
803
+ assert.equal(result.ok, false);
804
+ assert.equal(Array.isArray(result.attempts), true);
805
+ assert.equal(result.attempts.length, 2);
806
+ assert.equal(result.attempts[0].command, "python3");
807
+ }
808
+
809
+ function testParseArgsShouldSupportFeaturedAliasesAndInlinePort() {
810
+ const parsed = parseArgs([
811
+ "--criteria", "test criteria",
812
+ "--baseurl", "https://example.com/v1",
813
+ "--apikey", "key",
814
+ "--model", "test-model",
815
+ "--target-count", "3",
816
+ "--pageScope", "featured",
817
+ "--port=9222",
818
+ "--postAction", "favorite",
819
+ "--postActionConfirmed", "true"
820
+ ]);
821
+ assert.equal(parsed.pageScope, "featured");
822
+ assert.equal(parsed.port, 9222);
823
+ assert.equal(parsed.targetCount, 3);
824
+ assert.equal(parsed.postAction, "favorite");
825
+ assert.equal(parsed.postActionConfirmed, true);
826
+ assert.equal(parsed.__provided.pageScope, true);
827
+ assert.equal(parsed.__provided.port, true);
828
+ }
829
+
830
+ function testParseArgsShouldSupportLatestPageScope() {
831
+ const parsed = parseArgs([
832
+ "--criteria", "test criteria",
833
+ "--baseurl", "https://example.com/v1",
834
+ "--apikey", "key",
835
+ "--model", "test-model",
836
+ "--page-scope", "latest",
837
+ "--port", "9222",
838
+ "--post-action", "none",
839
+ "--post-action-confirmed", "true"
840
+ ]);
841
+ assert.equal(parsed.pageScope, "latest");
842
+ assert.equal(parsed.port, 9222);
843
+ }
844
+
845
+ async function main() {
846
+ testShouldAbortResumeProbeEarly();
847
+ await testSingleResumeCaptureFailureIsSkipped();
848
+ await testConsecutiveResumeCaptureFailuresStillAbort();
849
+ await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
850
+ await testPageExhaustedWithoutTargetShouldStillComplete();
851
+ await testFeaturedShouldUseNetworkResumeOnly();
852
+ await testRecommendShouldPreferNetworkResumeWhenAvailable();
853
+ await testNetworkMissShouldFallbackToImageCapture();
854
+ await testLatestShouldPreferNetworkResumeWhenAvailable();
855
+ await testLatestNetworkMissShouldFallbackToImageCapture();
856
+ await testVisionModelFailureShouldSkipCandidateAndContinue();
857
+ await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
858
+ await testFeaturedFavoriteShouldNotUseDomFallback();
859
+ await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
860
+ await testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd();
861
+ await testFeaturedFavoriteWithoutCalibrationShouldFail();
862
+ testFavoriteActionParserShouldSupportBodySignals();
863
+ testFavoriteActionParserShouldSupportFallbackRequestShape();
864
+ testFavoriteActionParserShouldSupportWebSocketPayload();
865
+ testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
866
+ testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
867
+ await testGetCenteredCandidateClickPointShouldSupportLatestSelector();
868
+ await testFeaturedPostActionFailureShouldStillRecordPassedCandidate();
869
+ await testStitchWithSharpShouldComposeExpectedImage();
870
+ testStitchWithAvailablePythonShouldFallbackToPython();
871
+ testStitchWithAvailablePythonShouldFailWhenScriptMissing();
872
+ testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
873
+ testParseArgsShouldSupportLatestPageScope();
874
+ console.log("recoverable resume failure tests passed");
875
+ }
876
+
877
+ main().catch((error) => {
878
+ console.error(error);
879
+ process.exit(1);
880
+ });