@reconcrap/boss-recommend-mcp 1.1.2 → 1.1.4

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.
@@ -0,0 +1,245 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const { RecommendScreenCli, __testables } = require("./boss-recommend-screen-cli.cjs");
7
+ const { __testables: captureTestables } = require("./scripts/capture-full-resume-canvas.cjs");
8
+
9
+ class FakeRecommendScreenCli extends RecommendScreenCli {
10
+ constructor(args, options = {}) {
11
+ super(args);
12
+ this.testCandidates = options.candidates || [];
13
+ this.captureOutcomes = options.captureOutcomes || new Map();
14
+ this.screeningByKey = options.screeningByKey || new Map();
15
+ this.discoveryCalls = 0;
16
+ this.lastCapturedCandidateKey = null;
17
+ }
18
+
19
+ async connect() {}
20
+
21
+ async disconnect() {}
22
+
23
+ async getDetailClosedState() {
24
+ return { closed: true, reason: "test" };
25
+ }
26
+
27
+ async closeDetailPage() {
28
+ return true;
29
+ }
30
+
31
+ async waitForListReady() {
32
+ return true;
33
+ }
34
+
35
+ async ensureHealthyListViewport() {
36
+ return {
37
+ ok: true,
38
+ state: { ok: true }
39
+ };
40
+ }
41
+
42
+ async discoverCandidates() {
43
+ if (this.discoveryCalls === 0) {
44
+ for (const candidate of this.testCandidates) {
45
+ this.candidateByKey.set(candidate.key, candidate);
46
+ this.discoveredKeys.add(candidate.key);
47
+ this.candidateQueue.push(candidate.key);
48
+ this.insertCounter += 1;
49
+ this.insertedAt.set(candidate.key, this.insertCounter);
50
+ }
51
+ this.discoveryCalls += 1;
52
+ return {
53
+ ok: true,
54
+ added: this.testCandidates.length,
55
+ candidate_count: this.testCandidates.length,
56
+ total_cards: this.testCandidates.length
57
+ };
58
+ }
59
+ this.discoveryCalls += 1;
60
+ return {
61
+ ok: true,
62
+ added: 0,
63
+ candidate_count: this.testCandidates.length,
64
+ total_cards: this.testCandidates.length
65
+ };
66
+ }
67
+
68
+ async scrollAndLoadMore() {
69
+ return {
70
+ before: {
71
+ candidateCount: this.testCandidates.length,
72
+ scrollTop: 0,
73
+ scrollHeight: 100
74
+ },
75
+ after: {
76
+ candidateCount: this.testCandidates.length,
77
+ scrollTop: 0,
78
+ scrollHeight: 100
79
+ },
80
+ bottom: {
81
+ isBottom: true
82
+ }
83
+ };
84
+ }
85
+
86
+ async clickCandidate() {}
87
+
88
+ async ensureDetailOpen() {
89
+ return true;
90
+ }
91
+
92
+ async captureResumeImage(candidate) {
93
+ const outcome = this.captureOutcomes.get(candidate.key);
94
+ if (outcome instanceof Error) {
95
+ throw outcome;
96
+ }
97
+ this.lastCapturedCandidateKey = candidate.key;
98
+ return outcome || {
99
+ stitchedImage: path.join(os.tmpdir(), `${candidate.key}.png`)
100
+ };
101
+ }
102
+
103
+ async callVisionModel() {
104
+ return this.screeningByKey.get(this.lastCapturedCandidateKey) || {
105
+ passed: false,
106
+ reason: "not matched",
107
+ summary: "not matched"
108
+ };
109
+ }
110
+
111
+ async favoriteCandidate() {
112
+ return { actionTaken: "favorite" };
113
+ }
114
+
115
+ async greetCandidate() {
116
+ return { actionTaken: "greet" };
117
+ }
118
+
119
+ async takeBreakIfNeeded() {}
120
+
121
+ saveCsv() {}
122
+
123
+ saveCheckpoint() {}
124
+ }
125
+
126
+ function createResumeCaptureError(message = "Resume canvas not found") {
127
+ const error = new Error(message);
128
+ error.code = "RESUME_CAPTURE_FAILED";
129
+ error.retryable = true;
130
+ return error;
131
+ }
132
+
133
+ function createArgs(tempDir) {
134
+ return {
135
+ baseUrl: "https://example.invalid/v1",
136
+ apiKey: "test-key",
137
+ model: "test-model",
138
+ criteria: "test criteria",
139
+ targetCount: null,
140
+ maxGreetCount: null,
141
+ port: 9222,
142
+ output: path.join(tempDir, "result.csv"),
143
+ checkpointPath: path.join(tempDir, "checkpoint.json"),
144
+ pauseControlPath: path.join(tempDir, "pause.json"),
145
+ resume: false,
146
+ postAction: "none",
147
+ postActionConfirmed: true,
148
+ help: false,
149
+ __provided: {
150
+ baseUrl: true,
151
+ apiKey: true,
152
+ model: true,
153
+ criteria: true,
154
+ targetCount: true,
155
+ maxGreetCount: false,
156
+ port: true,
157
+ postAction: true,
158
+ postActionConfirmed: true
159
+ }
160
+ };
161
+ }
162
+
163
+ function testShouldAbortResumeProbeEarly() {
164
+ const probe = {
165
+ ok: false,
166
+ reason: "NO_CRESUME_IFRAME",
167
+ debug: {
168
+ activeScopeCount: 0,
169
+ totalResumeIframes: 0,
170
+ visibleResumeIframes: 0
171
+ }
172
+ };
173
+ const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
174
+ probe,
175
+ stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
176
+ elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
177
+ waitResumeMs: 60000
178
+ });
179
+ assert.equal(shouldAbort, true);
180
+ }
181
+
182
+ async function testSingleResumeCaptureFailureIsSkipped() {
183
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
184
+ const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
185
+ const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
186
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
187
+ candidates: [badCandidate, goodCandidate],
188
+ captureOutcomes: new Map([
189
+ ["bad", createResumeCaptureError()],
190
+ ["good", { stitchedImage: path.join(tempDir, "good.png") }]
191
+ ]),
192
+ screeningByKey: new Map([
193
+ ["good", { passed: true, reason: "matched", summary: "matched" }]
194
+ ])
195
+ });
196
+
197
+ const result = await cli.run();
198
+ assert.equal(result.status, "COMPLETED");
199
+ assert.equal(result.result.processed_count, 2);
200
+ assert.equal(result.result.passed_count, 1);
201
+ assert.equal(result.result.skipped_count, 1);
202
+ assert.equal(cli.consecutiveResumeCaptureFailures, 0);
203
+ }
204
+
205
+ async function testConsecutiveResumeCaptureFailuresStillAbort() {
206
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
207
+ const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
208
+ const candidates = Array.from({ length: maxFailures }, (_, index) => ({
209
+ key: `fail-${index + 1}`,
210
+ geek_id: `fail-${index + 1}`,
211
+ name: `fail-${index + 1}`
212
+ }));
213
+ const captureOutcomes = new Map(
214
+ candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
215
+ );
216
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
217
+ candidates,
218
+ captureOutcomes
219
+ });
220
+
221
+ await assert.rejects(
222
+ () => cli.run(),
223
+ (error) => {
224
+ assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
225
+ assert.match(error.message, /连续 .* 位候选人简历捕获失败/);
226
+ assert.equal(error.rollback?.rollback_count, maxFailures);
227
+ assert.equal(error.partial_result?.processed_count, 0);
228
+ assert.equal(error.partial_result?.skipped_count, 0);
229
+ assert.deepEqual(Array.from(cli.processedKeys), []);
230
+ return true;
231
+ }
232
+ );
233
+ }
234
+
235
+ async function main() {
236
+ testShouldAbortResumeProbeEarly();
237
+ await testSingleResumeCaptureFailureIsSkipped();
238
+ await testConsecutiveResumeCaptureFailuresStillAbort();
239
+ console.log("recoverable resume failure tests passed");
240
+ }
241
+
242
+ main().catch((error) => {
243
+ console.error(error);
244
+ process.exit(1);
245
+ });