@reconcrap/boss-recommend-mcp 1.2.7 → 1.2.9

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.
@@ -124,6 +124,30 @@ class FakeRecommendScreenCli extends RecommendScreenCli {
124
124
  saveCheckpoint() {}
125
125
  }
126
126
 
127
+ class FakeDetailCloseProbeCli extends RecommendScreenCli {
128
+ constructor(args, options = {}) {
129
+ super(args);
130
+ this.listReady = options.listReady === true;
131
+ this.evaluateCallCount = 0;
132
+ }
133
+
134
+ async getDetailClosedState() {
135
+ return { closed: false, reason: "popup visible: .boss-popup__wrapper" };
136
+ }
137
+
138
+ async evaluate() {
139
+ this.evaluateCallCount += 1;
140
+ if (this.evaluateCallCount >= 2) {
141
+ return this.listReady
142
+ ? { ok: true, candidate_count: 1 }
143
+ : { ok: false, error: "LIST_NOT_READY" };
144
+ }
145
+ return { ok: false, error: "CLOSE_ACTION_NOOP" };
146
+ }
147
+
148
+ async pressEsc() {}
149
+ }
150
+
127
151
  function createResumeCaptureError(message = "Resume canvas not found") {
128
152
  const error = new Error(message);
129
153
  error.code = "RESUME_CAPTURE_FAILED";
@@ -306,6 +330,55 @@ async function testPageExhaustedWithoutTargetShouldStillComplete() {
306
330
  assert.equal(result.result.completion_reason, "page_exhausted");
307
331
  }
308
332
 
333
+ async function testTargetCountShouldStopWhenPassedCountReached() {
334
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-stop-"));
335
+ const args = createArgs(tempDir);
336
+ args.targetCount = 1;
337
+ const first = { key: "pass-1", geek_id: "pass-1", name: "pass-1" };
338
+ const second = { key: "skip-2", geek_id: "skip-2", name: "skip-2" };
339
+ const cli = new FakeRecommendScreenCli(args, {
340
+ candidates: [first, second],
341
+ screeningByKey: new Map([
342
+ ["pass-1", { passed: true, reason: "matched", summary: "matched" }],
343
+ ["skip-2", { passed: false, reason: "not matched", summary: "not matched" }]
344
+ ])
345
+ });
346
+
347
+ const result = await cli.run();
348
+ assert.equal(result.status, "COMPLETED");
349
+ assert.equal(result.result.processed_count, 1);
350
+ assert.equal(result.result.passed_count, 1);
351
+ assert.equal(result.result.skipped_count, 0);
352
+ assert.equal(result.result.completion_reason, "target_count_reached");
353
+ }
354
+
355
+ async function testTargetCountShouldNotTreatProcessedCountAsReached() {
356
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-only-"));
357
+ const args = createArgs(tempDir);
358
+ args.targetCount = 1;
359
+ const first = { key: "skip-a", geek_id: "skip-a", name: "skip-a" };
360
+ const second = { key: "skip-b", geek_id: "skip-b", name: "skip-b" };
361
+ const cli = new FakeRecommendScreenCli(args, {
362
+ candidates: [first, second],
363
+ screeningByKey: new Map([
364
+ ["skip-a", { passed: false, reason: "not matched", summary: "not matched" }],
365
+ ["skip-b", { passed: false, reason: "not matched", summary: "not matched" }]
366
+ ])
367
+ });
368
+
369
+ await assert.rejects(
370
+ () => cli.run(),
371
+ (error) => {
372
+ assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
373
+ assert.equal(error.retryable, true);
374
+ assert.equal(error.partial_result?.processed_count, 2);
375
+ assert.equal(error.partial_result?.passed_count, 0);
376
+ assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
377
+ return true;
378
+ }
379
+ );
380
+ }
381
+
309
382
  async function testFeaturedShouldUseNetworkResumeOnly() {
310
383
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
311
384
  const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
@@ -842,12 +915,152 @@ function testParseArgsShouldSupportLatestPageScope() {
842
915
  assert.equal(parsed.port, 9222);
843
916
  }
844
917
 
918
+ async function testCallTextModelShouldNotTruncateLongResume() {
919
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-full-"));
920
+ const cli = new RecommendScreenCli(createArgs(tempDir));
921
+ const marker = "__END_OF_RESUME_MARKER__";
922
+ const resumeText = `${"A".repeat(32000)}${marker}`;
923
+ const originalFetch = global.fetch;
924
+ let capturedUserContent = "";
925
+ global.fetch = async (_url, options = {}) => {
926
+ const payload = JSON.parse(String(options.body || "{}"));
927
+ capturedUserContent = String(payload?.messages?.[1]?.content || "");
928
+ return {
929
+ ok: true,
930
+ status: 200,
931
+ async json() {
932
+ return {
933
+ choices: [
934
+ {
935
+ message: {
936
+ content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"A\"]}"
937
+ }
938
+ }
939
+ ]
940
+ };
941
+ }
942
+ };
943
+ };
944
+ try {
945
+ const result = await cli.callTextModel(resumeText);
946
+ assert.equal(result.passed, false);
947
+ assert.equal(capturedUserContent.includes(marker), true);
948
+ } finally {
949
+ global.fetch = originalFetch;
950
+ }
951
+ }
952
+
953
+ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
954
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-chunk-fallback-"));
955
+ const cli = new RecommendScreenCli(createArgs(tempDir));
956
+ const originalFetch = global.fetch;
957
+ const prevChunkSize = process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
958
+ const prevChunkOverlap = process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
959
+ const prevMaxChunks = process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
960
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = "80";
961
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = "0";
962
+ process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = "6";
963
+
964
+ const passMarker = "PASS_MARKER_ABC";
965
+ const resumeText = `${"x".repeat(120)}${passMarker}${"y".repeat(120)}`;
966
+ let callCount = 0;
967
+ global.fetch = async (_url, options = {}) => {
968
+ callCount += 1;
969
+ if (callCount === 1) {
970
+ return {
971
+ ok: false,
972
+ status: 400,
973
+ async text() {
974
+ return "maximum context length exceeded";
975
+ }
976
+ };
977
+ }
978
+
979
+ const payload = JSON.parse(String(options.body || "{}"));
980
+ const userContent = String(payload?.messages?.[1]?.content || "");
981
+ const passed = userContent.includes(passMarker);
982
+ const response = passed
983
+ ? "{\"passed\": true, \"reason\": \"命中证据\", \"summary\": \"命中\", \"evidence\": [\"PASS_MARKER_ABC\"]}"
984
+ : "{\"passed\": false, \"reason\": \"本段证据不足\", \"summary\": \"不足\", \"evidence\": []}";
985
+ return {
986
+ ok: true,
987
+ status: 200,
988
+ async json() {
989
+ return {
990
+ choices: [
991
+ {
992
+ message: {
993
+ content: response
994
+ }
995
+ }
996
+ ]
997
+ };
998
+ }
999
+ };
1000
+ };
1001
+ try {
1002
+ const result = await cli.callTextModel(resumeText);
1003
+ assert.equal(result.passed, true);
1004
+ assert.equal(callCount >= 2, true);
1005
+ assert.equal(Array.isArray(result.evidence), true);
1006
+ } finally {
1007
+ global.fetch = originalFetch;
1008
+ if (prevChunkSize === undefined) {
1009
+ delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
1010
+ } else {
1011
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = prevChunkSize;
1012
+ }
1013
+ if (prevChunkOverlap === undefined) {
1014
+ delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
1015
+ } else {
1016
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = prevChunkOverlap;
1017
+ }
1018
+ if (prevMaxChunks === undefined) {
1019
+ delete process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
1020
+ } else {
1021
+ process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = prevMaxChunks;
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
1027
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
1028
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1029
+ const imagePath = path.join(tempDir, "long.png");
1030
+ await sharp({
1031
+ create: { width: 400, height: 1200, channels: 3, background: { r: 240, g: 240, b: 240 } }
1032
+ }).png().toFile(imagePath);
1033
+
1034
+ const prepared = await cli.prepareVisionImageSegmentsForModel(imagePath, 120000, "test");
1035
+ assert.equal(Array.isArray(prepared.imagePaths), true);
1036
+ assert.equal(prepared.imagePaths.length > 1, true);
1037
+ for (const segmentPath of prepared.imagePaths) {
1038
+ assert.equal(fs.existsSync(segmentPath), true);
1039
+ }
1040
+ }
1041
+
1042
+ async function testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady() {
1043
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-fail-"));
1044
+ const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: false });
1045
+ const closed = await cli.closeDetailPage(1);
1046
+ assert.equal(closed, false);
1047
+ }
1048
+
1049
+ async function testCloseDetailPageShouldContinueWhenListReady() {
1050
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-list-ready-"));
1051
+ const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: true });
1052
+ const closed = await cli.closeDetailPage(1);
1053
+ assert.equal(closed, true);
1054
+ }
1055
+
845
1056
  async function main() {
846
1057
  testShouldAbortResumeProbeEarly();
847
1058
  await testSingleResumeCaptureFailureIsSkipped();
848
1059
  await testConsecutiveResumeCaptureFailuresStillAbort();
849
1060
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
850
1061
  await testPageExhaustedWithoutTargetShouldStillComplete();
1062
+ await testTargetCountShouldStopWhenPassedCountReached();
1063
+ await testTargetCountShouldNotTreatProcessedCountAsReached();
851
1064
  await testFeaturedShouldUseNetworkResumeOnly();
852
1065
  await testRecommendShouldPreferNetworkResumeWhenAvailable();
853
1066
  await testNetworkMissShouldFallbackToImageCapture();
@@ -871,6 +1084,11 @@ async function main() {
871
1084
  testStitchWithAvailablePythonShouldFailWhenScriptMissing();
872
1085
  testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
873
1086
  testParseArgsShouldSupportLatestPageScope();
1087
+ await testCallTextModelShouldNotTruncateLongResume();
1088
+ await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
1089
+ await testPrepareVisionImageSegmentsShouldSplitLongImage();
1090
+ await testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady();
1091
+ await testCloseDetailPageShouldContinueWhenListReady();
874
1092
  console.log("recoverable resume failure tests passed");
875
1093
  }
876
1094