@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.
- package/package.json +13 -2
- package/skills/boss-recommend-pipeline/SKILL.md +1 -4
- package/src/adapters.js +307 -203
- package/src/pipeline.js +450 -375
- package/src/test-adapters-runtime.js +10 -1
- package/src/test-pipeline.js +506 -4
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +113 -34
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +287 -89
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +413 -245
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { runRecommendScreenCli, __testables as adapterTestables } from "./adapters.js";
|
|
5
|
+
import { runPipelinePreflight, runRecommendScreenCli, __testables as adapterTestables } from "./adapters.js";
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
runProcess,
|
|
@@ -149,6 +149,14 @@ async function testResumeRequiresCheckpointFile() {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function testPreflightShouldCheckSharpInsteadOfPython() {
|
|
153
|
+
const preflight = runPipelinePreflight(process.cwd());
|
|
154
|
+
const keys = new Set((preflight.checks || []).map((item) => item?.key));
|
|
155
|
+
assert.equal(keys.has("npm_dep_sharp"), true);
|
|
156
|
+
assert.equal(keys.has("python_cli"), false);
|
|
157
|
+
assert.equal(keys.has("python_pillow"), false);
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
async function main() {
|
|
153
161
|
await testRunProcessHeartbeatAndOutput();
|
|
154
162
|
await testRunProcessAbortSignal();
|
|
@@ -157,6 +165,7 @@ async function main() {
|
|
|
157
165
|
testResolveScreenTimeoutDefaultsTo24Hours();
|
|
158
166
|
testBuildRecommendScreenProcessErrorMapsTimeout();
|
|
159
167
|
await testResumeRequiresCheckpointFile();
|
|
168
|
+
testPreflightShouldCheckSharpInsteadOfPython();
|
|
160
169
|
console.log("adapters runtime tests passed");
|
|
161
170
|
}
|
|
162
171
|
|
package/src/test-pipeline.js
CHANGED
|
@@ -354,6 +354,505 @@ async function testConsecutiveResumeCaptureFailuresShouldRefreshAndRerunSearchWi
|
|
|
354
354
|
assert.equal(result.search_params.recent_not_view, "近14天没有");
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
async function testPageExhaustedBeforeTargetShouldRefreshInPageAndResumeScreen() {
|
|
358
|
+
const searchCalls = [];
|
|
359
|
+
const screenCalls = [];
|
|
360
|
+
let refreshCalls = 0;
|
|
361
|
+
let reloadCalls = 0;
|
|
362
|
+
let pageReadyCalls = 0;
|
|
363
|
+
const parsed = createParsed();
|
|
364
|
+
parsed.searchParams = {
|
|
365
|
+
...parsed.searchParams,
|
|
366
|
+
recent_not_view: "不限"
|
|
367
|
+
};
|
|
368
|
+
const result = await runRecommendPipeline(
|
|
369
|
+
{
|
|
370
|
+
workspaceRoot: process.cwd(),
|
|
371
|
+
instruction: "test",
|
|
372
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
373
|
+
overrides: {},
|
|
374
|
+
resume: {
|
|
375
|
+
resume: false,
|
|
376
|
+
output_csv: "C:/temp/resume.csv",
|
|
377
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
378
|
+
pause_control_path: "C:/temp/run.json",
|
|
379
|
+
previous_completion_reason: ""
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
parseRecommendInstruction: () => parsed,
|
|
384
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
385
|
+
ensureBossRecommendPageReady: async () => {
|
|
386
|
+
pageReadyCalls += 1;
|
|
387
|
+
return { ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } };
|
|
388
|
+
},
|
|
389
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
390
|
+
refreshBossRecommendList: async () => {
|
|
391
|
+
refreshCalls += 1;
|
|
392
|
+
return {
|
|
393
|
+
ok: true,
|
|
394
|
+
action: "in_page_refresh",
|
|
395
|
+
state: "RECOMMEND_READY",
|
|
396
|
+
before_state: {
|
|
397
|
+
finished_wrap_visible: true,
|
|
398
|
+
refresh_button_visible: true
|
|
399
|
+
},
|
|
400
|
+
after_state: {
|
|
401
|
+
finished_wrap_visible: false,
|
|
402
|
+
list_ready: true
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
reloadBossRecommendPage: async () => {
|
|
407
|
+
reloadCalls += 1;
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
state: "RECOMMEND_READY",
|
|
411
|
+
reloaded_url: "https://www.zhipin.com/web/chat/recommend"
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
runRecommendSearchCli: async ({ searchParams }) => {
|
|
415
|
+
searchCalls.push({ ...searchParams });
|
|
416
|
+
return {
|
|
417
|
+
ok: true,
|
|
418
|
+
summary: {
|
|
419
|
+
candidate_count: 9,
|
|
420
|
+
applied_filters: searchParams,
|
|
421
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
runRecommendScreenCli: async ({ resume }) => {
|
|
426
|
+
screenCalls.push({ ...resume });
|
|
427
|
+
if (screenCalls.length === 1) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
error: {
|
|
431
|
+
code: "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED",
|
|
432
|
+
message: "推荐列表已到底,但尚未达到目标数。",
|
|
433
|
+
page_exhaustion: {
|
|
434
|
+
reason: "bottom_reached",
|
|
435
|
+
bottom: {
|
|
436
|
+
isBottom: true,
|
|
437
|
+
finished_wrap_visible: true,
|
|
438
|
+
refresh_button_visible: true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
summary: {
|
|
443
|
+
processed_count: 4,
|
|
444
|
+
passed_count: 1,
|
|
445
|
+
skipped_count: 3,
|
|
446
|
+
output_csv: "C:/temp/resume.csv",
|
|
447
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
448
|
+
completion_reason: "page_exhausted_before_target_count"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
ok: true,
|
|
454
|
+
summary: {
|
|
455
|
+
processed_count: 10,
|
|
456
|
+
passed_count: 3,
|
|
457
|
+
skipped_count: 7,
|
|
458
|
+
output_csv: "C:/temp/resume.csv",
|
|
459
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
460
|
+
completion_reason: "target_count_reached"
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
assert.equal(result.status, "COMPLETED");
|
|
468
|
+
assert.equal(searchCalls.length, 1);
|
|
469
|
+
assert.equal(searchCalls[0].recent_not_view, "不限");
|
|
470
|
+
assert.equal(screenCalls.length, 2);
|
|
471
|
+
assert.equal(screenCalls[0].resume, false);
|
|
472
|
+
assert.equal(screenCalls[1].resume, true);
|
|
473
|
+
assert.equal(screenCalls[1].require_checkpoint, true);
|
|
474
|
+
assert.equal(screenCalls[1].output_csv, "C:/temp/resume.csv");
|
|
475
|
+
assert.equal(refreshCalls, 1);
|
|
476
|
+
assert.equal(reloadCalls, 0);
|
|
477
|
+
assert.equal(pageReadyCalls, 1);
|
|
478
|
+
assert.equal(result.result.candidate_count, null);
|
|
479
|
+
assert.equal(result.result.completion_reason, "target_count_reached");
|
|
480
|
+
assert.equal(result.result.auto_recovery.action, "in_page_refresh");
|
|
481
|
+
assert.equal(result.result.auto_recovery.refresh.ok, true);
|
|
482
|
+
assert.equal(result.result.auto_recovery.page_exhaustion.reason, "bottom_reached");
|
|
483
|
+
assert.equal(result.search_params.recent_not_view, "不限");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function testPageExhaustedBeforeTargetShouldReloadWhenRefreshButtonMissing() {
|
|
487
|
+
const searchCalls = [];
|
|
488
|
+
const screenCalls = [];
|
|
489
|
+
let refreshCalls = 0;
|
|
490
|
+
let reloadCalls = 0;
|
|
491
|
+
let pageReadyCalls = 0;
|
|
492
|
+
const parsed = createParsed();
|
|
493
|
+
parsed.searchParams = {
|
|
494
|
+
...parsed.searchParams,
|
|
495
|
+
recent_not_view: "不限"
|
|
496
|
+
};
|
|
497
|
+
const result = await runRecommendPipeline(
|
|
498
|
+
{
|
|
499
|
+
workspaceRoot: process.cwd(),
|
|
500
|
+
instruction: "test",
|
|
501
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
502
|
+
overrides: {},
|
|
503
|
+
resume: {
|
|
504
|
+
resume: false,
|
|
505
|
+
output_csv: "C:/temp/resume.csv",
|
|
506
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
507
|
+
pause_control_path: "C:/temp/run.json",
|
|
508
|
+
previous_completion_reason: ""
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
parseRecommendInstruction: () => parsed,
|
|
513
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
514
|
+
ensureBossRecommendPageReady: async () => {
|
|
515
|
+
pageReadyCalls += 1;
|
|
516
|
+
return { ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } };
|
|
517
|
+
},
|
|
518
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
519
|
+
refreshBossRecommendList: async () => {
|
|
520
|
+
refreshCalls += 1;
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
action: "in_page_refresh",
|
|
524
|
+
state: "REFRESH_BUTTON_NOT_FOUND",
|
|
525
|
+
message: "未找到页内刷新按钮。"
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
reloadBossRecommendPage: async () => {
|
|
529
|
+
reloadCalls += 1;
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
state: "RECOMMEND_READY",
|
|
533
|
+
reloaded_url: "https://www.zhipin.com/web/chat/recommend"
|
|
534
|
+
};
|
|
535
|
+
},
|
|
536
|
+
runRecommendSearchCli: async ({ searchParams }) => {
|
|
537
|
+
searchCalls.push({ ...searchParams });
|
|
538
|
+
return {
|
|
539
|
+
ok: true,
|
|
540
|
+
summary: {
|
|
541
|
+
candidate_count: searchCalls.length === 1 ? 9 : 12,
|
|
542
|
+
applied_filters: searchParams,
|
|
543
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
runRecommendScreenCli: async ({ resume }) => {
|
|
548
|
+
screenCalls.push({ ...resume });
|
|
549
|
+
if (screenCalls.length === 1) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: {
|
|
553
|
+
code: "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED",
|
|
554
|
+
message: "推荐列表已到底,但尚未达到目标数。",
|
|
555
|
+
page_exhaustion: {
|
|
556
|
+
reason: "bottom_reached",
|
|
557
|
+
bottom: {
|
|
558
|
+
isBottom: true,
|
|
559
|
+
finished_wrap_visible: true,
|
|
560
|
+
refresh_button_visible: false
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
summary: {
|
|
565
|
+
processed_count: 4,
|
|
566
|
+
passed_count: 1,
|
|
567
|
+
skipped_count: 3,
|
|
568
|
+
output_csv: "C:/temp/resume.csv",
|
|
569
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
570
|
+
completion_reason: "page_exhausted_before_target_count"
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
ok: true,
|
|
576
|
+
summary: {
|
|
577
|
+
processed_count: 10,
|
|
578
|
+
passed_count: 3,
|
|
579
|
+
skipped_count: 7,
|
|
580
|
+
output_csv: "C:/temp/resume.csv",
|
|
581
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
582
|
+
completion_reason: "target_count_reached"
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
assert.equal(result.status, "COMPLETED");
|
|
590
|
+
assert.equal(searchCalls.length, 2);
|
|
591
|
+
assert.equal(searchCalls[0].recent_not_view, "不限");
|
|
592
|
+
assert.equal(searchCalls[1].recent_not_view, "近14天没有");
|
|
593
|
+
assert.equal(screenCalls.length, 2);
|
|
594
|
+
assert.equal(screenCalls[0].resume, false);
|
|
595
|
+
assert.equal(screenCalls[1].resume, true);
|
|
596
|
+
assert.equal(screenCalls[1].require_checkpoint, true);
|
|
597
|
+
assert.equal(refreshCalls, 1);
|
|
598
|
+
assert.equal(reloadCalls, 1);
|
|
599
|
+
assert.equal(pageReadyCalls, 2);
|
|
600
|
+
assert.equal(result.result.candidate_count, 12);
|
|
601
|
+
assert.equal(result.result.auto_recovery.action, "reload_page_and_rerun_search");
|
|
602
|
+
assert.equal(result.result.auto_recovery.refresh.state, "REFRESH_BUTTON_NOT_FOUND");
|
|
603
|
+
assert.equal(result.result.auto_recovery.reload.ok, true);
|
|
604
|
+
assert.equal(result.search_params.recent_not_view, "近14天没有");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function testPageExhaustedBeforeTargetShouldReloadWhenRefreshDoesNotRecoverList() {
|
|
608
|
+
const searchCalls = [];
|
|
609
|
+
const screenCalls = [];
|
|
610
|
+
let refreshCalls = 0;
|
|
611
|
+
let reloadCalls = 0;
|
|
612
|
+
const parsed = createParsed();
|
|
613
|
+
parsed.searchParams = {
|
|
614
|
+
...parsed.searchParams,
|
|
615
|
+
recent_not_view: "不限"
|
|
616
|
+
};
|
|
617
|
+
const result = await runRecommendPipeline(
|
|
618
|
+
{
|
|
619
|
+
workspaceRoot: process.cwd(),
|
|
620
|
+
instruction: "test",
|
|
621
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
622
|
+
overrides: {},
|
|
623
|
+
resume: {
|
|
624
|
+
resume: false,
|
|
625
|
+
output_csv: "C:/temp/resume.csv",
|
|
626
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
627
|
+
pause_control_path: "C:/temp/run.json",
|
|
628
|
+
previous_completion_reason: ""
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
parseRecommendInstruction: () => parsed,
|
|
633
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
634
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } }),
|
|
635
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
636
|
+
refreshBossRecommendList: async () => {
|
|
637
|
+
refreshCalls += 1;
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
action: "in_page_refresh",
|
|
641
|
+
state: "LIST_NOT_RELOADED",
|
|
642
|
+
message: "点击刷新后列表没有重新就绪。"
|
|
643
|
+
};
|
|
644
|
+
},
|
|
645
|
+
reloadBossRecommendPage: async () => {
|
|
646
|
+
reloadCalls += 1;
|
|
647
|
+
return {
|
|
648
|
+
ok: true,
|
|
649
|
+
state: "RECOMMEND_READY",
|
|
650
|
+
reloaded_url: "https://www.zhipin.com/web/chat/recommend"
|
|
651
|
+
};
|
|
652
|
+
},
|
|
653
|
+
runRecommendSearchCli: async ({ searchParams }) => {
|
|
654
|
+
searchCalls.push({ ...searchParams });
|
|
655
|
+
return {
|
|
656
|
+
ok: true,
|
|
657
|
+
summary: {
|
|
658
|
+
candidate_count: searchCalls.length === 1 ? 9 : 11,
|
|
659
|
+
applied_filters: searchParams,
|
|
660
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
runRecommendScreenCli: async ({ resume }) => {
|
|
665
|
+
screenCalls.push({ ...resume });
|
|
666
|
+
if (screenCalls.length === 1) {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
error: {
|
|
670
|
+
code: "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED",
|
|
671
|
+
message: "推荐列表已到底,但尚未达到目标数。"
|
|
672
|
+
},
|
|
673
|
+
summary: {
|
|
674
|
+
processed_count: 4,
|
|
675
|
+
passed_count: 1,
|
|
676
|
+
skipped_count: 3,
|
|
677
|
+
output_csv: "C:/temp/resume.csv",
|
|
678
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
679
|
+
completion_reason: "page_exhausted_before_target_count"
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
ok: true,
|
|
685
|
+
summary: {
|
|
686
|
+
processed_count: 10,
|
|
687
|
+
passed_count: 3,
|
|
688
|
+
skipped_count: 7,
|
|
689
|
+
output_csv: "C:/temp/resume.csv",
|
|
690
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
691
|
+
completion_reason: "target_count_reached"
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
assert.equal(result.status, "COMPLETED");
|
|
699
|
+
assert.equal(searchCalls.length, 2);
|
|
700
|
+
assert.equal(searchCalls[1].recent_not_view, "近14天没有");
|
|
701
|
+
assert.equal(screenCalls.length, 2);
|
|
702
|
+
assert.equal(screenCalls[1].resume, true);
|
|
703
|
+
assert.equal(refreshCalls, 1);
|
|
704
|
+
assert.equal(reloadCalls, 1);
|
|
705
|
+
assert.equal(result.result.candidate_count, 11);
|
|
706
|
+
assert.equal(result.result.auto_recovery.action, "reload_page_and_rerun_search");
|
|
707
|
+
assert.equal(result.result.auto_recovery.refresh.state, "LIST_NOT_RELOADED");
|
|
708
|
+
assert.equal(result.search_params.recent_not_view, "近14天没有");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function testPageExhaustedBeforeTargetShouldFailAfterFiveRecoveryAttempts() {
|
|
712
|
+
const searchCalls = [];
|
|
713
|
+
const screenCalls = [];
|
|
714
|
+
let refreshCalls = 0;
|
|
715
|
+
const parsed = createParsed();
|
|
716
|
+
parsed.searchParams = {
|
|
717
|
+
...parsed.searchParams,
|
|
718
|
+
recent_not_view: "不限"
|
|
719
|
+
};
|
|
720
|
+
const result = await runRecommendPipeline(
|
|
721
|
+
{
|
|
722
|
+
workspaceRoot: process.cwd(),
|
|
723
|
+
instruction: "test",
|
|
724
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
725
|
+
overrides: {},
|
|
726
|
+
resume: {
|
|
727
|
+
resume: false,
|
|
728
|
+
output_csv: "C:/temp/resume.csv",
|
|
729
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
730
|
+
pause_control_path: "C:/temp/run.json",
|
|
731
|
+
previous_completion_reason: ""
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
parseRecommendInstruction: () => parsed,
|
|
736
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
737
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } }),
|
|
738
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
739
|
+
refreshBossRecommendList: async () => {
|
|
740
|
+
refreshCalls += 1;
|
|
741
|
+
return {
|
|
742
|
+
ok: true,
|
|
743
|
+
action: "in_page_refresh",
|
|
744
|
+
state: "RECOMMEND_READY",
|
|
745
|
+
before_state: {
|
|
746
|
+
finished_wrap_visible: true,
|
|
747
|
+
refresh_button_visible: true
|
|
748
|
+
},
|
|
749
|
+
after_state: {
|
|
750
|
+
finished_wrap_visible: false,
|
|
751
|
+
list_ready: true
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
runRecommendSearchCli: async ({ searchParams }) => {
|
|
756
|
+
searchCalls.push({ ...searchParams });
|
|
757
|
+
return {
|
|
758
|
+
ok: true,
|
|
759
|
+
summary: {
|
|
760
|
+
candidate_count: 9,
|
|
761
|
+
applied_filters: searchParams,
|
|
762
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
runRecommendScreenCli: async ({ resume }) => {
|
|
767
|
+
screenCalls.push({ ...resume });
|
|
768
|
+
return {
|
|
769
|
+
ok: false,
|
|
770
|
+
error: {
|
|
771
|
+
code: "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED",
|
|
772
|
+
message: "推荐列表已到底,但尚未达到目标数。",
|
|
773
|
+
page_exhaustion: {
|
|
774
|
+
reason: "bottom_reached",
|
|
775
|
+
bottom: {
|
|
776
|
+
isBottom: true,
|
|
777
|
+
finished_wrap_visible: true,
|
|
778
|
+
refresh_button_visible: true
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
summary: {
|
|
783
|
+
processed_count: 4,
|
|
784
|
+
passed_count: 1,
|
|
785
|
+
skipped_count: 3,
|
|
786
|
+
output_csv: "C:/temp/resume.csv",
|
|
787
|
+
checkpoint_path: "C:/temp/checkpoint.json",
|
|
788
|
+
completion_reason: "page_exhausted_before_target_count"
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
assert.equal(result.status, "FAILED");
|
|
796
|
+
assert.equal(result.error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
|
|
797
|
+
assert.match(result.error.message, /已达到自动恢复上限 5 次/);
|
|
798
|
+
assert.equal(searchCalls.length, 1);
|
|
799
|
+
assert.equal(screenCalls.length, 6);
|
|
800
|
+
assert.equal(refreshCalls, 5);
|
|
801
|
+
assert.equal(result.partial_result.output_csv, "C:/temp/resume.csv");
|
|
802
|
+
assert.equal(result.diagnostics.auto_recovery.attempt, 5);
|
|
803
|
+
assert.equal(result.diagnostics.auto_recovery.action, "in_page_refresh");
|
|
804
|
+
assert.equal(result.diagnostics.auto_recovery.partial_result.checkpoint_path, "C:/temp/checkpoint.json");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function testNullTargetCountShouldKeepPageExhaustedCompletion() {
|
|
808
|
+
const parsed = createParsed();
|
|
809
|
+
parsed.screenParams = {
|
|
810
|
+
...parsed.screenParams,
|
|
811
|
+
target_count: null
|
|
812
|
+
};
|
|
813
|
+
let receivedScreenParams = null;
|
|
814
|
+
const result = await runRecommendPipeline(
|
|
815
|
+
{
|
|
816
|
+
workspaceRoot: process.cwd(),
|
|
817
|
+
instruction: "test",
|
|
818
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
819
|
+
overrides: {}
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
parseRecommendInstruction: () => parsed,
|
|
823
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
824
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
825
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
826
|
+
runRecommendSearchCli: async () => ({
|
|
827
|
+
ok: true,
|
|
828
|
+
summary: {
|
|
829
|
+
candidate_count: 9,
|
|
830
|
+
applied_filters: {},
|
|
831
|
+
page_state: { state: "RECOMMEND_READY" }
|
|
832
|
+
}
|
|
833
|
+
}),
|
|
834
|
+
runRecommendScreenCli: async ({ screenParams }) => {
|
|
835
|
+
receivedScreenParams = screenParams;
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
summary: {
|
|
839
|
+
processed_count: 4,
|
|
840
|
+
passed_count: 1,
|
|
841
|
+
skipped_count: 3,
|
|
842
|
+
output_csv: "C:/temp/resume.csv",
|
|
843
|
+
completion_reason: "page_exhausted"
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
assert.equal(receivedScreenParams.target_count, null);
|
|
851
|
+
assert.equal(result.status, "COMPLETED");
|
|
852
|
+
assert.equal(result.result.completion_reason, "page_exhausted");
|
|
853
|
+
assert.equal(result.result.auto_recovery, null);
|
|
854
|
+
}
|
|
855
|
+
|
|
357
856
|
async function testNeedConfirmationGate() {
|
|
358
857
|
let preflightCalled = false;
|
|
359
858
|
const result = await runRecommendPipeline(
|
|
@@ -797,8 +1296,7 @@ async function testPreflightRecoveryPlanOrder() {
|
|
|
797
1296
|
checks: [
|
|
798
1297
|
{ key: "node_cli", ok: false },
|
|
799
1298
|
{ key: "npm_dep_ws", ok: false, install_cwd: "C:/workspace/boss-recommend-mcp" },
|
|
800
|
-
{ key: "
|
|
801
|
-
{ key: "python_pillow", ok: false }
|
|
1299
|
+
{ key: "npm_dep_sharp", ok: false, install_cwd: "C:/workspace/boss-recommend-mcp" }
|
|
802
1300
|
]
|
|
803
1301
|
}),
|
|
804
1302
|
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
@@ -811,10 +1309,9 @@ async function testPreflightRecoveryPlanOrder() {
|
|
|
811
1309
|
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
812
1310
|
assert.deepEqual(
|
|
813
1311
|
result.diagnostics.recovery.ordered_steps.map((item) => item.id),
|
|
814
|
-
["install_nodejs", "install_npm_dependencies"
|
|
1312
|
+
["install_nodejs", "install_npm_dependencies"]
|
|
815
1313
|
);
|
|
816
1314
|
assert.deepEqual(result.diagnostics.recovery.ordered_steps[1].blocked_by, ["install_nodejs"]);
|
|
817
|
-
assert.deepEqual(result.diagnostics.recovery.ordered_steps[3].blocked_by, ["install_python"]);
|
|
818
1315
|
assert.equal(result.diagnostics.recovery.agent_prompt.includes("不要并行跳步"), true);
|
|
819
1316
|
}
|
|
820
1317
|
|
|
@@ -1036,6 +1533,11 @@ async function main() {
|
|
|
1036
1533
|
await testResumeFromScreenPauseShouldSkipSearch();
|
|
1037
1534
|
await testResumeFromPausedBeforeScreenShouldRerunSearch();
|
|
1038
1535
|
await testConsecutiveResumeCaptureFailuresShouldRefreshAndRerunSearchWithForcedRecentFilter();
|
|
1536
|
+
await testPageExhaustedBeforeTargetShouldRefreshInPageAndResumeScreen();
|
|
1537
|
+
await testPageExhaustedBeforeTargetShouldReloadWhenRefreshButtonMissing();
|
|
1538
|
+
await testPageExhaustedBeforeTargetShouldReloadWhenRefreshDoesNotRecoverList();
|
|
1539
|
+
await testPageExhaustedBeforeTargetShouldFailAfterFiveRecoveryAttempts();
|
|
1540
|
+
await testNullTargetCountShouldKeepPageExhaustedCompletion();
|
|
1039
1541
|
await testNeedConfirmationGate();
|
|
1040
1542
|
await testNeedSchoolTagConfirmationGate();
|
|
1041
1543
|
await testNeedTargetCountConfirmationGate();
|