@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.
- package/bin/boss-recommend-mcp.js +2 -2
- package/config/screening-config.example.json +7 -7
- package/package.json +62 -62
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +244 -244
- package/src/adapters.js +293 -38
- package/src/pipeline.js +1453 -1432
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +469 -469
- package/src/test-index-async.js +264 -264
- package/src/test-pipeline.js +67 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +4052 -3721
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +141 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +880 -832
- package/vendor/boss-recommend-search-cli/src/cli.js +1650 -1650
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +211 -211
|
@@ -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
|
|
342
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-recommend-
|
|
343
|
-
const candidate = { key: "
|
|
344
|
-
const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
|
|
345
|
-
candidates: [candidate]
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
assert.equal(result.
|
|
362
|
-
assert.equal(result.result.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
assert.equal(result.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
const cli = new FakeRecommendScreenCli(
|
|
390
|
-
candidates: [
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
cli.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
args.
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
args.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
assert.equal(
|
|
596
|
-
assert.equal(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
assert.equal(
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
"
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
assert.equal(
|
|
710
|
-
assert.equal(
|
|
711
|
-
assert.equal(
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
assert.equal(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
+
});
|