@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.
@@ -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
 
@@ -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: "python_cli", ok: false },
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", "install_python", "install_pillow"]
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();